--- /dev/null
+import constants
+import utils
+
+class account(object):
+ """This defines an account base class which inherits from object, the
+ default base class in python. This is not a 100% abstract class, it
+ includes some default fields (like balance and name) and methods that
+ define behaviors (withdraw, appreciate, etc...). Several subclasses
+ are defined below."""
+
+ def __init__(self, name, owner):
+ """Constructor for account object takes a name and owner"""
+ self.name = name
+ self.owner = owner
+
+ def get_name(self):
+ return self.name
+
+ def get_owner(self):
+ return self.owner
+
+ def belongs_to_scott(self):
+ return self.get_owner() == constants.SCOTT
+
+ def belongs_to_lynn(self):
+ return self.get_owner() == constants.LYNN
+
+ def get_balance(self):
+ pass
+
+ def appreciate(self, multiplier):
+ """Grow the balance using rate."""
+ pass
+
+ def withdraw(self, amount, taxes):
+ """Withdraw money from the account if possible. Throws otherwise.
+ Money should be registered with taxes so that we can approximate
+ taxes due, too."""
+ pass
+
+ def deposit(self, amount):
+ pass
+
+ def is_age_restricted(self):
+ """Is this account age restricted. Subclasses should implement."""
+ pass
+
+ def has_rmd(self):
+ """Does this account have a required minimum distribution? Sub-
+ classes should implement."""
+ pass
+
+ def do_rmd_withdrawal(self, owner_age, taxes):
+ pass
+
+ def has_roth(self):
+ pass
+
+ def dump_final_report(self):
+ print " " + self.name + ":"
+
+class age_restricted_tax_deferred_account(account):
+ """Account object used to represent an age-restricted tax deferred
+ account such as a 401(k), IRA or annuity."""
+
+ def __init__(self, total_balance, roth_amount_subset, name, owner):
+ """C'tor for this class takes a roth_amount, the number of dollars
+ in the account that are in-plan Roth and can be taken out tax
+ free. It keeps this pile separate from the pretax pile and
+ tries to estimate taxes."""
+ self.roth = roth_amount_subset
+ self.pretax = total_balance - roth_amount_subset
+ self.total_roth_withdrawals = 0
+ self.total_pretax_withdrawals = 0
+ self.total_investment_gains = 0
+ self.total_roth_conversions = 0
+ self.initial_balance = self.get_balance()
+
+ # This calls the super class' c'tor.
+ account.__init__(self, name, owner)
+
+ # These accounts are all age-restricted
+ def is_age_restricted(self):
+ return True
+
+ def withdraw(self, amount, taxes):
+ """Assume that money withdrawn from this account will be a mixture
+ of pretax funds (which count as ordinary income) and Roth funds
+ (which are available tax-free). Assume that the ratio of pretax
+ to Roth in this overall account determines the amount from each
+ partition in this withdrawal."""
+ balance = self.get_balance()
+ if balance < amount:
+ raise Exception("Insufficient funds")
+
+ ratio = float(self.roth) / float(balance)
+ roth_part = amount * ratio
+ pretax_part = amount - roth_part
+ if roth_part > 0:
+ self.roth -= roth_part
+ self.total_roth_withdrawals += roth_part
+ print "## Satisfying %s from %s with Roth money." % (utils.format_money(roth_part), self.name)
+ taxes.record_roth_income(roth_part)
+
+ self.pretax -= pretax_part
+ self.total_pretax_withdrawals += pretax_part
+ print "## Satisfying %s from %s with pre-tax money." % (utils.format_money(pretax_part), self.name)
+ taxes.record_ordinary_income(pretax_part)
+
+ def deposit(self, amount):
+ self.pretax_part += amount
+
+ def appreciate(self, multiplier):
+ """In this class we basically ignore the balance field in favor of
+ just using pretax and roth so that we can track them separately."""
+ old_pretax = self.pretax
+ self.pretax *= multiplier
+ self.total_investment_gains += self.pretax - old_pretax
+ old_roth = self.roth
+ self.roth *= multiplier
+ self.total_investment_gains += self.roth - old_roth
+
+ def get_balance(self):
+ """In this class we keep the balance in two parts."""
+ return self.pretax + self.roth
+
+ def has_rmd(self):
+ return True
+
+ # Used to compute RMDs. Source:
+ # https://www.irs.gov/publications/p590b#en_US_2018_publink1000231258
+ def get_actuary_number_years_to_live(self, age):
+ if age < 70:
+ return 27.4 + age
+ elif age == 70:
+ return 27.4
+ elif age == 71:
+ return 26.5
+ elif age == 72:
+ return 25.6
+ elif age == 73:
+ return 24.7
+ elif age == 74:
+ return 23.8
+ elif age == 75:
+ return 22.9
+ elif age == 76:
+ return 22.0
+ elif age == 77:
+ return 21.2
+ elif age == 78:
+ return 20.3
+ elif age == 79:
+ return 19.5
+ elif age == 80:
+ return 18.7
+ elif age == 81:
+ return 17.9
+ elif age == 82:
+ return 17.1
+ elif age == 83:
+ return 16.3
+ elif age == 84:
+ return 15.5
+ elif age == 85:
+ return 14.8
+ elif age == 86:
+ return 14.1
+ elif age == 87:
+ return 13.4
+ elif age == 88:
+ return 12.7
+ elif age == 89:
+ return 12.0
+ elif age == 90:
+ return 11.4
+ elif age == 91:
+ return 10.8
+ elif age == 92:
+ return 10.2
+ elif age == 93:
+ return 9.6
+ elif age == 94:
+ return 9.1
+ elif age == 95:
+ return 8.6
+ elif age == 96:
+ return 8.1
+ elif age == 97:
+ return 7.6
+ elif age == 98:
+ return 7.1
+ elif age == 99:
+ return 6.7
+ elif age == 100:
+ return 6.3
+ elif age == 101:
+ return 5.9
+ elif age == 102:
+ return 5.5
+ elif age == 103:
+ return 5.2
+ elif age == 104:
+ return 4.9
+ elif age == 105:
+ return 4.5
+ elif age == 106:
+ return 4.2
+ elif age == 107:
+ return 3.9
+ elif age == 108:
+ return 3.7
+ elif age == 109:
+ return 3.4
+ elif age == 110:
+ return 3.1
+ elif age == 111:
+ return 2.9
+ elif age == 112:
+ return 2.6
+ elif age == 113:
+ return 2.4
+ elif age == 114:
+ return 2.1
+ else:
+ return 1.9
+
+ def do_rmd_withdrawal(self, owner_age, taxes):
+ balance = self.get_balance()
+ if balance > 0 and owner_age >= 72:
+ rmd_factor = self.get_actuary_number_years_to_live(owner_age)
+ amount = balance / rmd_factor
+ self.withdraw(amount, taxes)
+ return amount
+ return 0
+
+ def has_roth(self):
+ return True
+
+ def do_roth_conversion(self, amount):
+ if amount <= 0:
+ return 0
+ if self.pretax >= amount:
+ self.roth += amount
+ self.pretax -= amount
+ self.total_roth_conversions += amount
+ print "## Executed pre-tax --> Roth conversion of %s in %s" % (
+ utils.format_money(amount),
+ self.get_name())
+ return amount
+ elif self.pretax > 0:
+ actual_amount = self.pretax
+ self.roth += actual_amount
+ self.pretax = 0
+ self.total_roth_conversions += actual_amount
+ print "## Executed pre-tax --> Roth conversion of %s in %s" % (
+ utils.format_money(actual_amount),
+ self.get_name())
+ return actual_amount
+ return 0
+
+ def dump_final_report(self):
+ super(age_restricted_tax_deferred_account, self).dump_final_report()
+ print " {:<50}: {:>14}".format("Initial balance",
+ utils.format_money(self.initial_balance))
+ print " {:<50}: {:>14}".format("Ending balance",
+ utils.format_money(self.get_balance()))
+ print " {:<50}: {:>14}".format("Total investment gains",
+ utils.format_money(self.total_investment_gains))
+ print " {:<50}: {:>14}".format("Total Roth withdrawals",
+ utils.format_money(self.total_roth_withdrawals))
+ print " {:<50}: {:>14}".format("Total pre-tax withdrawals",
+ utils.format_money(self.total_pretax_withdrawals))
+ print " {:<50}: {:>14}".format("Total pre-tax converted to Roth",
+ utils.format_money(self.total_roth_conversions))
+
+class age_restricted_roth_account(age_restricted_tax_deferred_account):
+ """This is an object to represent a Roth account like a Roth IRA. All
+ money in here is tax free. Most of the base account class works here
+ including the implementation of withdraw() which says that none of the
+ money withdrawn was taxable."""
+
+ def __init__(self, total_balance, name, owner):
+ """C'tor for this class takes a roth_amount, the number of dollars
+ in the account that are in-plan Roth and can be taken out tax
+ free. It keeps this pile separate from the pretax pile and
+ tries to estimate taxes."""
+ age_restricted_tax_deferred_account.__init__(
+ self, total_balance, 0, name, owner)
+
+ # Override
+ def has_rmd(self):
+ return False
+
+ # Override
+ def do_rmd_withdrawal(self, owner_age, taxes):
+ raise Exception("This account has no RMDs")
+
+ # Override
+ def do_roth_conversion(self, amount):
+ return 0
+
+class brokerage_account(account):
+ """A class to represent money in a taxable brokerage account."""
+
+ def __init__(self, total_balance, cost_basis, name, owner):
+ """The c'tor of this class partitions balance into three pieces:
+ the cost_basis (i.e. how much money was invested in the account),
+ the short_term_gain (i.e. appreciation that has been around for
+ less than a year) and long_term_gain (i.e. appreciation that has
+ been around for more than a year). We separate these because
+ taxes on long_term_gain (and qualified dividends, which are not
+ modeled) are usually lower than short_term_gain. Today those
+ are taxed at 15% and as ordinary income, respectively."""
+ self.cost_basis = float(cost_basis)
+ self.short_term_gain = 0.0
+ self.long_term_gain = float(total_balance - cost_basis)
+ self.total_cost_basis_withdrawals = 0
+ self.total_long_term_gain_withdrawals = 0
+ self.total_short_term_gain_withdrawals = 0
+ self.total_investment_gains = 0
+ self.initial_balance = self.get_balance()
+
+ # Call the base class' c'tor as well to set up the name.
+ account.__init__(self, name, owner)
+
+ def withdraw(self, amount, taxes):
+ """Override the base class' withdraw implementation since we're
+ dealing with three piles of money instead of one. When you sell
+ securities to get money out of this account the gains are taxed
+ (and the cost_basis part isn't). Assume that the ratio of
+ cost_basis to overall balance can be used to determine how much
+ of the withdrawal will be taxed (and how)."""
+ balance = self.get_balance()
+ if balance < amount:
+ raise Exception("Insufficient funds")
+
+ ratio = self.cost_basis / balance
+ invested_capital_part = amount * ratio
+ gains_part = amount - invested_capital_part
+
+ if self.cost_basis >= invested_capital_part:
+ self.cost_basis -= invested_capital_part
+ self.total_cost_basis_withdrawals += invested_capital_part
+ print "## Satisfying %s from %s as cost basis." % (utils.format_money(invested_capital_part), self.name)
+ self.withdraw_from_gains(gains_part, taxes)
+ else:
+ self.withdraw_from_gains(amount, taxes)
+
+ def withdraw_from_gains(self, amount, taxes):
+ """Withdraw some money from gains. Prefer the long term ones if
+ possible."""
+ if amount > (self.long_term_gain + self.short_term_gain):
+ raise Exception("Insufficient funds")
+
+ if self.long_term_gain >= amount:
+ self.long_term_gain -= amount
+ self.total_long_term_gain_withdrawals += amount
+ print "## Satisfying %s from %s as long term gains." % (
+ utils.format_money(amount),
+ self.name)
+ taxes.record_dividend_or_long_term_gain(amount)
+ return
+
+ else:
+ print "## Satisfying %s from %s as long term gains (exhausting all current long term gains in account)." % (
+ utils.format_money(self.long_term_gain),
+ self.name)
+ amount -= self.long_term_gain
+ self.total_long_term_gain_withdrawals += self.long_term_gain
+ taxes.record_dividend_or_long_term_gain(self.long_term_gain)
+ self.long_term_gain = 0
+ # Get the rest of amount from short term gains...
+
+ self.short_term_gain -= amount
+ self.total_short_term_gain_withdrawals += amount
+ print "## Satisfying %s from %s as short term gains." % (
+ utils.format_money(amount),
+ self.name)
+ taxes.record_short_term_gain(amount)
+
+ def deposit(self, amount):
+ self.cost_basis += amount
+
+ def get_balance(self):
+ """We're ignoring the base class' balance field in favor of tracking
+ it as three separate partitions of money."""
+ return self.cost_basis + self.long_term_gain + self.short_term_gain
+
+ def appreciate(self, multiplier):
+ """Appreciate... another year has passed so short_term_gains turn into
+ long_term_gains and the appreciation is our new short_term_gains."""
+ balance = self.get_balance()
+ balance *= multiplier
+ gain_or_loss = balance - self.get_balance()
+ self.total_investment_gains += gain_or_loss
+ self.long_term_gain += self.short_term_gain
+ self.short_term_gain = gain_or_loss
+
+ def is_age_restricted(self):
+ return False
+
+ def has_rmd(self):
+ return False
+
+ def has_roth(self):
+ return False
+
+ def do_roth_conversion(self, amount):
+ return 0
+
+ def dump_final_report(self):
+ super(brokerage_account, self).dump_final_report()
+ print " {:<50}: {:>14}".format("Initial balance",
+ utils.format_money(self.initial_balance))
+ print " {:<50}: {:>14}".format("Ending balance",
+ utils.format_money(self.get_balance()))
+ print " {:<50}: {:>14}".format("Total investment gains",
+ utils.format_money(self.total_investment_gains))
+ print " {:<50}: {:>14}".format("Total cost basis withdrawals",
+ utils.format_money(self.total_cost_basis_withdrawals))
+ print " {:<50}: {:>14}".format("Total long term gain withdrawals",
+ utils.format_money(self.total_long_term_gain_withdrawals))
+ print " {:<50}: {:>14}".format("Total short term gain withdrawals",
+ utils.format_money(self.total_short_term_gain_withdrawals))
--- /dev/null
+
+# Consts
+DEFAULT = 0
+SCOTT = 1
+LYNN = 2
+
+PESSIMISTIC_FEDERAL_INCOME_TAX_BRACKETS = [
+ [ 612351, 0.50 ],
+ [ 408201, 0.45 ],
+ [ 321451, 0.35 ],
+ [ 168401, 0.25 ],
+ [ 78951, 0.22 ],
+ [ 19401, 0.15 ],
+ [ 1, 0.12 ]
+]
+CURRENT_FEDERAL_INCOME_TAX_BRACKETS = [
+ [ 612351, 0.37 ],
+ [ 408201, 0.35 ],
+ [ 321451, 0.32 ],
+ [ 168401, 0.24 ],
+ [ 78951, 0.22 ],
+ [ 19401, 0.12 ],
+ [ 1, 0.10 ]
+]
+CURRENT_LONG_TERM_GAIN_FEDERAL_TAX_BRACKETS = [
+ [ 488851, 0.20 ],
+ [ 78751, 0.15 ],
+ [ 1, 0.00 ]
+]
+PESSIMISTIC_LONG_TERM_GAIN_FEDERAL_TAX_BRACKETS = (
+ PESSIMISTIC_FEDERAL_INCOME_TAX_BRACKETS)
--- /dev/null
+import constants
+import utils
+from tax_brackets import tax_brackets
+
+class parameters(object):
+ """A container to hold the initial states of several simulation
+ parameters. Play with them as you see fit and see what happens."""
+
+ def __init__(self):
+ self.with_default_values()
+
+ def with_default_values(self):
+ # Annual expenses in USD at the start of the simulation. This
+ # will be adjusted upwards with inflation_multiplier every year.
+ self.initial_annual_expenses = 144300
+
+ # The average US inflation rate during the simulation. The
+ # Fed's target rate is 2.0% as of 2020. The long term observed
+ # average historical inflation rate in the US is 2.1%.
+ #
+ # Note this is a multiplier... so 1.0 would be no inflation,
+ # 1.21 is 2.1% inflation, 0.9 is deflation, etc...
+ self.inflation_multiplier = 1.03
+
+ # We want to be able to model social security payments not
+ # keeping pace with inflation. Like the inflation_multiplier
+ # above, this is a multiplier. It affect the magnitide of
+ # social security payments year over year.
+ self.social_security_multiplier = 1.02
+
+ # This is the average investment return rate. Asset allocation has
+ # a large effect on this as does the overall economy. That said,
+ # the US stock market has returned 10%/year over a large enough
+ # time window. Our investments at Fidelity have returned 6.91%
+ # in the lifetime they have been there. The expected return of a
+ # 50/50 stock/bond investment mix based on historical data is 8.3%.
+ self.investment_multiplier = 1.04
+
+ # The age at which each person will take social security
+ # benefits in this simulation and how much she will get the
+ # first year. Note, this benefit size increases year over
+ # year at social_security_multiplier. This is what the social
+ # security website estimates if we both earn $0 from 2020 on:
+ #
+ # Lynn's benefits: Scott's benefits:
+ # age 62 - $21,120 age 62 - $21,420
+ # age 67 - $30,000 age 67 - $30,420
+ # age 70 - $37,200 age 70 - $37,728
+ #
+ self.social_security_age = [ 0, 62, 70 ]
+ self.initial_social_security_dollars = [ 0, 15000, 25000 ]
+
+ # Tax details... the standard deduction amount and tax
+ # brackets for ordinary income and long term capital gains.
+ self.federal_standard_deduction_dollars = 24800
+ self.federal_ordinary_income_tax_brackets = tax_brackets(
+ constants.PESSIMISTIC_FEDERAL_INCOME_TAX_BRACKETS)
+ self.federal_dividends_and_long_term_gains_brackets = tax_brackets(
+ constants.CURRENT_LONG_TERM_GAIN_FEDERAL_TAX_BRACKETS)
+ return self
+
+ def with_initial_annual_expenses(self, expenses):
+ self.initial_annual_expenses = expenses
+ return self
+
+ def get_initial_annual_expenses(self):
+ return self.initial_annual_expenses
+
+ def with_average_inflation_multiplier(self, multiplier):
+ self.inflation_multiplier = multiplier
+ return self
+
+ def get_average_inflation_multiplier(self):
+ return self.inflation_multiplier
+
+ def with_average_social_security_multiplier(self, multiplier):
+ self.social_security_multiplier = multiplier
+ return self
+
+ def get_average_social_security_multiplier(self):
+ return self.social_security_multiplier
+
+ def with_average_investment_return_multiplier(self, multiplier):
+ self.investment_multiplier = multiplier
+ return self
+
+ def get_average_investment_return_multiplier(self):
+ return self.investment_multiplier
+
+ def with_initial_social_security_age_and_benefits(self,
+ person,
+ age,
+ amount):
+ self.social_security_age[person] = age
+ self.initial_social_security_dollars[person] = amount
+ return self
+
+ def get_initial_social_security_age(self, person):
+ return self.social_security_age[person]
+
+ def get_initial_social_security_benefit(self, person):
+ return self.initial_social_security_dollars[person]
+
+ def with_federal_standard_deduction(self, deduction):
+ self.federal_standard_deduction_dollars = deduction
+ return self
+
+ def get_federal_standard_deduction(self):
+ return self.federal_standard_deduction_dollars
+
+ def with_federal_ordinary_income_tax_brackets(self, brackets):
+ self.federal_ordinary_income_tax_brackets = brackets
+ return self
+
+ def get_federal_ordinary_income_tax_brackets(self):
+ return self.federal_ordinary_income_tax_brackets
+
+ def with_federal_dividends_and_long_term_gains_income_tax_brackets(self,
+ brackets):
+ self.federal_dividends_and_long_term_gains_brackets = brackets;
+ return self
+
+ def get_federal_dividends_and_long_term_gains_income_tax_brackets(self):
+ return self.federal_dividends_and_long_term_gains_brackets
+
+ def dump(self):
+ print "SIMULATION PARAMETERS"
+ print " {:<50}: {:>14}".format("Initial year annual expenses",
+ utils.format_money(self.initial_annual_expenses))
+ print " {:<50}: {:>14}".format("Annual inflation rate",
+ utils.format_rate(self.inflation_multiplier))
+ print " {:<50}: {:>14}".format("Average annual investment return rate",
+ utils.format_rate(self.investment_multiplier))
+ print " {:<50}: {:>14}".format("Annual social security benefit increase rate",
+ utils.format_rate(self.social_security_multiplier))
+ print " {:<50}: {:>14}".format("Age at which Lynn takes social security",
+ self.social_security_age[constants.LYNN])
+ print " {:<50}: {:>14}".format("Lynn's first year social security benefit",
+ utils.format_money(self.initial_social_security_dollars[constants.LYNN]))
+ print " {:<50}: {:>14}".format("Age at which Scott takes social security",
+ self.social_security_age[constants.SCOTT])
+ print " {:<50}: {:>14}".format("Scott's first year social security benefit",
+ utils.format_money(self.initial_social_security_dollars[constants.SCOTT]))
+ print " Federal tax brackets ["
+ self.federal_ordinary_income_tax_brackets.dump()
+ print " ]"
+
+ print " Federal dividend and long term gain brackets ["
+ self.federal_dividends_and_long_term_gains_brackets.dump()
+ print " ]"
+ print " We assume Roth money continues to be available tax-free."
--- /dev/null
+#!/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()
--- /dev/null
+
+accounts = [ <<< your accounts >>> ]
--- /dev/null
+import utils
+
+class tax_brackets:
+ """A class to represent tax brackets and some operations on them."""
+
+ def __init__(self, brackets):
+ self.brackets = brackets
+
+ def compute_taxes_for_income(self, income):
+ """Compute the tax bill for income given our brackets."""
+ taxes_due = 0
+ while income > 1:
+ (threshold, rate) = self.get_bracket_for_income(income)
+ taxes_due += (income - threshold) * rate
+ income = threshold
+ return taxes_due
+
+ def get_bracket_for_income(self, income):
+ """Return the bracket that the last dollar of income was in."""
+ rate = 0.0
+ threshold = 0
+ for bracket in self.brackets:
+ # Bracket is a tuple of (monetary_threshold,
+ # tax_rate). So bracket[0] is a dollar amount and
+ # bracket[1] is a tax rate. e.g. ( 250000, 0.20 )
+ # indicates that every dollar you earn over 250K is
+ # taxed at 20%.
+ if (income > bracket[0] and rate < bracket[1]):
+ threshold = bracket[0]
+ rate = bracket[1]
+ return (threshold, rate)
+
+ def get_bracket_above_income(self, income):
+ """Return the next bracket above the one activated by income."""
+ rate = 1.0
+ threshold = None
+
+ # Walk the income tax brackets and look for the one just above
+ # the one this year's income is activating.
+ for bracket in self.brackets:
+ if (bracket[0] > income and bracket[1] < rate):
+ threshold = bracket[0]
+ rate = bracket[1]
+
+ # Note: this can return (None, 1.0) iff the income is already
+ # in the top tax bracket.
+ return (threshold, rate)
+
+ def get_bracket_below_income(self, income):
+ """Return the next bracket below the one activated by income."""
+ bracket = self.get_bracket_for_income(income)
+ income = bracket[0]
+ return self.get_bracket_for_income(income)
+
+ def get_effective_tax_rate_for_income(self, income):
+ """Compute and return the effective tax rate for an income."""
+ tax_bill = self.compute_taxes_for_income(income)
+ return float(tax_bill) / income
+
+ def adjust_with_multiplier(self, multiplier):
+ """Every year the IRS adjusts the income tax brackets for inflation."""
+ for bracket in self.brackets:
+ if bracket[0] != 1:
+ bracket[0] *= multiplier
+
+ def dump(self):
+ for x in self.brackets:
+ print "{:<20} -> {:<3}".format(utils.format_money(x[0]),
+ utils.format_rate(x[1]))
--- /dev/null
+import utils
+
+class tax_collector(object):
+ def __init__(self):
+ # These four accumulate and then clear every year (i.e. every call
+ # to record_taxes_paid_and_reset_for_next_year)
+ self.ordinary_income = 0
+ self.short_term_gains = 0
+ self.dividends_and_long_term_gains = 0
+ self.roth_income = 0
+
+ # The rest of these aggregate and are used as part of the final stats.
+ self.total_tax_bill = 0
+ self.total_ordinary_income = 0
+ self.total_short_term_gains = 0
+ self.total_dividends_and_long_term_gains = 0
+ self.total_roth_income = 0
+ self.total_aggregate_income = 0
+
+ def record_taxes_paid_and_reset_for_next_year(self,
+ taxes_paid):
+ self.total_tax_bill += taxes_paid
+ self.total_aggregate_income += self.get_total_income()
+ self.ordinary_income = 0
+ self.short_term_gains = 0
+ self.dividends_and_long_term_gains = 0
+ self.roth_income = 0
+
+ def record_ordinary_income(self, amount):
+ self.ordinary_income += amount
+ self.total_ordinary_income += amount
+
+ def record_short_term_gain(self, amount):
+ self.short_term_gains += amount
+ self.total_short_term_gains += amount
+
+ def record_dividend_or_long_term_gain(self, amount):
+ self.dividends_and_long_term_gains += amount
+ self.total_dividends_and_long_term_gains += amount
+
+ def record_roth_income(self, amount):
+ self.roth_income += amount
+ self.total_roth_income += amount
+
+ def approximate_taxes(self,
+ standard_deduction,
+ ordinary_income_tax_brackets,
+ dividends_and_long_term_gains_brackets):
+ taxes_due = 0
+
+ # Handle ordinary income:
+ ordinary_income = (self.ordinary_income +
+ self.short_term_gains -
+ standard_deduction)
+ if ordinary_income < 0:
+ ordinary_income = 0
+ taxes_due += ordinary_income_tax_brackets.compute_taxes_for_income(
+ ordinary_income)
+
+ # Handle dividends and long term gains:
+ taxes_due += dividends_and_long_term_gains_brackets.compute_taxes_for_income(
+ self.dividends_and_long_term_gains)
+
+ # Assume Roth money is still available tax free in the future.
+ return taxes_due
+
+ def how_many_more_dollars_can_we_earn_without_changing_tax_rate(
+ self,
+ federal_ordinary_income_tax_brackets):
+ """Return number of ordinary income dollars we can make without
+ changing this year's tax rate. Note: this may return None
+ which actually indicates something more like 'infinite since
+ you're already at the top tax bracket.'"""
+ income = self.ordinary_income
+ if income <= 1: income = 2
+ b = federal_ordinary_income_tax_brackets.get_bracket_above_income(
+ income)
+ if b[0] is not None:
+ delta = b[0] - income
+ return delta
+ return 0
+
+ def get_total_income(self):
+ return (self.ordinary_income +
+ self.short_term_gains +
+ self.dividends_and_long_term_gains +
+ self.roth_income)
+
+ def dump_final_report(self):
+ print " Taxes and income:"
+ print " {:<50}: {:>14}".format("Total aggregate income",
+ utils.format_money(self.total_aggregate_income))
+ print " ...{:<47}: {:>14}".format("Ordinary income",
+ utils.format_money(self.total_ordinary_income))
+ print " ...{:<47}: {:>14}".format("Income from short term gains",
+ utils.format_money(self.total_short_term_gains))
+ print " ...{:<47}: {:>14}".format("Income from dividends and long term gains",
+ utils.format_money(self.total_dividends_and_long_term_gains))
+ print " ...{:<47}: {:>14}".format("Roth income",
+ utils.format_money(self.total_roth_income))
+ print " {:<50}: {:>14}".format("Total taxes paid",
+ utils.format_money(self.total_tax_bill))
+ overall_tax_rate = float(self.total_tax_bill) / float(self.total_aggregate_income)
+ print " {:<50}: {:>14}".format("Effective tax rate",
+ utils.format_rate(overall_tax_rate))
--- /dev/null
+
+# Global helper functions
+def truncate(n, decimals=2):
+ multiplier = 10 ** decimals
+ return int(n * multiplier) / multiplier
+
+def format_money(number):
+ return ("${:,}".format(truncate(number)))
+
+def format_rate(rate):
+ if rate >= 1.0:
+ return format_rate(rate - 1.0)
+ else:
+ return "{:<}%".format(round(rate * 100, 3))