From: Scott Gasch Date: Tue, 14 Jan 2020 20:37:29 +0000 (-0800) Subject: Added some assertions to account.py. Cleaned up and simplified the X-Git-Url: https://wannabe.guru.org/gitweb/?a=commitdiff_plain;h=950cfdebc709a54c940041cd10df0f0aafd5fc51;p=retire.git Added some assertions to account.py. Cleaned up and simplified the go_find_money method in retire.py. --- diff --git a/accounts.py b/accounts.py index 81ab871..5f0def0 100644 --- a/accounts.py +++ b/accounts.py @@ -11,6 +11,7 @@ class account(object): 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): @@ -26,6 +27,7 @@ class account(object): return self.get_owner() == constants.LYNN def get_balance(self): + """Return the current account balance.""" pass def appreciate(self, multiplier): @@ -39,6 +41,7 @@ class account(object): pass def deposit(self, amount): + """Deposit money into the account.""" pass def is_age_restricted(self): @@ -51,12 +54,15 @@ class account(object): 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): @@ -68,6 +74,8 @@ 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 @@ -75,6 +83,7 @@ class age_restricted_tax_deferred_account(account): 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) @@ -89,9 +98,9 @@ class age_restricted_tax_deferred_account(account): (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 @@ -111,8 +120,6 @@ class age_restricted_tax_deferred_account(account): 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 @@ -242,6 +249,7 @@ class age_restricted_tax_deferred_account(account): 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" % ( @@ -275,6 +283,7 @@ class age_restricted_roth_account(age_restricted_tax_deferred_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" age_restricted_tax_deferred_account.__init__( self, total_balance, total_balance, name, owner) @@ -302,6 +311,8 @@ class brokerage_account(account): 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) @@ -321,9 +332,9 @@ class brokerage_account(account): (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 @@ -340,6 +351,7 @@ class brokerage_account(account): 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") @@ -370,6 +382,7 @@ class brokerage_account(account): 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): diff --git a/constants.py b/constants.py index ab86887..af0399a 100644 --- a/constants.py +++ b/constants.py @@ -4,6 +4,10 @@ DEFAULT = 0 SCOTT = 1 LYNN = 2 +def is_valid_owner(owner): + """Is an owner valid?""" + return (owner >= DEFAULT and owner <= LYNN) + PESSIMISTIC_FEDERAL_INCOME_TAX_BRACKETS = [ [ 612351, 0.50 ], [ 408201, 0.45 ], diff --git a/retire.py b/retire.py index 5fd5b12..270c608 100755 --- a/retire.py +++ b/retire.py @@ -34,89 +34,45 @@ class simulation(object): 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, @@ -185,107 +141,107 @@ class simulation(object): 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()