def __init__(self, name, owner):
"""Constructor for account object takes a name and owner"""
self.name = name
+ assert constants.is_valid_owner(owner), "Bad account owner"
self.owner = owner
def get_name(self):
return self.get_owner() == constants.LYNN
def get_balance(self):
+ """Return the current account balance."""
pass
def appreciate(self, multiplier):
pass
def deposit(self, amount):
+ """Deposit money into the account."""
pass
def is_age_restricted(self):
pass
def do_rmd_withdrawal(self, owner_age, taxes):
+ """Handle RMD withdrawals for account."""
pass
def has_roth(self):
+ """Does this account have a Roth part?"""
pass
def dump_final_report(self):
+ """Output account-specific info for the final simulation report."""
print " " + self.name + ":"
class age_restricted_tax_deferred_account(account):
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."""
+ assert total_balance >= 0, "Initial balance must be >= 0"
+ assert roth_amount_subset <= total_balance, "Roth subset too high!"
self.roth = roth_amount_subset
self.pretax = total_balance - roth_amount_subset
self.total_roth_withdrawals = 0
self.total_investment_gains = 0
self.total_roth_conversions = 0
self.initial_balance = self.get_balance()
+ assert self.initial_balance >= 0, "Bad initial balance"
# This calls the super class' c'tor.
account.__init__(self, name, owner)
(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."""
+ if amount <= 0: return 0
balance = self.get_balance()
- if balance < amount:
- raise Exception("Insufficient funds")
+ if balance < amount: raise Exception("Insufficient funds")
ratio = float(self.roth) / float(balance)
roth_part = amount * ratio
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
amount = min(amount, self.pretax)
self.roth += amount
self.pretax -= amount
+ assert self.pretax >= 0, "Somehow exhausted more than all pretax money"
self.total_roth_conversions += amount
taxes.record_ordinary_income(amount)
print "## Executed pre-tax --> Roth conversion of %s in %s" % (
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."""
+ assert total_balance >= 0, "Initial balance must be >= 0"
age_restricted_tax_deferred_account.__init__(
self, total_balance, total_balance, name, owner)
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."""
+ assert total_balance >= 0, "Initial balance must be >= 0"
+ assert cost_basis <= total_balance, "Bad initial cost basis"
self.cost_basis = float(cost_basis)
self.short_term_gain = 0.0
self.long_term_gain = float(total_balance - cost_basis)
(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)."""
+ if amount <= 0: return 0
balance = self.get_balance()
- if balance < amount:
- raise Exception("Insufficient funds")
+ if balance < amount: raise Exception("Insufficient funds")
ratio = self.cost_basis / balance
invested_capital_part = amount * ratio
def withdraw_from_gains(self, amount, taxes):
"""Withdraw some money from gains. Prefer the long term ones if
possible."""
+ if amount <= 0: return 0
if amount > (self.long_term_gain + self.short_term_gain):
raise Exception("Insufficient funds")
taxes.record_short_term_gain(amount)
def deposit(self, amount):
+ assert amount >= 0, "Can't deposit negative amounts"
self.cost_basis += amount
def get_balance(self):
total_withdrawn += rmd
return total_withdrawn
- def go_find_money(self, amount, taxes):
+ def go_find_money(self, amount_needed, 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
+ amount_to_withdraw = min(amount_needed, x.get_balance())
+ print "## Withdrawing %s from %s" % (utils.format_money(amount_to_withdraw), x.get_name())
+ x.withdraw(amount_to_withdraw, taxes)
+ amount_needed -= amount_to_withdraw
+ if amount_needed <= 0: return
# 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
+ ((x.belongs_to_lynn() and self.lynn_age > 60) or
+ (x.belongs_to_scott() and self.scott_age > 60))):
- 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
+ amount_to_withdraw = min(amount_needed, x.get_balance())
+ print "## Withdrawing %s from %s" % (utils.format_money(amount_to_withdraw), x.get_name())
+ x.withdraw(amount_to_withdraw, taxes)
+ amount_needed -= amount_to_withdraw
+ if amount_needed <= 0: return
# 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
+ ((x.belongs_to_lynn() and self.lynn_age > 60) or
+ (x.belongs_to_scott() and self.scott_age > 60))):
- 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!")
+ amount_to_withdraw = min(amount_needed, x.get_balance())
+ print "## Withdrawing %s from %s" % (utils.format_money(amount_to_withdraw), x.get_name())
+ x.withdraw(amount_to_withdraw, taxes)
+ amount_needed -= amount_to_withdraw
+ if amount_needed <= 0: return
+ raise Exception("Unable to find enough money this year, still need %s more!" % utils.format_money(amount_needed))
def get_social_security(self,
scott_annual_social_security_dollars,
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
-
- look_for_conversions = True
- while True:
- # 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)
- print "INCOME: %s, TAXES: %s\n" % (utils.format_money(total_income), utils.format_money(taxes_due))
-
- if (look_for_conversions and
- tax_rate <= 0.14 and
- taxes_due < 20000 and
- self.year <= 2035):
-
- look_for_conversions = self.do_opportunistic_roth_conversions(taxes)
- # because these conversions affect taxes, spin
- # once more in the loop and recompute taxes
- # due and tax rate.
- else:
- break
-
- # 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)
+# 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
+
+ look_for_conversions = True
+ while True:
+ # 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)
+ print "INCOME: %s, TAXES: %s\n" % (utils.format_money(total_income), utils.format_money(taxes_due))
+
+ if (look_for_conversions and
+ tax_rate <= 0.14 and
+ taxes_due < 20000 and
+ self.year <= 2035):
+
+ look_for_conversions = self.do_opportunistic_roth_conversions(taxes)
+ # because these conversions affect taxes, spin
+ # once more in the loop and recompute taxes
+ # due and tax rate.
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)
+ break
+
+ # 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()