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