#!/usr/local/bin/python import sys from accounts import * import constants from parameters import parameters from tax_brackets import tax_brackets from tax_collector import tax_collector import utils import secrets class simulation(object): def __init__(self, parameters, accounts): """Initialize simulation parameters and starting account balances""" self.params = parameters self.accounts = accounts self.lynn_age = 0 self.scott_age = 0 self.alex_age = 0 self.year = 0 def do_rmd_withdrawals(self, taxes): """Determine if any account that has RMDs will require someone to take money out of it this year and, if so, how much. Then do the withdrawal.""" total_withdrawn = 0 for x in self.accounts: if x.has_rmd() and x.get_owner() == constants.SCOTT: rmd = x.do_rmd_withdrawal(self.scott_age, taxes) total_withdrawn += rmd elif x.has_rmd() and x.get_owner() == constants.LYNN: rmd = x.do_rmd_withdrawal(self.lynn_age, taxes) total_withdrawn += rmd return total_withdrawn def go_find_money(self, amount, taxes): """Look through accounts and try to find amount using some heuristics about where to withdraw money first.""" # Try brokerage accounts first for x in self.accounts: if not x.is_age_restricted(): if x.get_balance() >= amount: print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name()) x.withdraw(amount, taxes) return elif x.get_balance() > 0 and x.get_balance() < amount: money_available = x.get_balance() print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name()) x.withdraw(money_available, taxes) amount -= money_available # Next try age restircted accounts for x in self.accounts: if (x.is_age_restricted() and x.has_rmd() and x.belongs_to_lynn() and self.lynn_age > 60): if x.get_balance() >= amount: print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name()) x.withdraw(amount, taxes) return elif x.get_balance() > 0 and x.get_balance() < amount: money_available = x.get_balance() print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name()) x.withdraw(money_available, taxes) amount -= money_available if (x.is_age_restricted() and x.has_rmd() and x.belongs_to_scott() and self.scott_age > 60): if x.get_balance() >= amount: print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name()) x.withdraw(amount, taxes) return elif x.get_balance() > 0 and x.get_balance() < amount: money_available = x.get_balance() print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name()) x.withdraw(money_available, taxes) amount -= money_available # Last try Roth accounts for x in self.accounts: if (x.is_age_restricted() and x.has_roth() and x.belongs_to_lynn() and self.lynn_age > 60): if x.get_balance() >= amount: print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name()) x.withdraw(amount, taxes) return elif x.get_balance() > 0 and x.get_balance() < amount: money_available = x.get_balance() print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name()) x.withdraw(money_available, taxes) amount -= money_available if (x.is_age_restricted() and x.has_roth() and x.belongs_to_scott() and self.scott_age > 60): if x.get_balance() >= amount: print "## Withdrawing %s from %s" % (utils.format_money(amount), x.get_name()) x.withdraw(amount, taxes) return elif x.get_balance() > 0 and x.get_balance() < amount: money_available = x.get_balance() print "## Withdrawing %s from %s (and exhausting the account)" % (utils.format_money(money_available), x.get_name()) x.withdraw(money_available, taxes) amount -= money_available raise Exception("Unable to find enough money this year!") def get_social_security(self, scott_annual_social_security_dollars, lynn_annual_social_security_dollars, taxes): """Figure out if Scott and/or Lynn are taking social security at their present age in the simulation and, if so, how much their annual benefit should be.""" total_benefit = 0 if self.scott_age >= self.params.get_initial_social_security_age(constants.SCOTT): total_benefit += scott_annual_social_security_dollars taxes.record_ordinary_income(scott_annual_social_security_dollars) if self.lynn_age >= self.params.get_initial_social_security_age(constants.LYNN): total_benefit += lynn_annual_social_security_dollars taxes.record_ordinary_income(lynn_annual_social_security_dollars) return total_benefit def do_opportunistic_roth_conversions(self, taxes): """Roll over money from pretax 401(k)s or IRAs into Roth.""" desired_conversion_amount = ( taxes.how_many_more_dollars_can_we_earn_without_changing_tax_rate( self.params.get_federal_ordinary_income_tax_brackets())) if desired_conversion_amount > 0: money_converted = 0 for x in self.accounts: if x.has_roth(): amount = x.do_roth_conversion(desired_conversion_amount) money_converted += amount desired_conversion_amount -= amount def dump_report_header(self): self.params.dump() def dump_annual_header(self, money_needed): print "\nYear: %d, estimated annual expenses %s ---------------\n" % ( self.year, utils.format_money(money_needed)) print "Scott is %d, Lynn is %d and Alex is %d.\n" % ( self.scott_age, self.lynn_age, self.alex_age) # Print out how much money is in each account + overall net worth. total = 0 for x in self.accounts: total += x.get_balance() print "{:<50}: {:>14}".format(x.get_name(), utils.format_money(x.get_balance())) print "{:<50}: {:>14}\n".format("TOTAL", utils.format_money(total)) def dump_final_report(self, taxes): print "\nAGGREGATE STATS FINAL REPORT:" for x in self.accounts: x.dump_final_report() taxes.dump_final_report() def run(self): """Run the simulation!""" self.dump_report_header() taxes = tax_collector() adjusted_annual_expenses = self.params.get_initial_annual_expenses() adjusted_scott_annual_social_security_dollars = ( self.params.get_initial_social_security_benefit(constants.SCOTT)) adjusted_lynn_annual_social_security_dollars = ( self.params.get_initial_social_security_benefit(constants.LYNN)) try: for self.year in xrange(2020, 2080): self.scott_age = self.year - 1974 self.lynn_age = self.year - 1964 self.alex_age = self.year - 2005 # Computed money needed this year based on inflated budget. money_needed = adjusted_annual_expenses # When Alex is in college, we need $50K more per year. if self.alex_age > 18 and self.alex_age <= 22: money_needed += 50000 self.dump_annual_header(money_needed) # Now, figure out how to find money to pay for this year # and how much of it is taxable. total_income = 0 # When we reach a certain age we have to take RMDs from # some accounts. Handle that here. rmds = self.do_rmd_withdrawals(taxes) if rmds > 0: print "## Satisfied %s of RMDs from age-restricted accounts." % utils.format_money(rmds) total_income += rmds money_needed -= rmds # When we reach a certain age we are eligible for SS # payments. ss = self.get_social_security(adjusted_scott_annual_social_security_dollars, adjusted_lynn_annual_social_security_dollars, taxes) if ss > 0: print "## Social security paid %s" % utils.format_money(ss) total_income += ss money_needed -= ss # If we still need money, try to go find it. if money_needed > 0: self.go_find_money(money_needed, taxes) total_income += money_needed money_needed = 0 # Maybe do some opportunistic Roth conversions. taxes_due = taxes.approximate_taxes( self.params.get_federal_standard_deduction(), self.params.get_federal_ordinary_income_tax_brackets(), self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets()) total_income = taxes.get_total_income() tax_rate = float(taxes_due) / float(total_income) if tax_rate <= 0.14 and self.year <= 2035: self.do_opportunistic_roth_conversions(taxes) # These conversions will affect taxes due. Recompute # them now. taxes_due = taxes.approximate_taxes( self.params.get_federal_standard_deduction(), self.params.get_federal_ordinary_income_tax_brackets(), self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets()) total_income = taxes.get_total_income() tax_rate = float(taxes_due) / float(total_income) # Pay taxes_due by going to find more money. This is a # bit hacky since withdrawing more money to cover taxes # will, in turn, cause taxable income. But I think taxes # are low enough and this simulation is rough enough that # we can ignore this. if taxes_due > 0: print "## Estimated federal tax due: %s (this year's tax rate=%s)" % ( utils.format_money(taxes_due), utils.format_rate(tax_rate)) self.go_find_money(taxes_due, taxes) else: print "## No federal taxes due this year!" taxes.record_taxes_paid_and_reset_for_next_year(taxes_due) # Inflation and appreciation: # * Cost of living increases # * Social security benefits increase # * Tax brackets are adjusted for inflation inflation_multiplier = self.params.get_average_inflation_multiplier() adjusted_annual_expenses *= inflation_multiplier for x in self.accounts: x.appreciate(self.params.get_average_investment_return_multiplier()) if self.scott_age >= self.params.get_initial_social_security_age(constants.SCOTT): adjusted_scott_annual_social_security_dollars *= self.params.get_average_social_security_multiplier() if self.lynn_age >= self.params.get_initial_social_security_age(constants.LYNN): adjusted_lynn_annual_social_security_dollars *= self.params.get_average_social_security_multiplier() self.params.get_federal_ordinary_income_tax_brackets().adjust_with_multiplier(inflation_multiplier) self.params.get_federal_dividends_and_long_term_gains_income_tax_brackets().adjust_with_multiplier(inflation_multiplier) except: print "Ran out of money!!!" pass finally: self.dump_final_report(taxes) # main params = parameters().with_default_values() accounts = secrets.accounts s = simulation(params, accounts) s.run()