Modify the roth conversion logic and simplify the code around that.
authorScott Gasch <[email protected]>
Tue, 14 Jan 2020 19:44:16 +0000 (11:44 -0800)
committerScott Gasch <[email protected]>
Tue, 14 Jan 2020 19:44:16 +0000 (11:44 -0800)
Fix a bug in accounts.py with Roth accounts not having a roth balance.

accounts.py
retire.py

index 7867d21b4b18836a502b4fac1c3c0df36b60958a..81ab871e96974ce32992907e65403b74e9016279 100644 (file)
@@ -101,11 +101,11 @@ class age_restricted_tax_deferred_account(account):
             self.total_roth_withdrawals += roth_part
             print "## Satisfying %s from %s with Roth money." % (utils.format_money(roth_part), self.name)
             taxes.record_roth_income(roth_part)
-
-        self.pretax -= pretax_part
-        self.total_pretax_withdrawals += pretax_part
-        print "## Satisfying %s from %s with pre-tax money." % (utils.format_money(pretax_part), self.name)
-        taxes.record_ordinary_income(pretax_part)
+        if pretax_part > 0:
+            self.pretax -= pretax_part
+            self.total_pretax_withdrawals += pretax_part
+            print "## Satisfying %s from %s with pre-tax money." % (utils.format_money(pretax_part), self.name)
+            taxes.record_ordinary_income(pretax_part)
 
     def deposit(self, amount):
         self.pretax_part += amount
@@ -237,27 +237,17 @@ class age_restricted_tax_deferred_account(account):
     def has_roth(self):
         return True
 
-    def do_roth_conversion(self, amount):
-        if amount <= 0:
-            return 0
-        if self.pretax >= amount:
-            self.roth += amount
-            self.pretax -= amount
-            self.total_roth_conversions += amount
-            print "## Executed pre-tax --> Roth conversion of %s in %s" % (
-                utils.format_money(amount),
-                self.get_name())
-            return amount
-        elif self.pretax > 0:
-            actual_amount = self.pretax
-            self.roth += actual_amount
-            self.pretax = 0
-            self.total_roth_conversions += actual_amount
-            print "## Executed pre-tax --> Roth conversion of %s in %s" % (
-                utils.format_money(actual_amount),
-                self.get_name())
-            return actual_amount
-        return 0
+    def do_roth_conversion(self, amount, taxes):
+        if amount <= 0: return 0
+        amount = min(amount, self.pretax)
+        self.roth += amount
+        self.pretax -= amount
+        self.total_roth_conversions += amount
+        taxes.record_ordinary_income(amount)
+        print "## Executed pre-tax --> Roth conversion of %s in %s" % (
+            utils.format_money(amount),
+            self.get_name())
+        return amount
 
     def dump_final_report(self):
         super(age_restricted_tax_deferred_account, self).dump_final_report()
@@ -286,7 +276,7 @@ class age_restricted_roth_account(age_restricted_tax_deferred_account):
            free.  It keeps this pile separate from the pretax pile and
            tries to estimate taxes."""
         age_restricted_tax_deferred_account.__init__(
-            self, total_balance, 0, name, owner)
+            self, total_balance, total_balance, name, owner)
 
     # Override
     def has_rmd(self):
@@ -297,7 +287,7 @@ class age_restricted_roth_account(age_restricted_tax_deferred_account):
         raise Exception("This account has no RMDs")
 
     # Override
-    def do_roth_conversion(self, amount):
+    def do_roth_conversion(self, amount, taxes):
         return 0
 
 class brokerage_account(account):
@@ -406,7 +396,7 @@ class brokerage_account(account):
     def has_roth(self):
         return False
 
-    def do_roth_conversion(self, amount):
+    def do_roth_conversion(self, amount, taxes):
         return 0
 
     def dump_final_report(self):
index 423e254d49e10a80738c5bd56cc6a8affa549b01..5fd5b12f729b8709797555bcc984530ee235c811 100755 (executable)
--- a/retire.py
+++ b/retire.py
@@ -143,9 +143,12 @@ class simulation(object):
             money_converted = 0
             for x in self.accounts:
                 if x.has_roth():
-                    amount = x.do_roth_conversion(desired_conversion_amount)
+                    amount = x.do_roth_conversion(desired_conversion_amount, taxes)
                     money_converted += amount
                     desired_conversion_amount -= amount
+            if money_converted > 0:
+                return True
+        return False
 
     def dump_report_header(self):
         self.params.dump()
@@ -225,25 +228,28 @@ class simulation(object):
                     total_income += money_needed
                     money_needed = 0
 
-                # 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)
-
-                if tax_rate <= 0.14 and self.year <= 2035:
-                    self.do_opportunistic_roth_conversions(taxes)
-
-                    # These conversions will affect taxes due.  Recompute
-                    # them now.
+                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