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