Added some assertions to account.py. Cleaned up and simplified the
authorScott Gasch <[email protected]>
Tue, 14 Jan 2020 20:37:29 +0000 (12:37 -0800)
committerScott Gasch <[email protected]>
Tue, 14 Jan 2020 20:37:29 +0000 (12:37 -0800)
go_find_money method in retire.py.

accounts.py
constants.py
retire.py

index 81ab871e96974ce32992907e65403b74e9016279..5f0def0479bf33221e829f6939bf834a742941dc 100644 (file)
@@ -11,6 +11,7 @@ class account(object):
     def __init__(self, name, owner):
         """Constructor for account object takes a name and owner"""
         self.name = name
+        assert constants.is_valid_owner(owner), "Bad account owner"
         self.owner = owner
 
     def get_name(self):
@@ -26,6 +27,7 @@ class account(object):
         return self.get_owner() == constants.LYNN
 
     def get_balance(self):
+        """Return the current account balance."""
         pass
 
     def appreciate(self, multiplier):
@@ -39,6 +41,7 @@ class account(object):
         pass
 
     def deposit(self, amount):
+        """Deposit money into the account."""
         pass
 
     def is_age_restricted(self):
@@ -51,12 +54,15 @@ class account(object):
         pass
 
     def do_rmd_withdrawal(self, owner_age, taxes):
+        """Handle RMD withdrawals for account."""
         pass
 
     def has_roth(self):
+        """Does this account have a Roth part?"""
         pass
 
     def dump_final_report(self):
+        """Output account-specific info for the final simulation report."""
         print "  " + self.name + ":"
 
 class age_restricted_tax_deferred_account(account):
@@ -68,6 +74,8 @@ class age_restricted_tax_deferred_account(account):
            in the account that are in-plan Roth and can be taken out tax
            free.  It keeps this pile separate from the pretax pile and
            tries to estimate taxes."""
+        assert total_balance >= 0, "Initial balance must be >= 0"
+        assert roth_amount_subset <= total_balance, "Roth subset too high!"
         self.roth = roth_amount_subset
         self.pretax = total_balance - roth_amount_subset
         self.total_roth_withdrawals = 0
@@ -75,6 +83,7 @@ class age_restricted_tax_deferred_account(account):
         self.total_investment_gains = 0
         self.total_roth_conversions = 0
         self.initial_balance = self.get_balance()
+        assert self.initial_balance >= 0, "Bad initial balance"
 
         # This calls the super class' c'tor.
         account.__init__(self, name, owner)
@@ -89,9 +98,9 @@ class age_restricted_tax_deferred_account(account):
            (which are available tax-free).  Assume that the ratio of pretax
            to Roth in this overall account determines the amount from each
            partition in this withdrawal."""
+        if amount <= 0: return 0
         balance = self.get_balance()
-        if balance < amount:
-            raise Exception("Insufficient funds")
+        if balance < amount: raise Exception("Insufficient funds")
 
         ratio = float(self.roth) / float(balance)
         roth_part = amount * ratio
@@ -111,8 +120,6 @@ class age_restricted_tax_deferred_account(account):
         self.pretax_part += amount
 
     def appreciate(self, multiplier):
-        """In this class we basically ignore the balance field in favor of
-           just using pretax and roth so that we can track them separately."""
         old_pretax = self.pretax
         self.pretax *= multiplier
         self.total_investment_gains += self.pretax - old_pretax
@@ -242,6 +249,7 @@ class age_restricted_tax_deferred_account(account):
         amount = min(amount, self.pretax)
         self.roth += amount
         self.pretax -= amount
