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