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