+        assert self.pretax >= 0, "Somehow exhausted more than all pretax money"
         self.total_roth_conversions += amount
         taxes.record_ordinary_income(amount)
         print "## Executed pre-tax --> Roth conversion of %s in %s" % (
@@ -275,6 +283,7 @@ class age_restricted_roth_account(age_restricted_tax_deferred_account):
            in the account that are in-plan Roth and can be taken out tax
            free.  It keeps this pile separate from the pretax pile and
            tries to estimate taxes."""
+        assert total_balance >= 0, "Initial balance must be >= 0"
         age_restricted_tax_deferred_account.__init__(
             self, total_balance, total_balance, name, owner)
 
@@ -302,6 +311,8 @@ class brokerage_account(account):
            taxes on long_term_gain (and qualified dividends, which are not
            modeled) are usually lower than short_term_gain.  Today those
            are taxed at 15% and as ordinary income, respectively."""
+        assert total_balance >= 0, "Initial balance must be >= 0"
+        assert cost_basis <= total_balance, "Bad initial cost basis"
         self.cost_basis = float(cost_basis)
         self.short_term_gain = 0.0
         self.long_term_gain = float(total_balance - cost_basis)
@@ -321,9 +332,9 @@ class brokerage_account(account):
            (and the cost_basis part isn't).  Assume that the ratio of
            cost_basis to overall balance can be used to determine how much
            of the withdrawal will be taxed (and how)."""
+        if amount <= 0: return 0
         balance = self.get_balance()
-        if balance < amount:
-            raise Exception("Insufficient funds")
+        if balance < amount: raise Exception("Insufficient funds")
 
         ratio = self.cost_basis / balance
         invested_capital_part = amount * ratio
@@ -340,6 +351,7 @@ class brokerage_account(account):
     def withdraw_from_gains(self, amount, taxes):
         """Withdraw some money from gains.  Prefer the long term ones if
            possible."""
+        if amount <= 0: return 0
         if amount > (self.long_term_gain + self.short_term_gain):
             raise Exception("Insufficient funds")
 
@@ -370,6 +382,7 @@ class brokerage_account(account):
         taxes.record_short_term_gain(amount)
 
     def deposit(self, amount):
+        assert amount >= 0, "Can't deposit negative amounts"
         self.cost_basis += amount
 
     def get_balance(self):
index ab868879e44a095b68d6b74f2acb6e6d28a0d6fe..af0399af20a0532015e85161e0fc013e34648a74 100644 (file)
@@ -4,6 +4,10 @@ DEFAULT = 0
 SCOTT = 1
 LYNN = 2
 
+def is_valid_owner(owner):
+    """Is an owner valid?"""
+    return (owner >= DEFAULT and owner <= LYNN)
+
 PESSIMISTIC_FEDERAL_INCOME_TAX_BRACKETS = [
     [ 612351, 0.50 ],
     [ 408201, 0.45 ],
index 5fd5b12f729b8709797555bcc984530ee235c811..270c608e094d4a19d76a958e3708563839baf4b4 100755 (executable)
--- a/retire.py
+++ b/retire.py
@@ -34,89 +34,45 @@ class simulation(object):
                 total_withdrawn += rmd
         return total_withdrawn
 
-    def go_find_money(self, amount, taxes):
+    def go_find_money(self, amount_needed, taxes):
         """Look through accounts and try to find amount using some heuristics
            about where to withdraw money first."""
 
         # Try brokerage accounts first
         for x in self.accounts:
             if not x.is_age_restricted():
-                if x.get_balance() >= amount:
-                    print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name())
-                    x.withdraw(amount, taxes)
-                    return
-
-                elif x.get_balance() > 0 and x.get_balance() < amount:
-                    money_available = x.get_balance()
-                    print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name())
-                    x.withdraw(money_available, taxes)
-                    amount -= money_available
+                amount_to_withdraw = min(amount_needed, x.get_balance())
+                print "## Withdrawing %s from %s" % (utils.format_money(amount_to_withdraw), x.get_name())
+                x.withdraw(amount_to_withdraw, taxes)
+                amount_needed -= amount_to_withdraw
+                if amount_needed <= 0: return
 
         # Next try age restircted accounts
         for x in self.accounts:
             if (x.is_age_restricted() and
                 x.has_rmd() and
-                x.belongs_to_lynn() and
-                self.lynn_age > 60):
-
-                if x.get_balance() >= amount:
-                    print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name())
-                    x.withdraw(amount, taxes)
-                    return
+                ((x.belongs_to_lynn() and self.lynn_age > 60) or
+                 (x.belongs_to_scott() and self.scott_age > 60))):
 
-                elif x.get_balance() > 0 and x.get_balance() < amount:
-                    money_available = x.get_balance()
-                    print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name())
-                    x.withdraw(money_available, taxes)
-                    amount -= money_available
-
-            if (x.is_age_restricted() and
-                x.has_rmd() and
-                x.belongs_to_scott() and
-                self.scott_age > 60):
-                if x.get_balance() >= amount:
-                    print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name())
-                    x.withdraw(amount, taxes)
-                    return
-
-                elif x.get_balance() > 0 and x.get_balance() < amount:
-                    money_available = x.get_balance()
-                    print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name())
-                    x.withdraw(money_available, taxes)
-                    amount -= money_available
+                amount_to_withdraw = min(amount_needed, x.get_balance())
+                print "## Withdrawing %s from %s" % (utils.format_money(amount_to_withdraw), x.get_name())
+                x.withdraw(amount_to_withdraw, taxes)
+                amount_needed -= amount_to_withdraw
+                if amount_needed <= 0: return
 
         # Last try Roth accounts
         for x in self.accounts:
             if (x.is_age_restricted() and
                 x.has_roth() and
-                x.belongs_to_lynn() and
-                self.lynn_age > 60):
-                if x.get_balance() >= amount:
-                    print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name())
-                    x.withdraw(amount, taxes)
-                    return
-
-                elif x.get_balance() > 0 and x.get_balance() < amount:
-                    money_available = x.get_balance()
-                    print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name())
-                    x.withdraw(money_available, taxes)
-                    amount -= money_available
+                ((x.belongs_to_lynn() and self.lynn_age > 60) or
+                 (x.belongs_to_scott() and self.scott_age > 60))):
 
-            if (x.is_age_restricted() and
-                x.has_roth() and
-                x.belongs_to_scott() and
-                self.scott_age > 60):
-                if x.get_balance() >= amount:
-                    print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name())
-                    x.withdraw(amount, taxes)
-                    return
-
-                elif x.get_balance() > 0 and x.get_balance() < amount:
-                    money_available = x.get_balance()
-                    print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name())
-                    x.withdraw(money_available, taxes)
-                    amount -= money_available
-        raise Exception("Unable to find enough money this year!")
+                amount_to_withdraw = min(amount_needed, x.get_balance())
+                print "## Withdrawing %s from %s" % (utils.format_money(amount_to_withdraw), x.get_name())
+                x.withdraw(amount_to_withdraw, taxes)
+                amount_needed -= amount_to_withdraw
+                if amount_needed <= 0: return
+        raise Exception("Unable to find enough money this year, still need %s more!" % utils.format_money(amount_needed))
 
     def get_social_security(self,
                             scott_annual_social_security_dollars,
@@ -185,107 +141,107 @@ class simulation(object):
         adjusted_lynn_annual_social_security_dollars = (
             self.params.get_initial_social_security_benefit(constants.LYNN))
 
-        try:
-            for self.year in xrange(2020, 2080):
-                self.scott_age = self.year - 1974
-                self.lynn_age = self.year - 1964
-                self.alex_age = self.year - 2005
-
-                # Computed money needed this year based on inflated budget.
-                money_needed = adjusted_annual_expenses
-
-                # When Alex is in college, we need $50K more per year.
-                if self.alex_age > 18 and self.alex_age <= 22:
-                    money_needed += 50000
-
-                self.dump_annual_header(money_needed)
-
-                # Now, figure out how to find money to pay for this year
-                # and how much of it is taxable.
-                total_income = 0
-
-                # When we reach a certain age we have to take RMDs from
-                # some accounts.  Handle that here.
-                rmds = self.do_rmd_withdrawals(taxes)
-                if rmds > 0:
-                    print "## Satisfied %s of RMDs from age-restricted accounts." % utils.format_money(rmds)
-                    total_income += rmds
-                    money_needed -= rmds
-
-                # When we reach a certain age we are eligible for SS
-                # payments.
-                ss = self.get_social_security(adjusted_scott_annual_social_security_dollars,
-                                              adjusted_lynn_annual_social_security_dollars,
-                                              taxes)
-                if ss > 0:
-                    print "## Social security paid %s" % utils.format_money(ss)
-                    total_income += ss
-                    money_needed -= ss
-
-                # If we still need money, try to go find it.
-                if money_needed > 0:
-                    self.go_find_money(money_needed, taxes)
-                    total_income += money_needed
-                    money_needed = 0
-
-                look_for_conversions = True
-                while True:
-                    # Maybe do some opportunistic Roth conversions.
-                    taxes_due = taxes.approximate_taxes(
-                        self.params.get_federal_standard_deduction(),
-                        self.params.get_federal_ordinary_income_tax_brackets(),
-                        self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets())
-                    total_income = taxes.get_total_income()
-                    tax_rate = float(taxes_due) / float(total_income)
-                    print "INCOME: %s, TAXES: %s\n" % (utils.format_money(total_income), utils.format_money(taxes_due))
-
-                    if (look_for_conversions and
-                        tax_rate <= 0.14 and
-                        taxes_due < 20000 and
-                        self.year <= 2035):
-
-                        look_for_conversions = self.do_opportunistic_roth_conversions(taxes)
-                        # because these conversions affect taxes, spin
-                        # once more in the loop and recompute taxes
-                        # due and tax rate.
-                    else:
-                        break
-
-                # Pay taxes_due by going to find more money.  This is a
-                # bit hacky since withdrawing more money to cover taxes
-                # will, in turn, cause taxable income.  But I think taxes
-                # are low enough and this simulation is rough enough that
-                # we can ignore this.
-                if taxes_due > 0:
-                    print "## Estimated federal tax due: %s (this year's tax rate=%s)" % (
-                        utils.format_money(taxes_due),
-                        utils.format_rate(tax_rate))
-                    self.go_find_money(taxes_due, taxes)
+#        try:
+        for self.year in xrange(2020, 2080):
+            self.scott_age = self.year - 1974
+            self.lynn_age = self.year - 1964
+            self.alex_age = self.year - 2005
+
+            # Computed money needed this year based on inflated budget.
+            money_needed = adjusted_annual_expenses
+
+            # When Alex is in college, we need $50K more per year.
+            if self.alex_age > 18 and self.alex_age <= 22:
+                money_needed += 50000
+
+            self.dump_annual_header(money_needed)
+
+            # Now, figure out how to find money to pay for this year
+            # and how much of it is taxable.
+            total_income = 0
+
+            # When we reach a certain age we have to take RMDs from
+            # some accounts.  Handle that here.
+            rmds = self.do_rmd_withdrawals(taxes)
+            if rmds > 0:
+                print "## Satisfied %s of RMDs from age-restricted accounts." % utils.format_money(rmds)
+                total_income += rmds
+                money_needed -= rmds
+
+            # When we reach a certain age we are eligible for SS
+            # payments.
+            ss = self.get_social_security(adjusted_scott_annual_social_security_dollars,
+                                          adjusted_lynn_annual_social_security_dollars,
+                                          taxes)
+            if ss > 0:
+                print "## Social security paid %s" % utils.format_money(ss)
+                total_income += ss
+                money_needed -= ss
+
+            # If we still need money, try to go find it.
+            if money_needed > 0:
+                self.go_find_money(money_needed, taxes)
+                total_income += money_needed
+                money_needed = 0
+
+            look_for_conversions = True
+            while True:
+                # Maybe do some opportunistic Roth conversions.
+                taxes_due = taxes.approximate_taxes(
+                    self.params.get_federal_standard_deduction(),
+                    self.params.get_federal_ordinary_income_tax_brackets(),
+                    self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets())
+                total_income = taxes.get_total_income()
+                tax_rate = float(taxes_due) / float(total_income)
+                print "INCOME: %s, TAXES: %s\n" % (utils.format_money(total_income), utils.format_money(taxes_due))
+
+                if (look_for_conversions and
+                    tax_rate <= 0.14 and
+                    taxes_due < 20000 and
+                    self.year <= 2035):
+
+                    look_for_conversions = self.do_opportunistic_roth_conversions(taxes)
+                    # because these conversions affect taxes, spin
+                    # once more in the loop and recompute taxes
+                    # due and tax rate.
                 else:
-                    print "## No federal taxes due this year!"
-                taxes.record_taxes_paid_and_reset_for_next_year(taxes_due)
-
-                # Inflation and appreciation:
-                #   * Cost of living increases
-                #   * Social security benefits increase
-                #   * Tax brackets are adjusted for inflation
-                inflation_multiplier = self.params.get_average_inflation_multiplier()
-                adjusted_annual_expenses *= inflation_multiplier
-                for x in self.accounts:
-                    x.appreciate(self.params.get_average_investment_return_multiplier())
-                if self.scott_age >= self.params.get_initial_social_security_age(constants.SCOTT):
-                    adjusted_scott_annual_social_security_dollars *= self.params.get_average_social_security_multiplier()
-                if self.lynn_age >= self.params.get_initial_social_security_age(constants.LYNN):
-                    adjusted_lynn_annual_social_security_dollars *= self.params.get_average_social_security_multiplier()
-
-                self.params.get_federal_ordinary_income_tax_brackets().adjust_with_multiplier(inflation_multiplier)
-                self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets().adjust_with_multiplier(inflation_multiplier)
-        except:
-            print "Ran out of money!!!"
-            pass
-
-        finally:
-            self.dump_final_report(taxes)
+                    break
+
+            # Pay taxes_due by going to find more money.  This is a
+            # bit hacky since withdrawing more money to cover taxes
+            # will, in turn, cause taxable income.  But I think taxes
+            # are low enough and this simulation is rough enough that
+            # we can ignore this.
+            if taxes_due > 0:
+                print "## Estimated federal tax due: %s (this year's tax rate=%s)" % (
+                    utils.format_money(taxes_due),
+                    utils.format_rate(tax_rate))
+                self.go_find_money(taxes_due, taxes)
+            else:
+                print "## No federal taxes due this year!"
+            taxes.record_taxes_paid_and_reset_for_next_year(taxes_due)
+
+            # Inflation and appreciation:
+            #   * Cost of living increases
+            #   * Social security benefits increase
+            #   * Tax brackets are adjusted for inflation
+            inflation_multiplier = self.params.get_average_inflation_multiplier()
+            adjusted_annual_expenses *= inflation_multiplier
+            for x in self.accounts:
+                x.appreciate(self.params.get_average_investment_return_multiplier())
+            if self.scott_age >= self.params.get_initial_social_security_age(constants.SCOTT):
+                adjusted_scott_annual_social_security_dollars *= self.params.get_average_social_security_multiplier()
+            if self.lynn_age >= self.params.get_initial_social_security_age(constants.LYNN):
+                adjusted_lynn_annual_social_security_dollars *= self.params.get_average_social_security_multiplier()
+
+            self.params.get_federal_ordinary_income_tax_brackets().adjust_with_multiplier(inflation_multiplier)
+            self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets().adjust_with_multiplier(inflation_multiplier)
+#        except:
+#            print "Ran out of money!!!"
+#            pass
+
+#        finally:
+        self.dump_final_report(taxes)
 
 # main
 params = parameters().with_default_values()