Initial commit
[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, 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                 if x.get_balance() >= amount:
45                     print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name())
46                     x.withdraw(amount, taxes)
47                     return
48
49                 elif x.get_balance() > 0 and x.get_balance() < amount:
50                     money_available = x.get_balance()
51                     print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name())
52                     x.withdraw(money_available, taxes)
53                     amount -= money_available
54
55         # Next try age restircted accounts
56         for x in self.accounts:
57             if (x.is_age_restricted() and
58                 x.has_rmd() and
59                 x.belongs_to_lynn() and
60                 self.lynn_age > 60):
61
62                 if x.get_balance() >= amount:
63                     print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name())
64                     x.withdraw(amount, taxes)
65                     return
66
67                 elif x.get_balance() > 0 and x.get_balance() < amount:
68                     money_available = x.get_balance()
69                     print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name())
70                     x.withdraw(money_available, taxes)
71                     amount -= money_available
72
73             if (x.is_age_restricted() and
74                 x.has_rmd() and
75                 x.belongs_to_scott() and
76                 self.scott_age > 60):
77                 if x.get_balance() >= amount:
78                     print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name())
79                     x.withdraw(amount, taxes)
80                     return
81
82                 elif x.get_balance() > 0 and x.get_balance() < amount:
83                     money_available = x.get_balance()
84                     print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name())
85                     x.withdraw(money_available, taxes)
86                     amount -= money_available
87
88         # Last try Roth accounts
89         for x in self.accounts:
90             if (x.is_age_restricted() and
91                 x.has_roth() and
92                 x.belongs_to_lynn() and
93                 self.lynn_age > 60):
94                 if x.get_balance() >= amount:
95                     print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name())
96                     x.withdraw(amount, taxes)
97                     return
98
99                 elif x.get_balance() > 0 and x.get_balance() < amount:
100                     money_available = x.get_balance()
101                     print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name())
102                     x.withdraw(money_available, taxes)
103                     amount -= money_available
104
105             if (x.is_age_restricted() and
106                 x.has_roth() and
107                 x.belongs_to_scott() and
108                 self.scott_age > 60):
109                 if x.get_balance() >= amount:
110                     print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name())
111                     x.withdraw(amount, taxes)
112                     return
113
114                 elif x.get_balance() > 0 and x.get_balance() < amount:
115                     money_available = x.get_balance()
116                     print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name())
117                     x.withdraw(money_available, taxes)
118                     amount -= money_available
119         raise Exception("Unable to find enough money this year!")
120
121     def get_social_security(self,
122                             scott_annual_social_security_dollars,
123                             lynn_annual_social_security_dollars,
124                             taxes):
125         """Figure out if Scott and/or Lynn are taking social security at
126            their present age in the simulation and, if so, how much their
127            annual benefit should be."""
128         total_benefit = 0
129         if self.scott_age >= self.params.get_initial_social_security_age(constants.SCOTT):
130             total_benefit += scott_annual_social_security_dollars
131             taxes.record_ordinary_income(scott_annual_social_security_dollars)
132         if self.lynn_age >= self.params.get_initial_social_security_age(constants.LYNN):
133             total_benefit += lynn_annual_social_security_dollars
134             taxes.record_ordinary_income(lynn_annual_social_security_dollars)
135         return total_benefit
136
137     def do_opportunistic_roth_conversions(self, taxes):
138         """Roll over money from pretax 401(k)s or IRAs into Roth."""
139         desired_conversion_amount = (
140             taxes.how_many_more_dollars_can_we_earn_without_changing_tax_rate(
141                 self.params.get_federal_ordinary_income_tax_brackets()))
142         if desired_conversion_amount > 0:
143             money_converted = 0
144             for x in self.accounts:
145                 if x.has_roth():
146                     amount = x.do_roth_conversion(desired_conversion_amount)
147                     money_converted += amount
148                     desired_conversion_amount -= amount
149
150     def dump_report_header(self):
151         self.params.dump()
152
153     def dump_annual_header(self, money_needed):
154         print "\nYear: %d, estimated annual expenses %s ---------------\n" % (
155             self.year,
156             utils.format_money(money_needed))
157         print "Scott is %d, Lynn is %d and Alex is %d.\n" % (
158             self.scott_age, self.lynn_age, self.alex_age)
159
160         # Print out how much money is in each account + overall net worth.
161         total = 0
162         for x in self.accounts:
163             total += x.get_balance()
164             print "{:<50}: {:>14}".format(x.get_name(),
165                                           utils.format_money(x.get_balance()))
166         print "{:<50}: {:>14}\n".format("TOTAL", utils.format_money(total))
167
168     def dump_final_report(self, taxes):
169         print "\nAGGREGATE STATS FINAL REPORT:"
170         for x in self.accounts:
171             x.dump_final_report()
172         taxes.dump_final_report()
173
174     def run(self):
175         """Run the simulation!"""
176         self.dump_report_header()
177
178         taxes = tax_collector()
179         adjusted_annual_expenses = self.params.get_initial_annual_expenses()
180         adjusted_scott_annual_social_security_dollars = (
181             self.params.get_initial_social_security_benefit(constants.SCOTT))
182         adjusted_lynn_annual_social_security_dollars = (
183             self.params.get_initial_social_security_benefit(constants.LYNN))
184
185         try:
186             for self.year in xrange(2020, 2080):
187                 self.scott_age = self.year - 1974
188                 self.lynn_age = self.year - 1964
189                 self.alex_age = self.year - 2005
190
191                 # Computed money needed this year based on inflated budget.
192                 money_needed = adjusted_annual_expenses
193
194                 # When Alex is in college, we need $50K more per year.
195                 if self.alex_age > 18 and self.alex_age <= 22:
196                     money_needed += 50000
197
198                 self.dump_annual_header(money_needed)
199
200                 # Now, figure out how to find money to pay for this year
201                 # and how much of it is taxable.
202                 total_income = 0
203
204                 # When we reach a certain age we have to take RMDs from
205                 # some accounts.  Handle that here.
206                 rmds = self.do_rmd_withdrawals(taxes)
207                 if rmds > 0:
208                     print "## Satisfied %s of RMDs from age-restricted accounts." % utils.format_money(rmds)
209                     total_income += rmds
210                     money_needed -= rmds
211
212                 # When we reach a certain age we are eligible for SS
213                 # payments.
214                 ss = self.get_social_security(adjusted_scott_annual_social_security_dollars,
215                                               adjusted_lynn_annual_social_security_dollars,
216                                               taxes)
217                 if ss > 0:
218                     print "## Social security paid %s" % utils.format_money(ss)
219                     total_income += ss
220                     money_needed -= ss
221
222                 # If we still need money, try to go find it.
223                 if money_needed > 0:
224                     self.go_find_money(money_needed, taxes)
225                     total_income += money_needed
226                     money_needed = 0
227
228                 # Maybe do some opportunistic Roth conversions.
229                 taxes_due = taxes.approximate_taxes(
230                     self.params.get_federal_standard_deduction(),
231                     self.params.get_federal_ordinary_income_tax_brackets(),
232                     self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets())
233                 total_income = taxes.get_total_income()
234                 tax_rate = float(taxes_due) / float(total_income)
235
236                 if tax_rate <= 0.14 and self.year <= 2035:
237                     self.do_opportunistic_roth_conversions(taxes)
238
239                     # These conversions will affect taxes due.  Recompute
240                     # them now.
241                     taxes_due = taxes.approximate_taxes(
242                         self.params.get_federal_standard_deduction(),
243                         self.params.get_federal_ordinary_income_tax_brackets(),
244                         self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets())
245                     total_income = taxes.get_total_income()
246                     tax_rate = float(taxes_due) / float(total_income)
247
248                 # Pay taxes_due by going to find more money.  This is a
249                 # bit hacky since withdrawing more money to cover taxes
250                 # will, in turn, cause taxable income.  But I think taxes
251                 # are low enough and this simulation is rough enough that
252                 # we can ignore this.
253                 if taxes_due > 0:
254                     print "## Estimated federal tax due: %s (this year's tax rate=%s)" % (
255                         utils.format_money(taxes_due),
256                         utils.format_rate(tax_rate))
257                     self.go_find_money(taxes_due, taxes)
258                 else:
259                     print "## No federal taxes due this year!"
260                 taxes.record_taxes_paid_and_reset_for_next_year(taxes_due)
261
262                 # Inflation and appreciation:
263                 #   * Cost of living increases
264                 #   * Social security benefits increase
265                 #   * Tax brackets are adjusted for inflation
266                 inflation_multiplier = self.params.get_average_inflation_multiplier()
267                 adjusted_annual_expenses *= inflation_multiplier
268                 for x in self.accounts:
269                     x.appreciate(self.params.get_average_investment_return_multiplier())
270                 if self.scott_age >= self.params.get_initial_social_security_age(constants.SCOTT):
271                     adjusted_scott_annual_social_security_dollars *= self.params.get_average_social_security_multiplier()
272                 if self.lynn_age >= self.params.get_initial_social_security_age(constants.LYNN):
273                     adjusted_lynn_annual_social_security_dollars *= self.params.get_average_social_security_multiplier()
274
275                 self.params.get_federal_ordinary_income_tax_brackets().adjust_with_multiplier(inflation_multiplier)
276                 self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets().adjust_with_multiplier(inflation_multiplier)
277         except:
278             print "Ran out of money!!!"
279             pass
280
281         finally:
282             self.dump_final_report(taxes)
283
284 # main
285 params = parameters().with_default_values()
286 accounts = secrets.accounts
287 s = simulation(params, accounts)
288 s.run()