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):
16 def __init__(self, parameters, accounts):
17 """Initialize simulation parameters and starting account balances."""
18 self.params = parameters
19 self.accounts = accounts
24 self.max_net_worth = 0
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
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
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."""
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,
51 x.withdraw(amount_to_withdraw, taxes)
52 amount_needed -= amount_to_withdraw
53 if amount_needed <= 0: return
55 # Next try age restircted accounts
56 for x in self.accounts:
57 if (x.is_age_restricted() and
59 ((x.belongs_to_lynn() and self.lynn_age > 60) or
60 (x.belongs_to_scott() and self.scott_age > 60))):
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,
66 x.withdraw(amount_to_withdraw, taxes)
67 amount_needed -= amount_to_withdraw
68 if amount_needed <= 0: return
70 # Last try Roth accounts
71 for x in self.accounts:
72 if (x.is_age_restricted() and
74 ((x.belongs_to_lynn() and self.lynn_age > 60) or
75 (x.belongs_to_scott() and self.scott_age > 60))):
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,
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)
86 def get_social_security(self,
87 scott_annual_social_security_dollars,
88 lynn_annual_social_security_dollars,
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)
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:
109 for x in self.accounts:
111 amount = x.do_roth_conversion(desired_conversion_amount, taxes)
112 money_converted += amount
113 desired_conversion_amount -= amount
114 if money_converted > 0:
118 def dump_report_header(self):
121 def dump_annual_header(self, money_needed):
122 print "\nYear: %d, estimated annual expenses %s ---------------\n" % (
125 print "Scott is %d, Lynn is %d and Alex is %d.\n" % (
126 self.scott_age, self.lynn_age, self.alex_age)
128 # Print out how much money is in each account + overall net worth.
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
137 def dump_final_report(self, taxes):
138 print "\nAGGREGATE STATS FINAL REPORT:"
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",
146 print "==> {:<50}: {:>14}".format("Final net worth of simulation",
150 """Run the simulation!"""
151 self.dump_report_header()
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))
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)
168 # Computed money needed this year based on inflated budget.
169 money_needed = money(adjusted_annual_expenses)
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
175 self.dump_annual_header(money_needed)
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)
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)
185 print "## Satisfied %s of RMDs from age-restricted accounts." % rmds
189 # When we reach a certain age we are eligible for SS
191 ss = self.get_social_security(
192 adjusted_scott_annual_social_security_dollars,
193 adjusted_lynn_annual_social_security_dollars,
196 print "## Social security paid %s" % ss
200 # If we still need money, try to go find it.
202 self.go_find_money(money_needed, taxes)
203 total_income += money_needed
206 look_for_conversions = True
207 tax_limit = money(25000)
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()
217 tax_rate = float(taxes_due) / float(total_income)
219 if (look_for_conversions and
221 taxes_due < tax_limit and
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
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.
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)
241 print "## No federal taxes due this year!"
242 taxes.record_taxes_paid_and_reset_for_next_year(taxes_due)
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)
261 except Exception as e:
262 print "Exception: %s" % e
263 print traceback.print_exc(e)
264 print "Ran out of money!?!"
268 self.dump_final_report(taxes)
272 #params = mutable_default_parameters()
277 for x in xrange(1, 100):
281 params = mutable_dynamic_historical_parameters()
282 accounts = secrets.get_starting_account_list()
283 s = simulation(params, accounts)
284 print "====================== Simulation %d ======================" % x
286 print "This simulation failed!"