From cb38b693d10a844c80e6dea9f596bd419bfac3e4 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Tue, 14 Jan 2020 17:26:14 -0800 Subject: [PATCH] Adding comments and asserts. --- parameters.py | 4 + retire.py | 200 +++++++++++++++++++++++------------------------ tax_brackets.py | 6 ++ tax_collector.py | 8 ++ utils.py | 4 + 5 files changed, 122 insertions(+), 100 deletions(-) diff --git a/parameters.py b/parameters.py index 899545b..a8eb839 100644 --- a/parameters.py +++ b/parameters.py @@ -60,6 +60,7 @@ class parameters(object): return self def with_initial_annual_expenses(self, expenses): + assert expenses >= 0, "You can't have negative expenses" self.initial_annual_expenses = expenses return self @@ -91,7 +92,9 @@ class parameters(object): person, age, amount): + assert age >= 60 and age <= 70, "age should be between 60 and 70" self.social_security_age[person] = age + assert amount >= 0, "Social security won't pay negative dollars" self.initial_social_security_dollars[person] = amount return self @@ -102,6 +105,7 @@ class parameters(object): return self.initial_social_security_dollars[person] def with_federal_standard_deduction(self, deduction): + assert deduction >= 0, "Standard deduction should be non-negative" self.federal_standard_deduction_dollars = deduction return self diff --git a/retire.py b/retire.py index 270c608..a0b28cd 100755 --- a/retire.py +++ b/retire.py @@ -141,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. + 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) 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) - 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) + 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() diff --git a/tax_brackets.py b/tax_brackets.py index 29b0079..c64cf2b 100644 --- a/tax_brackets.py +++ b/tax_brackets.py @@ -13,10 +13,12 @@ class tax_brackets: (threshold, rate) = self.get_bracket_for_income(income) taxes_due += (income - threshold) * rate income = threshold + assert taxes_due >= 0, "Somehow computed negative tax bill?!" return taxes_due def get_bracket_for_income(self, income): """Return the bracket that the last dollar of income was in.""" + assert income > 0, "Income should be >0 to be taxed" rate = 0.0 threshold = 0 for bracket in self.brackets: @@ -32,6 +34,7 @@ class tax_brackets: def get_bracket_above_income(self, income): """Return the next bracket above the one activated by income.""" + assert income > 0, "Income should be >0 to be taxed" rate = 1.0 threshold = None @@ -48,12 +51,14 @@ class tax_brackets: def get_bracket_below_income(self, income): """Return the next bracket below the one activated by income.""" + assert income > 0, "Income should be >0 to be taxed" 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.""" + if income <= 0: return 0 tax_bill = self.compute_taxes_for_income(income) return float(tax_bill) / income @@ -64,6 +69,7 @@ class tax_brackets: bracket[0] *= multiplier def dump(self): + """Print out the tax brackets we're using in here.""" for x in self.brackets: print "{:<20} -> {:<3}".format(utils.format_money(x[0]), utils.format_rate(x[1])) diff --git a/tax_collector.py b/tax_collector.py index 09dc3c7..248c944 100644 --- a/tax_collector.py +++ b/tax_collector.py @@ -19,26 +19,32 @@ class tax_collector(object): def record_taxes_paid_and_reset_for_next_year(self, taxes_paid): + assert taxes_paid >= 0, "You can't pay negative taxes" self.total_tax_bill += taxes_paid self.total_aggregate_income += self.get_total_income() + assert self.total_aggregate_income >= 0, "Accumulator should be >= 0" 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): + assert amount >= 0, "Income should be non-negative" self.ordinary_income += amount self.total_ordinary_income += amount def record_short_term_gain(self, amount): + assert amount >= 0, "Income should be non-negative" self.short_term_gains += amount self.total_short_term_gains += amount def record_dividend_or_long_term_gain(self, amount): + assert amount >= 0, "Income should be non-negative" self.dividends_and_long_term_gains += amount self.total_dividends_and_long_term_gains += amount def record_roth_income(self, amount): + assert amount >= 0, "Income should be non-negative" self.roth_income += amount self.total_roth_income += amount @@ -46,6 +52,7 @@ class tax_collector(object): standard_deduction, ordinary_income_tax_brackets, dividends_and_long_term_gains_brackets): + assert standard_deduction >= 0, "Standard deduction should be non-negative" taxes_due = 0 # Handle ordinary income: @@ -62,6 +69,7 @@ class tax_collector(object): self.dividends_and_long_term_gains) # Assume Roth money is still available tax free in the future. + assert taxes_due >= 0, "Computed negative taxes?!" return taxes_due def how_many_more_dollars_can_we_earn_without_changing_tax_rate( diff --git a/utils.py b/utils.py index a9cd2f0..ce66420 100644 --- a/utils.py +++ b/utils.py @@ -1,13 +1,17 @@ # Global helper functions def truncate(n, decimals=2): + """Truncate a float to a particular number of decimals.""" + assert decimals > 0 and decimals < 10, "Decimals is weird" multiplier = 10 ** decimals return int(n * multiplier) / multiplier def format_money(number): + """Format a monetary amount with a $ and comma thousands separators.""" return ("${:,}".format(truncate(number))) def format_rate(rate): + """Format a multiplier nee rate to look nice.""" if rate >= 1.0: return format_rate(rate - 1.0) else: -- 2.45.0