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