#!/usr/bin/env python3 from typing import List, Optional, Tuple from type.money import Money from type.rate import Rate import string_utils as su PESSIMISTIC_FEDERAL_INCOME_TAX_BRACKETS = [ (Money(612351), Rate(0.70)), (Money(408201), Rate(0.55)), (Money(321451), Rate(0.45)), (Money(168401), Rate(0.35)), (Money( 78951), Rate(0.26)), (Money( 19401), Rate(0.18)), (Money( 1), Rate(0.13)), ] CURRENT_FEDERAL_INCOME_TAX_BRACKETS = [ (Money(612351), Rate(0.37)), (Money(408201), Rate(0.35)), (Money(321451), Rate(0.32)), (Money(168401), Rate(0.24)), (Money( 78951), Rate(0.22)), (Money( 19401), Rate(0.12)), (Money( 1), Rate(0.10)), ] CURRENT_LONG_TERM_GAIN_FEDERAL_TAX_BRACKETS = [ (Money(488851), Rate(0.20)), (Money( 78751), Rate(0.15)), (Money( 1), Rate(0.00)), ] PESSIMISTIC_LONG_TERM_GAIN_FEDERAL_TAX_BRACKETS = ( PESSIMISTIC_FEDERAL_INCOME_TAX_BRACKETS) federal_standard_deduction_dollars = Money(24800) # Federal tax standard deduction federal_ordinary_income_tax_brackets = PESSIMISTIC_FEDERAL_INCOME_TAX_BRACKETS federal_dividends_and_long_term_gains_brackets = CURRENT_LONG_TERM_GAIN_FEDERAL_TAX_BRACKETS class TaxCollector(object): def __init__( self, ordinary_income_brackets, dividends_and_long_term_gains_brackets, ): self.ordinary_income_brackets = ordinary_income_brackets self.dividends_and_long_term_gains_brackets = dividends_and_long_term_gains_brackets # These four accumulate and then clear every year (i.e. every call # to record_taxes_paid_and_reset_for_next_year) self.ordinary_income = Money(0) self.short_term_gains = Money(0) self.dividends_and_long_term_gains = Money(0) self.roth_income = Money(0) # The rest of these aggregate and are used as part of the final stats. self.total_tax_bill = Money(0) self.total_ordinary_income = Money(0) self.total_short_term_gains = Money(0) self.total_dividends_and_long_term_gains = Money(0) self.total_roth_income = Money(0) self.total_aggregate_income = Money(0) self.max_annual_income = None self.max_annual_income_year = None def record_taxes_paid_and_reset_for_next_year(self, current_year: int): self.total_tax_bill += self.approximate_annual_taxes() income = self.get_total_annual_income() if self.max_annual_income is None or income > self.max_annual_income: self.max_annual_income = income self.max_annual_income_year = current_year self.total_aggregate_income += income self.ordinary_income = Money(0) self.short_term_gains = Money(0) self.dividends_and_long_term_gains = Money(0) self.roth_income = Money(0) def record_ordinary_income(self, amount: Money): self.ordinary_income += amount self.total_ordinary_income += amount def record_short_term_gain(self, amount: Money): self.short_term_gains += amount self.total_short_term_gains += amount def record_dividend_or_long_term_gain(self, amount: Money): self.dividends_and_long_term_gains += amount self.total_dividends_and_long_term_gains += amount def record_roth_income(self, amount: Money): self.roth_income += amount self.total_roth_income += amount def approximate_annual_taxes(self): taxes_due = Money(0) # Handle ordinary income: ordinary_income = (self.ordinary_income + self.short_term_gains - federal_standard_deduction_dollars) if ordinary_income < 0: ordinary_income = 0 taxes_due += self.compute_taxes_on_income_using_brackets( ordinary_income, self.ordinary_income_brackets ) # Handle dividends and long term gains: taxes_due += self.compute_taxes_on_income_using_brackets( self.dividends_and_long_term_gains, self.dividends_and_long_term_gains_brackets ) # Assume Roth money is still available tax free in the future. return taxes_due def compute_taxes_on_income_using_brackets( self, income: Money, brackets: List[Tuple[Money, Rate]] ) -> Money: taxes_due = Money(0) while income > Money(1.0): rate = Rate(0.0) threshold = 0 for bracket in 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] taxed_amount = income - threshold taxes_due += taxed_amount * rate income = threshold return taxes_due def how_many_more_dollars_can_we_earn_without_changing_tax_rate( self ) -> Optional[Money]: """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.'""" threshold = None rate = Rate(999.0) income = self.ordinary_income # Kinda hacky but if there's been no ordinary income this year # this method was saying "oh, well, you can earn one dollar # before your tax rate goes from zero to something non-zero." # This is correct but not useful -- this method is used to # opportunistically shuffle pretax money into Roth. So let's # assume we've earned at least one dollar of ordinary income # this year and work from there. if income <= Money(0): income = Money(1.0) # Walk the income tax brackets and look for the one just above # the one this year's income is activating. for bracket in self.ordinary_income_brackets: if (bracket[0] > income and bracket[1] < rate): threshold = bracket[0] rate = bracket[1] # If we found the bracket just above here, use it to compute a # delta. if threshold is not None: assert threshold > income delta = threshold - income return delta return None def get_total_annual_income(self) -> Money: return ( self.ordinary_income + self.short_term_gains + self.dividends_and_long_term_gains + self.roth_income ) def approximate_annual_tax_rate(self) -> Rate: total_income = self.get_total_annual_income() total_taxes = self.approximate_annual_taxes() return Rate( multiplier=float(total_taxes) / float(total_income) ) def __repr__(self): with su.SprintfStdout() as ret: print("Taxes and Income:") print(" %-50s: %18s" % ("Total aggregate income", self.total_aggregate_income)) print(" ...%-47s: %18s" % ("Ordinary income", self.total_ordinary_income)) print(" ...%-47s: %18s" % ("Income from short term gains", self.total_short_term_gains)) print(" ...%-47s: %18s" % ("Income from dividends and long term gains", self.total_dividends_and_long_term_gains)) print(" ...%-47s: %18s" % ("Roth income", self.total_roth_income)) print(" %-50s: %18s (%s)" % ("Maximum annual income", self.max_annual_income, f'in {self.max_annual_income_year}')) print(" %-50s: %18s" % ("Total taxes paid", self.total_tax_bill)) overall_tax_rate = Rate( float(self.total_tax_bill) / float(self.total_aggregate_income) ) print(" %-50s: %18s" % ("Effective tax rate", overall_tax_rate), end='') return ret()