Creates a parameters subclass that chooses random runs of years from
[retire.git] / retire.py
1 #!/usr/local/bin/python
2
3 import sys
4
5 from accounts import *
6 import constants
7 from parameters import *
8 from tax_brackets import tax_brackets
9 from tax_collector import tax_collector
10 import utils
11 import secrets
12 from money import money
13
14 class simulation(object):
15     def __init__(self, parameters, accounts):
16         """Initialize simulation parameters and starting account balances"""
17         self.params = parameters
18         self.accounts = accounts
19         self.lynn_age = 0
20         self.scott_age = 0
21         self.alex_age = 0
22         self.year = 0
23
24     def do_rmd_withdrawals(self, taxes):
25         """Determine if any account that has RMDs will require someone to
26            take money out of it this year and, if so, how much.  Then do
27            the withdrawal."""
28         total_withdrawn = money(0)
29         for x in self.accounts:
30             if x.has_rmd() and x.get_owner() == constants.SCOTT:
31                 rmd = x.do_rmd_withdrawal(self.scott_age, taxes)
32                 total_withdrawn += rmd
33             elif x.has_rmd() and x.get_owner() == constants.LYNN:
34                 rmd = x.do_rmd_withdrawal(self.lynn_age, taxes)
35                 total_withdrawn += rmd
36         return total_withdrawn
37
38     def go_find_money(self, amount_needed, taxes):
39         """Look through accounts and try to find amount using some heuristics
40            about where to withdraw money first."""
41
42         # Try brokerage accounts first
43         for x in self.accounts:
44             if not x.is_age_restricted():
45                 amount_to_withdraw = min(amount_needed, x.get_balance())
46                 if amount_to_withdraw > 0:
47                     print "## Withdrawing %s from %s" % (amount_to_withdraw,
48                                                          x.get_name())
49                     x.withdraw(amount_to_withdraw, taxes)
50                     amount_needed -= amount_to_withdraw
51                     if amount_needed <= 0: return
52
53         # Next try age restircted accounts
54         for x in self.accounts:
55             if (x.is_age_restricted() and
56                 x.has_rmd() and
57                 ((x.belongs_to_lynn() and self.lynn_age > 60) or
58                  (x.belongs_to_scott() and self.scott_age > 60))):
59
60                 amount_to_withdraw = min(amount_needed, x.get_balance())
61                 if amount_to_withdraw > 0:
62                     print "## Withdrawing %s from %s" % (amount_to_withdraw,
63                                                          x.get_name())
64                     x.withdraw(amount_to_withdraw, taxes)
65                     amount_needed -= amount_to_withdraw
66                     if amount_needed <= 0: return
67
68         # Last try Roth accounts
69         for x in self.accounts:
70             if (x.is_age_restricted() and
71                 x.has_roth() and
72                 ((x.belongs_to_lynn() and self.lynn_age > 60) or
73                  (x.belongs_to_scott() and self.scott_age > 60))):
74
75                 amount_to_withdraw = min(amount_needed, x.get_balance())
76                 if amount_to_withdraw > 0:
77                     print "## Withdrawing %s from %s" % (amount_to_withdraw,
78                                                          x.get_name())
79                     x.withdraw(amount_to_withdraw, taxes)
80                     amount_needed -= amount_to_withdraw
81                     if amount_needed <= 0: return
82         raise Exception("Unable to find enough money this year, still need %s more!" % amount_needed)
83
84     def get_social_security(self,
85                             scott_annual_social_security_dollars,
86                             lynn_annual_social_security_dollars,
87                             taxes):
88         """Figure out if Scott and/or Lynn are taking social security at
89            their present age in the simulation and, if so, how much their
90            annual benefit should be."""
91         total_benefit = money(0)
92         if self.scott_age >= self.params.get_initial_social_security_age(constants.SCOTT):
93             total_benefit += scott_annual_social_security_dollars
94             taxes.record_ordinary_income(scott_annual_social_security_dollars)
95         if self.lynn_age >= self.params.get_initial_social_security_age(constants.LYNN):
96             total_benefit += lynn_annual_social_security_dollars
97             taxes.record_ordinary_income(lynn_annual_social_security_dollars)
98         return total_benefit
99
100     def do_opportunistic_roth_conversions(self, taxes):
101         """Roll over money from pretax 401(k)s or IRAs into Roth."""
102         desired_conversion_amount = (
103             taxes.how_many_more_dollars_can_we_earn_without_changing_tax_rate(
104                 self.params.get_federal_ordinary_income_tax_brackets()))
105         if desired_conversion_amount > 0:
106             money_converted = 0
107             for x in self.accounts:
108                 if x.has_roth():
109                     amount = x.do_roth_conversion(desired_conversion_amount, taxes)
110                     money_converted += amount
111                     desired_conversion_amount -= amount
112             if money_converted > 0:
113                 return True
114         return False
115
116     def dump_report_header(self):
117         self.params.dump()
118
119     def dump_annual_header(self, money_needed):
120         print "\nYear: %d, estimated annual expenses %s ---------------\n" % (
121             self.year,
122             money_needed)
123         print "Scott is %d, Lynn is %d and Alex is %d.\n" % (
124             self.scott_age, self.lynn_age, self.alex_age)
125
126         # Print out how much money is in each account + overall net worth.
127         total = money(0)
128         for x in self.accounts:
129             total += x.get_balance()
130             print "{:<50}: {:>14}".format(x.get_name(), x.get_balance())
131         print "{:<50}: {:>14}\n".format("TOTAL", total)
132
133     def dump_final_report(self, taxes):
134         print "\nAGGREGATE STATS FINAL REPORT:"
135         for x in self.accounts:
136             x.dump_final_report()
137         taxes.dump_final_report()
138
139     def run(self):
140         """Run the simulation!"""
141         self.dump_report_header()
142
143         taxes = tax_collector()
144         adjusted_annual_expenses = self.params.get_initial_annual_expenses()
145         adjusted_scott_annual_social_security_dollars = (
146             self.params.get_initial_social_security_benefit(constants.SCOTT))
147         adjusted_lynn_annual_social_security_dollars = (
148             self.params.get_initial_social_security_benefit(constants.LYNN))
149
150         try:
151             for self.year in xrange(2020, 2080):
152                 self.scott_age = self.year - 1974
153                 self.lynn_age = self.year - 1964
154                 self.alex_age = self.year - 2005
155                 self.params.report_year(self.year)
156
157                 # Computed money needed this year based on inflated budget.
158                 money_needed = money(adjusted_annual_expenses)
159
160                 # When Alex is in college, we need $50K more per year.
161                 if self.alex_age > 18 and self.alex_age <= 22:
162                     money_needed += 50000
163
164                 self.dump_annual_header(money_needed)
165
166                 # Now, figure out how to find money to pay for this year
167                 # and how much of it is taxable.
168                 total_income = money(0)
169
170                 # When we reach a certain age we have to take RMDs from
171                 # some accounts.  Handle that here.
172                 rmds = self.do_rmd_withdrawals(taxes)
173                 if rmds > 0:
174                     print "## Satisfied %s of RMDs from age-restricted accounts." % rmds
175                     total_income += rmds
176                     money_needed -= rmds
177
178                 # When we reach a certain age we are eligible for SS
179                 # payments.
180                 ss = self.get_social_security(
181                     adjusted_scott_annual_social_security_dollars,
182                     adjusted_lynn_annual_social_security_dollars,
183                     taxes)
184                 if ss > 0:
185                     print "## Social security paid %s" % ss
186                     total_income += ss
187                     money_needed -= ss
188
189                 # If we still need money, try to go find it.
190                 if money_needed > 0:
191                     self.go_find_money(money_needed, taxes)
192                     total_income += money_needed
193                     money_needed = 0
194
195                 look_for_conversions = True
196                 tax_limit = money(25000)
197                 while True:
198                     # Maybe do some opportunistic Roth conversions.
199                     taxes_due = taxes.approximate_taxes(
200                         self.params.get_federal_standard_deduction(),
201                         self.params.get_federal_ordinary_income_tax_brackets(),
202                         self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets())
203                     total_income = taxes.get_total_income()
204                     tax_rate = float(taxes_due) / float(total_income)
205
206                     if (look_for_conversions and
207                         tax_rate <= 0.14 and
208                         taxes_due < tax_limit and
209                         self.year <= 2036):
210
211                         look_for_conversions = self.do_opportunistic_roth_conversions(taxes)
212                         # because these conversions affect taxes, spin
213                         # once more in the loop and recompute taxes
214                         # due and tax rate.
215                     else:
216                         break
217
218                 # Pay taxes_due by going to find more money.  This is a
219                 # bit hacky since withdrawing more money to cover taxes
220                 # will, in turn, cause taxable income.  But I think taxes
221                 # are low enough and this simulation is rough enough that
222                 # we can ignore this.
223                 if taxes_due > 0:
224                     print "## Estimated federal tax due: %s (this year's tax rate=%s)" % (
225                         taxes_due, utils.format_rate(tax_rate))
226                     self.go_find_money(taxes_due, taxes)
227                 else:
228                     print "## No federal taxes due this year!"
229                 taxes.record_taxes_paid_and_reset_for_next_year(taxes_due)
230
231                 # Inflation and appreciation:
232                 #   * Cost of living increases
233                 #   * Social security benefits increase
234                 #   * Tax brackets are adjusted for inflation
235                 inflation_multiplier = self.params.get_average_inflation_multiplier()
236                 returns_multiplier = self.params.get_average_investment_return_multiplier()
237                 ss_multiplier = self.params.get_average_social_security_multiplier()
238                 adjusted_annual_expenses *= inflation_multiplier
239                 for x in self.accounts:
240                     x.appreciate(returns_multiplier)
241                 if self.scott_age >= self.params.get_initial_social_security_age(constants.SCOTT):
242                     adjusted_scott_annual_social_security_dollars *= ss_multiplier
243                 if self.lynn_age >= self.params.get_initial_social_security_age(constants.LYNN):
244                     adjusted_lynn_annual_social_security_dollars *= ss_multiplier
245                 self.params.get_federal_ordinary_income_tax_brackets().adjust_with_multiplier(inflation_multiplier)
246                 self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets().adjust_with_multiplier(inflation_multiplier)
247         except Exception as e:
248             print "Exception: %s" % e
249             print "Ran out of money!?!"
250
251         finally:
252             self.dump_final_report(taxes)
253
254 # main
255 #params = mutable_default_parameters()
256 params = mutable_dynamic_historical_parameters()
257 accounts = secrets.accounts
258 s = simulation(params, accounts)
259 s.run()