Updated.
[retire.git] / taxman.py
1 #!/usr/bin/env python3
2
3 from typing import List, Optional, Tuple
4
5 from type.money import Money
6 from type.rate import Rate
7 import string_utils as su
8
9
10 PESSIMISTIC_FEDERAL_INCOME_TAX_BRACKETS = [
11     (Money(612351), Rate(0.70)),
12     (Money(408201), Rate(0.55)),
13     (Money(321451), Rate(0.45)),
14     (Money(168401), Rate(0.35)),
15     (Money( 78951), Rate(0.26)),
16     (Money( 19401), Rate(0.18)),
17     (Money(     1), Rate(0.13)),
18 ]
19
20 CURRENT_FEDERAL_INCOME_TAX_BRACKETS = [
21     (Money(612351), Rate(0.37)),
22     (Money(408201), Rate(0.35)),
23     (Money(321451), Rate(0.32)),
24     (Money(168401), Rate(0.24)),
25     (Money( 78951), Rate(0.22)),
26     (Money( 19401), Rate(0.12)),
27     (Money(     1), Rate(0.10)),
28 ]
29
30 CURRENT_LONG_TERM_GAIN_FEDERAL_TAX_BRACKETS = [
31     (Money(488851), Rate(0.20)),
32     (Money( 78751), Rate(0.15)),
33     (Money(     1), Rate(0.00)),
34 ]
35
36 PESSIMISTIC_LONG_TERM_GAIN_FEDERAL_TAX_BRACKETS = (
37     PESSIMISTIC_FEDERAL_INCOME_TAX_BRACKETS)
38
39
40 federal_standard_deduction_dollars = Money(24800)   # Federal tax standard deduction
41 federal_ordinary_income_tax_brackets = PESSIMISTIC_FEDERAL_INCOME_TAX_BRACKETS
42 federal_dividends_and_long_term_gains_brackets = CURRENT_LONG_TERM_GAIN_FEDERAL_TAX_BRACKETS
43
44
45 class TaxCollector(object):
46     def __init__(
47             self,
48             ordinary_income_brackets,
49             dividends_and_long_term_gains_brackets,
50     ):
51         self.ordinary_income_brackets = ordinary_income_brackets
52         self.dividends_and_long_term_gains_brackets = dividends_and_long_term_gains_brackets
53
54         # These four accumulate and then clear every year (i.e. every call
55         # to record_taxes_paid_and_reset_for_next_year)
56         self.ordinary_income = Money(0)
57         self.short_term_gains = Money(0)
58         self.dividends_and_long_term_gains = Money(0)
59         self.roth_income = Money(0)
60
61         # The rest of these aggregate and are used as part of the final stats.
62         self.total_tax_bill = Money(0)
63         self.total_ordinary_income = Money(0)
64         self.total_short_term_gains = Money(0)
65         self.total_dividends_and_long_term_gains = Money(0)
66         self.total_roth_income = Money(0)
67         self.total_aggregate_income = Money(0)
68         self.max_annual_income = None
69         self.max_annual_income_year = None
70
71     def record_taxes_paid_and_reset_for_next_year(self, current_year: int):
72         self.total_tax_bill += self.approximate_annual_taxes()
73         income = self.get_total_annual_income()
74         if self.max_annual_income is None or income > self.max_annual_income:
75             self.max_annual_income = income
76             self.max_annual_income_year = current_year
77         self.total_aggregate_income += income
78         self.ordinary_income = Money(0)
79         self.short_term_gains = Money(0)
80         self.dividends_and_long_term_gains = Money(0)
81         self.roth_income = Money(0)
82
83     def record_ordinary_income(self, amount: Money):
84         self.ordinary_income += amount
85         self.total_ordinary_income += amount
86
87     def record_short_term_gain(self, amount: Money):
88         self.short_term_gains += amount
89         self.total_short_term_gains += amount
90
91     def record_dividend_or_long_term_gain(self, amount: Money):
92         self.dividends_and_long_term_gains += amount
93         self.total_dividends_and_long_term_gains += amount
94
95     def record_roth_income(self, amount: Money):
96         self.roth_income += amount
97         self.total_roth_income += amount
98
99     def approximate_annual_taxes(self):
100         taxes_due = Money(0)
101
102         # Handle ordinary income:
103         ordinary_income = (self.ordinary_income +
104                            self.short_term_gains -
105                            federal_standard_deduction_dollars)
106         if ordinary_income < 0:
107             ordinary_income = 0
108         taxes_due += self.compute_taxes_on_income_using_brackets(
109             ordinary_income,
110             self.ordinary_income_brackets
111         )
112
113         # Handle dividends and long term gains:
114         taxes_due += self.compute_taxes_on_income_using_brackets(
115             self.dividends_and_long_term_gains,
116             self.dividends_and_long_term_gains_brackets
117         )
118
119         # Assume Roth money is still available tax free in the future.
120         return taxes_due
121
122     def compute_taxes_on_income_using_brackets(
123             self,
124             income: Money,
125             brackets: List[Tuple[Money, Rate]]
126     ) -> Money:
127         taxes_due = Money(0)
128         while income > Money(1.0):
129             rate = Rate(0.0)
130             threshold = 0
131             for bracket in brackets:
132                 # Bracket is a tuple of (monetary_threshold,
133                 # tax_rate).  So bracket[0] is a dollar amount and
134                 # bracket[1] is a tax rate.  e.g. ( 250000, 0.20 )
135                 # indicates that every dollar you earn over 250K is
136                 # taxed at 20%.
137                 if (income > bracket[0] and rate < bracket[1]):
138                     threshold = bracket[0]
139                     rate = bracket[1]
140             taxed_amount = income - threshold
141             taxes_due += taxed_amount * rate
142             income = threshold
143         return taxes_due
144
145     def how_many_more_dollars_can_we_earn_without_changing_tax_rate(
146             self
147     ) -> Optional[Money]:
148         """Return number of ordinary income dollars we can make without
149            changing this year's tax rate.  Note: this may return None
150            which actually indicates something more like 'infinite since
151            you're already at the top tax bracket.'"""
152         threshold = None
153         rate = Rate(999.0)
154         income = self.ordinary_income
155
156         # Kinda hacky but if there's been no ordinary income this year
157         # this method was saying "oh, well, you can earn one dollar
158         # before your tax rate goes from zero to something non-zero."
159         # This is correct but not useful -- this method is used to
160         # opportunistically shuffle pretax money into Roth.  So let's
161         # assume we've earned at least one dollar of ordinary income
162         # this year and work from there.
163         if income <= Money(0):
164             income = Money(1.0)
165
166         # Walk the income tax brackets and look for the one just above
167         # the one this year's income is activating.
168         for bracket in self.ordinary_income_brackets:
169             if (bracket[0] > income and bracket[1] < rate):
170                 threshold = bracket[0]
171                 rate = bracket[1]
172
173         # If we found the bracket just above here, use it to compute a
174         # delta.
175         if threshold is not None:
176             assert threshold > income
177             delta = threshold - income
178             return delta
179         return None
180
181     def get_total_annual_income(self) -> Money:
182         return (
183             self.ordinary_income +
184             self.short_term_gains +
185             self.dividends_and_long_term_gains +
186             self.roth_income
187         )
188
189     def approximate_annual_tax_rate(self) -> Rate:
190         total_income = self.get_total_annual_income()
191         total_taxes = self.approximate_annual_taxes()
192         return Rate(
193             multiplier=float(total_taxes) / float(total_income)
194         )
195
196     def __repr__(self):
197         with su.SprintfStdout() as ret:
198             print("Taxes and Income:")
199             print("    %-50s: %18s" % ("Total aggregate income",
200                                        self.total_aggregate_income))
201             print("    ...%-47s: %18s" % ("Ordinary income",
202                                           self.total_ordinary_income))
203             print("    ...%-47s: %18s" % ("Income from short term gains",
204                                           self.total_short_term_gains))
205             print("    ...%-47s: %18s" % ("Income from dividends and long term gains",
206                                           self.total_dividends_and_long_term_gains))
207             print("    ...%-47s: %18s" % ("Roth income",
208                                           self.total_roth_income))
209             print("    %-50s: %18s (%s)" % ("Maximum annual income",
210                                             self.max_annual_income,
211                                             f'in {self.max_annual_income_year}'))
212             print("    %-50s: %18s" % ("Total taxes paid",
213                                        self.total_tax_bill))
214             overall_tax_rate = Rate(
215                 float(self.total_tax_bill) / float(self.total_aggregate_income)
216             )
217             print("    %-50s: %18s" % ("Effective tax rate",
218                                        overall_tax_rate), end='')
219         return ret()