1 #!/usr/local/bin/python
7 from parameters import *
8 from tax_brackets import tax_brackets
9 from tax_collector import tax_collector
12 from money import money
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
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
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
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."""
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,
49 x.withdraw(amount_to_withdraw, taxes)
50 amount_needed -= amount_to_withdraw
51 if amount_needed <= 0: return
53 # Next try age restircted accounts
54 for x in self.accounts:
55 if (x.is_age_restricted() and
57 ((x.belongs_to_lynn() and self.lynn_age > 60) or
58 (x.belongs_to_scott() and self.scott_age > 60))):
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,
64 x.withdraw(amount_to_withdraw, taxes)
65 amount_needed -= amount_to_withdraw
66 if amount_needed <= 0: return
68 # Last try Roth accounts
69 for x in self.accounts:
70 if (x.is_age_restricted() and
72 ((x.belongs_to_lynn() and self.lynn_age > 60) or
73 (x.belongs_to_scott() and self.scott_age > 60))):
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,
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)
84 def get_social_security(self,
85 scott_annual_social_security_dollars,
86 lynn_annual_social_security_dollars,
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)
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:
107 for x in self.accounts:
109 amount = x.do_roth_conversion(desired_conversion_amount, taxes)
110 money_converted += amount
111 desired_conversion_amount -= amount
112 if money_converted > 0:
116 def dump_report_header(self):
119 def dump_annual_header(self, money_needed):
120 print "\nYear: %d, estimated annual expenses %s ---------------\n" % (
123 print "Scott is %d, Lynn is %d and Alex is %d.\n" % (
124 self.scott_age, self.lynn_age, self.alex_age)
126 # Print out how much money is in each account + overall net worth.
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)
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()
140 """Run the simulation!"""
141 self.dump_report_header()
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))
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 self.params.report_year(self.year)
157 # Computed money needed this year based on inflated budget.
158 money_needed = money(adjusted_annual_expenses)
160 # When Alex is in college, we need $50K more per year.
161 if self.alex_age > 18 and self.alex_age <= 22:
162 money_needed += 50000
164 self.dump_annual_header(money_needed)
166 # Now, figure out how to find money to pay for this year
167 # and how much of it is taxable.
168 total_income = money(0)
170 # When we reach a certain age we have to take RMDs from
171 # some accounts. Handle that here.
172 rmds = self.do_rmd_withdrawals(taxes)
174 print "## Satisfied %s of RMDs from age-restricted accounts." % rmds
178 # When we reach a certain age we are eligible for SS
180 ss = self.get_social_security(
181 adjusted_scott_annual_social_security_dollars,
182 adjusted_lynn_annual_social_security_dollars,
185 print "## Social security paid %s" % ss
189 # If we still need money, try to go find it.
191 self.go_find_money(money_needed, taxes)
192 total_income += money_needed
195 look_for_conversions = True
196 tax_limit = money(25000)
198 # Maybe do some opportunistic Roth conversions.
199 taxes_due = taxes.approximate_taxes(
200 self.params.get_federal_standard_deduction(),
201 self.params.get_federal_ordinary_income_tax_brackets(),
202 self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets())
203 total_income = taxes.get_total_income()
204 tax_rate = float(taxes_due) / float(total_income)
206 if (look_for_conversions and
208 taxes_due < tax_limit and
211 look_for_conversions = self.do_opportunistic_roth_conversions(taxes)
212 # because these conversions affect taxes, spin
213 # once more in the loop and recompute taxes
218 # Pay taxes_due by going to find more money. This is a
219 # bit hacky since withdrawing more money to cover taxes
220 # will, in turn, cause taxable income. But I think taxes
221 # are low enough and this simulation is rough enough that
222 # we can ignore this.
224 print "## Estimated federal tax due: %s (this year's tax rate=%s)" % (
225 taxes_due, utils.format_rate(tax_rate))
226 self.go_find_money(taxes_due, taxes)
228 print "## No federal taxes due this year!"
229 taxes.record_taxes_paid_and_reset_for_next_year(taxes_due)
231 # Inflation and appreciation:
232 # * Cost of living increases
233 # * Social security benefits increase
234 # * Tax brackets are adjusted for inflation
235 inflation_multiplier = self.params.get_average_inflation_multiplier()
236 returns_multiplier = self.params.get_average_investment_return_multiplier()
237 ss_multiplier = self.params.get_average_social_security_multiplier()
238 adjusted_annual_expenses *= inflation_multiplier
239 for x in self.accounts:
240 x.appreciate(returns_multiplier)
241 if self.scott_age >= self.params.get_initial_social_security_age(constants.SCOTT):
242 adjusted_scott_annual_social_security_dollars *= ss_multiplier
243 if self.lynn_age >= self.params.get_initial_social_security_age(constants.LYNN):
244 adjusted_lynn_annual_social_security_dollars *= ss_multiplier
245 self.params.get_federal_ordinary_income_tax_brackets().adjust_with_multiplier(inflation_multiplier)
246 self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets().adjust_with_multiplier(inflation_multiplier)
247 except Exception as e:
248 print "Exception: %s" % e
249 print "Ran out of money!?!"
252 self.dump_final_report(taxes)
255 #params = mutable_default_parameters()
256 params = mutable_dynamic_historical_parameters()
257 accounts = secrets.accounts
258 s = simulation(params, accounts)