1 #!/usr/local/bin/python
7 from parameters import parameters
8 from tax_brackets import tax_brackets
9 from tax_collector import tax_collector
13 class simulation(object):
14 def __init__(self, parameters, accounts):
15 """Initialize simulation parameters and starting account balances"""
16 self.params = parameters
17 self.accounts = accounts
23 def do_rmd_withdrawals(self, taxes):
24 """Determine if any account that has RMDs will require someone to
25 take money out of it this year and, if so, how much. Then do
28 for x in self.accounts:
29 if x.has_rmd() and x.get_owner() == constants.SCOTT:
30 rmd = x.do_rmd_withdrawal(self.scott_age, taxes)
31 total_withdrawn += rmd
32 elif x.has_rmd() and x.get_owner() == constants.LYNN:
33 rmd = x.do_rmd_withdrawal(self.lynn_age, taxes)
34 total_withdrawn += rmd
35 return total_withdrawn
37 def go_find_money(self, amount, taxes):
38 """Look through accounts and try to find amount using some heuristics
39 about where to withdraw money first."""
41 # Try brokerage accounts first
42 for x in self.accounts:
43 if not x.is_age_restricted():
44 if x.get_balance() >= amount:
45 print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name())
46 x.withdraw(amount, taxes)
49 elif x.get_balance() > 0 and x.get_balance() < amount:
50 money_available = x.get_balance()
51 print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name())
52 x.withdraw(money_available, taxes)
53 amount -= money_available
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
62 if x.get_balance() >= amount:
63 print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name())
64 x.withdraw(amount, taxes)
67 elif x.get_balance() > 0 and x.get_balance() < amount:
68 money_available = x.get_balance()
69 print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name())
70 x.withdraw(money_available, taxes)
71 amount -= money_available
73 if (x.is_age_restricted() and
75 x.belongs_to_scott() and
77 if x.get_balance() >= amount:
78 print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name())
79 x.withdraw(amount, taxes)
82 elif x.get_balance() > 0 and x.get_balance() < amount:
83 money_available = x.get_balance()
84 print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name())
85 x.withdraw(money_available, taxes)
86 amount -= money_available
88 # Last try Roth accounts
89 for x in self.accounts:
90 if (x.is_age_restricted() and
92 x.belongs_to_lynn() and
94 if x.get_balance() >= amount:
95 print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name())
96 x.withdraw(amount, taxes)
99 elif x.get_balance() > 0 and x.get_balance() < amount:
100 money_available = x.get_balance()
101 print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name())
102 x.withdraw(money_available, taxes)
103 amount -= money_available
105 if (x.is_age_restricted() and
107 x.belongs_to_scott() and
108 self.scott_age > 60):
109 if x.get_balance() >= amount:
110 print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name())
111 x.withdraw(amount, taxes)
114 elif x.get_balance() > 0 and x.get_balance() < amount:
115 money_available = x.get_balance()
116 print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name())
117 x.withdraw(money_available, taxes)
118 amount -= money_available
119 raise Exception("Unable to find enough money this year!")
121 def get_social_security(self,
122 scott_annual_social_security_dollars,
123 lynn_annual_social_security_dollars,
125 """Figure out if Scott and/or Lynn are taking social security at
126 their present age in the simulation and, if so, how much their
127 annual benefit should be."""
129 if self.scott_age >= self.params.get_initial_social_security_age(constants.SCOTT):
130 total_benefit += scott_annual_social_security_dollars
131 taxes.record_ordinary_income(scott_annual_social_security_dollars)
132 if self.lynn_age >= self.params.get_initial_social_security_age(constants.LYNN):
133 total_benefit += lynn_annual_social_security_dollars
134 taxes.record_ordinary_income(lynn_annual_social_security_dollars)
137 def do_opportunistic_roth_conversions(self, taxes):
138 """Roll over money from pretax 401(k)s or IRAs into Roth."""
139 desired_conversion_amount = (
140 taxes.how_many_more_dollars_can_we_earn_without_changing_tax_rate(
141 self.params.get_federal_ordinary_income_tax_brackets()))
142 if desired_conversion_amount > 0:
144 for x in self.accounts:
146 amount = x.do_roth_conversion(desired_conversion_amount)
147 money_converted += amount
148 desired_conversion_amount -= amount
150 def dump_report_header(self):
153 def dump_annual_header(self, money_needed):
154 print "\nYear: %d, estimated annual expenses %s ---------------\n" % (
156 utils.format_money(money_needed))
157 print "Scott is %d, Lynn is %d and Alex is %d.\n" % (
158 self.scott_age, self.lynn_age, self.alex_age)
160 # Print out how much money is in each account + overall net worth.
162 for x in self.accounts:
163 total += x.get_balance()
164 print "{:<50}: {:>14}".format(x.get_name(),
165 utils.format_money(x.get_balance()))
166 print "{:<50}: {:>14}\n".format("TOTAL", utils.format_money(total))
168 def dump_final_report(self, taxes):
169 print "\nAGGREGATE STATS FINAL REPORT:"
170 for x in self.accounts:
171 x.dump_final_report()
172 taxes.dump_final_report()
175 """Run the simulation!"""
176 self.dump_report_header()
178 taxes = tax_collector()
179 adjusted_annual_expenses = self.params.get_initial_annual_expenses()
180 adjusted_scott_annual_social_security_dollars = (
181 self.params.get_initial_social_security_benefit(constants.SCOTT))
182 adjusted_lynn_annual_social_security_dollars = (
183 self.params.get_initial_social_security_benefit(constants.LYNN))
186 for self.year in xrange(2020, 2080):
187 self.scott_age = self.year - 1974
188 self.lynn_age = self.year - 1964
189 self.alex_age = self.year - 2005
191 # Computed money needed this year based on inflated budget.
192 money_needed = adjusted_annual_expenses
194 # When Alex is in college, we need $50K more per year.
195 if self.alex_age > 18 and self.alex_age <= 22:
196 money_needed += 50000
198 self.dump_annual_header(money_needed)
200 # Now, figure out how to find money to pay for this year
201 # and how much of it is taxable.
204 # When we reach a certain age we have to take RMDs from
205 # some accounts. Handle that here.
206 rmds = self.do_rmd_withdrawals(taxes)
208 print "## Satisfied %s of RMDs from age-restricted accounts." % utils.format_money(rmds)
212 # When we reach a certain age we are eligible for SS
214 ss = self.get_social_security(adjusted_scott_annual_social_security_dollars,
215 adjusted_lynn_annual_social_security_dollars,
218 print "## Social security paid %s" % utils.format_money(ss)
222 # If we still need money, try to go find it.
224 self.go_find_money(money_needed, taxes)
225 total_income += money_needed
228 # Maybe do some opportunistic Roth conversions.
229 taxes_due = taxes.approximate_taxes(
230 self.params.get_federal_standard_deduction(),
231 self.params.get_federal_ordinary_income_tax_brackets(),
232 self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets())
233 total_income = taxes.get_total_income()
234 tax_rate = float(taxes_due) / float(total_income)
236 if tax_rate <= 0.14 and self.year <= 2035:
237 self.do_opportunistic_roth_conversions(taxes)
239 # These conversions will affect taxes due. Recompute
241 taxes_due = taxes.approximate_taxes(
242 self.params.get_federal_standard_deduction(),
243 self.params.get_federal_ordinary_income_tax_brackets(),
244 self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets())
245 total_income = taxes.get_total_income()
246 tax_rate = float(taxes_due) / float(total_income)
248 # Pay taxes_due by going to find more money. This is a
249 # bit hacky since withdrawing more money to cover taxes
250 # will, in turn, cause taxable income. But I think taxes
251 # are low enough and this simulation is rough enough that
252 # we can ignore this.
254 print "## Estimated federal tax due: %s (this year's tax rate=%s)" % (
255 utils.format_money(taxes_due),
256 utils.format_rate(tax_rate))
257 self.go_find_money(taxes_due, taxes)
259 print "## No federal taxes due this year!"
260 taxes.record_taxes_paid_and_reset_for_next_year(taxes_due)
262 # Inflation and appreciation:
263 # * Cost of living increases
264 # * Social security benefits increase
265 # * Tax brackets are adjusted for inflation
266 inflation_multiplier = self.params.get_average_inflation_multiplier()
267 adjusted_annual_expenses *= inflation_multiplier
268 for x in self.accounts:
269 x.appreciate(self.params.get_average_investment_return_multiplier())
270 if self.scott_age >= self.params.get_initial_social_security_age(constants.SCOTT):
271 adjusted_scott_annual_social_security_dollars *= self.params.get_average_social_security_multiplier()
272 if self.lynn_age >= self.params.get_initial_social_security_age(constants.LYNN):
273 adjusted_lynn_annual_social_security_dollars *= self.params.get_average_social_security_multiplier()
275 self.params.get_federal_ordinary_income_tax_brackets().adjust_with_multiplier(inflation_multiplier)
276 self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets().adjust_with_multiplier(inflation_multiplier)
278 print "Ran out of money!!!"
282 self.dump_final_report(taxes)
285 params = parameters().with_default_values()
286 accounts = secrets.accounts
287 s = simulation(params, accounts)