1 #!/usr/local/bin/python
8 from parameters import *
9 from tax_brackets import tax_brackets
10 from tax_collector import tax_collector
13 from money import money
15 class simulation(object):
17 def __init__(self, parameters, accounts):
18 """Initialize simulation parameters and starting account balances"""
19 self.params = parameters
20 self.accounts = accounts
25 self.max_net_worth = 0
27 def do_rmd_withdrawals(self, taxes):
28 """Determine if any account that has RMDs will require someone to
29 take money out of it this year and, if so, how much. Then do
31 total_withdrawn = money(0)
32 for x in self.accounts:
33 if x.has_rmd() and x.get_owner() == constants.SCOTT:
34 rmd = x.do_rmd_withdrawal(self.scott_age, taxes)
35 total_withdrawn += rmd
36 elif x.has_rmd() and x.get_owner() == constants.LYNN:
37 rmd = x.do_rmd_withdrawal(self.lynn_age, taxes)
38 total_withdrawn += rmd
39 return total_withdrawn
41 def go_find_money(self, amount_needed, taxes):
42 """Look through accounts and try to find amount using some heuristics
43 about where to withdraw money first."""
45 # Try brokerage accounts first
46 for x in self.accounts:
47 if not x.is_age_restricted():
48 amount_to_withdraw = min(amount_needed, x.get_balance())
49 if amount_to_withdraw > 0:
50 print "## Withdrawing %s from %s" % (amount_to_withdraw,
52 x.withdraw(amount_to_withdraw, taxes)
53 amount_needed -= amount_to_withdraw
54 if amount_needed <= 0: return
56 # Next try age restircted accounts
57 for x in self.accounts:
58 if (x.is_age_restricted() and
60 ((x.belongs_to_lynn() and self.lynn_age > 60) or
61 (x.belongs_to_scott() and self.scott_age > 60))):
63 amount_to_withdraw = min(amount_needed, x.get_balance())
64 if amount_to_withdraw > 0:
65 print "## Withdrawing %s from %s" % (amount_to_withdraw,
67 x.withdraw(amount_to_withdraw, taxes)
68 amount_needed -= amount_to_withdraw
69 if amount_needed <= 0: return
71 # Last try Roth accounts
72 for x in self.accounts:
73 if (x.is_age_restricted() and
75 ((x.belongs_to_lynn() and self.lynn_age > 60) or
76 (x.belongs_to_scott() and self.scott_age > 60))):
78 amount_to_withdraw = min(amount_needed, x.get_balance())
79 if amount_to_withdraw > 0:
80 print "## Withdrawing %s from %s" % (amount_to_withdraw,
82 x.withdraw(amount_to_withdraw, taxes)
83 amount_needed -= amount_to_withdraw
84 if amount_needed <= 0: return
85 raise Exception("Unable to find enough money this year, still need %s more!" % amount_needed)
87 def get_social_security(self,
88 scott_annual_social_security_dollars,
89 lynn_annual_social_security_dollars,
91 """Figure out if Scott and/or Lynn are taking social security at
92 their present age in the simulation and, if so, how much their
93 annual benefit should be."""
94 total_benefit = money(0)
95 if self.scott_age >= self.params.get_initial_social_security_age(constants.SCOTT):
96 total_benefit += scott_annual_social_security_dollars
97 taxes.record_ordinary_income(scott_annual_social_security_dollars)
98 if self.lynn_age >= self.params.get_initial_social_security_age(constants.LYNN):
99 total_benefit += lynn_annual_social_security_dollars
100 taxes.record_ordinary_income(lynn_annual_social_security_dollars)
103 def do_opportunistic_roth_conversions(self, taxes):
104 """Roll over money from pretax 401(k)s or IRAs into Roth."""
105 desired_conversion_amount = (
106 taxes.how_many_more_dollars_can_we_earn_without_changing_tax_rate(
107 self.params.get_federal_ordinary_income_tax_brackets()))
108 if desired_conversion_amount > 0:
110 for x in self.accounts:
112 amount = x.do_roth_conversion(desired_conversion_amount, taxes)
113 money_converted += amount
114 desired_conversion_amount -= amount
115 if money_converted > 0:
119 def dump_report_header(self):
122 def dump_annual_header(self, money_needed):
123 print "\nYear: %d, estimated annual expenses %s ---------------\n" % (
126 print "Scott is %d, Lynn is %d and Alex is %d.\n" % (
127 self.scott_age, self.lynn_age, self.alex_age)
129 # Print out how much money is in each account + overall net worth.
131 for x in self.accounts:
132 total += x.get_balance()
133 print "{:<50}: {:>14}".format(x.get_name(), x.get_balance())
134 print "{:<50}: {:>14}\n".format("TOTAL", total)
135 if self.max_net_worth < total:
136 self.max_net_worth = total
138 def dump_final_report(self, taxes):
139 print "\nAGGREGATE STATS FINAL REPORT:"
141 for x in self.accounts:
142 x.dump_final_report()
143 total += x.get_balance()
144 taxes.dump_final_report()
145 print " {:<50}: {:>14}".format("Max net worth achieved",
147 print "==> {:<50}: {:>14}".format("Final net worth of simulation",
151 """Run the simulation!"""
152 self.dump_report_header()
154 taxes = tax_collector()
155 adjusted_annual_expenses = self.params.get_initial_annual_expenses()
156 adjusted_scott_annual_social_security_dollars = (
157 self.params.get_initial_social_security_benefit(constants.SCOTT))
158 adjusted_lynn_annual_social_security_dollars = (
159 self.params.get_initial_social_security_benefit(constants.LYNN))
163 for self.year in xrange(2020, 2080):
164 self.scott_age = self.year - 1974
165 self.lynn_age = self.year - 1964
166 self.alex_age = self.year - 2005
167 self.params.report_year(self.year)
169 # Computed money needed this year based on inflated budget.
170 money_needed = money(adjusted_annual_expenses)
172 # When Alex is in college, we need $50K more per year.
173 if self.alex_age > 18 and self.alex_age <= 22:
174 money_needed += 50000
176 self.dump_annual_header(money_needed)
178 # Now, figure out how to find money to pay for this year
179 # and how much of it is taxable.
180 total_income = money(0)
182 # When we reach a certain age we have to take RMDs from
183 # some accounts. Handle that here.
184 rmds = self.do_rmd_withdrawals(taxes)
186 print "## Satisfied %s of RMDs from age-restricted accounts." % rmds
190 # When we reach a certain age we are eligible for SS
192 ss = self.get_social_security(
193 adjusted_scott_annual_social_security_dollars,
194 adjusted_lynn_annual_social_security_dollars,
197 print "## Social security paid %s" % ss
201 # If we still need money, try to go find it.
203 self.go_find_money(money_needed, taxes)
204 total_income += money_needed
207 look_for_conversions = True
208 tax_limit = money(25000)
210 # Maybe do some opportunistic Roth conversions.
211 taxes_due = taxes.approximate_taxes(
212 self.params.get_federal_standard_deduction(),
213 self.params.get_federal_ordinary_income_tax_brackets(),
214 self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets())
215 total_income = taxes.get_total_income()
218 tax_rate = float(taxes_due) / float(total_income)
220 if (look_for_conversions and
222 taxes_due < tax_limit and
225 look_for_conversions = self.do_opportunistic_roth_conversions(taxes)
226 # because these conversions affect taxes, spin
227 # once more in the loop and recompute taxes
232 # Pay taxes_due by going to find more money. This is a
233 # bit hacky since withdrawing more money to cover taxes
234 # will, in turn, cause taxable income. But I think taxes
235 # are low enough and this simulation is rough enough that
236 # we can ignore this.
238 print "## Estimated federal tax due: %s (this year's tax rate=%s)" % (
239 taxes_due, utils.format_rate(tax_rate))
240 self.go_find_money(taxes_due, taxes)
242 print "## No federal taxes due this year!"
243 taxes.record_taxes_paid_and_reset_for_next_year(taxes_due)
245 # Inflation and appreciation:
246 # * Cost of living increases
247 # * Social security benefits increase
248 # * Tax brackets are adjusted for inflation
249 inflation_multiplier = self.params.get_average_inflation_multiplier()
250 returns_multiplier = self.params.get_average_investment_return_multiplier()
251 ss_multiplier = self.params.get_average_social_security_multiplier()
252 adjusted_annual_expenses *= inflation_multiplier
253 for x in self.accounts:
254 x.appreciate(returns_multiplier)
255 if self.scott_age >= self.params.get_initial_social_security_age(constants.SCOTT):
256 adjusted_scott_annual_social_security_dollars *= ss_multiplier
257 if self.lynn_age >= self.params.get_initial_social_security_age(constants.LYNN):
258 adjusted_lynn_annual_social_security_dollars *= ss_multiplier
259 self.params.get_federal_ordinary_income_tax_brackets().adjust_with_multiplier(inflation_multiplier)
260 self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets().adjust_with_multiplier(inflation_multiplier)
262 except Exception as e:
263 print "Exception: %s" % e
264 print traceback.print_exc(e)
265 print "Ran out of money!?!"
269 self.dump_final_report(taxes)
273 #params = mutable_default_parameters()
278 for x in xrange(1, 100):
282 params = mutable_dynamic_historical_parameters()
283 accounts = secrets.get_starting_account_list()
284 s = simulation(params, accounts)
285 print "====================== Simulation %d ======================" % x
287 print "This simulation failed!"