owlplanner 2025.12.5__py3-none-any.whl → 2026.1.26__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. owlplanner/In Discussion #58, the case of Kim and Sam.md +307 -0
  2. owlplanner/__init__.py +20 -1
  3. owlplanner/abcapi.py +24 -23
  4. owlplanner/cli/README.md +50 -0
  5. owlplanner/cli/_main.py +52 -0
  6. owlplanner/cli/cli_logging.py +56 -0
  7. owlplanner/cli/cmd_list.py +83 -0
  8. owlplanner/cli/cmd_run.py +86 -0
  9. owlplanner/config.py +315 -136
  10. owlplanner/data/__init__.py +21 -0
  11. owlplanner/data/awi.csv +75 -0
  12. owlplanner/data/bendpoints.csv +49 -0
  13. owlplanner/data/newawi.csv +75 -0
  14. owlplanner/data/rates.csv +99 -98
  15. owlplanner/debts.py +315 -0
  16. owlplanner/fixedassets.py +288 -0
  17. owlplanner/mylogging.py +157 -25
  18. owlplanner/plan.py +1044 -332
  19. owlplanner/plotting/__init__.py +16 -3
  20. owlplanner/plotting/base.py +17 -3
  21. owlplanner/plotting/factory.py +16 -3
  22. owlplanner/plotting/matplotlib_backend.py +30 -7
  23. owlplanner/plotting/plotly_backend.py +33 -10
  24. owlplanner/progress.py +66 -9
  25. owlplanner/rates.py +366 -361
  26. owlplanner/socialsecurity.py +142 -22
  27. owlplanner/tax2026.py +170 -57
  28. owlplanner/timelists.py +316 -32
  29. owlplanner/utils.py +204 -5
  30. owlplanner/version.py +20 -1
  31. {owlplanner-2025.12.5.dist-info → owlplanner-2026.1.26.dist-info}/METADATA +50 -158
  32. owlplanner-2026.1.26.dist-info/RECORD +36 -0
  33. owlplanner-2026.1.26.dist-info/entry_points.txt +2 -0
  34. owlplanner-2026.1.26.dist-info/licenses/AUTHORS +15 -0
  35. owlplanner/tax2025.py +0 -339
  36. owlplanner-2025.12.5.dist-info/RECORD +0 -24
  37. {owlplanner-2025.12.5.dist-info → owlplanner-2026.1.26.dist-info}/WHEEL +0 -0
  38. {owlplanner-2025.12.5.dist-info → owlplanner-2026.1.26.dist-info}/licenses/LICENSE +0 -0
@@ -1,16 +1,23 @@
1
1
  """
2
+ Social Security benefit calculation rules and utilities.
2
3
 
3
- Owl/socialsecurity
4
- --------
4
+ This module implements Social Security rules including full retirement age
5
+ calculations, benefit computations, and related retirement planning functions.
5
6
 
6
- A retirement planner using linear programming optimization.
7
+ Copyright (C) 2025-2026 The Owlplanner Authors
7
8
 
8
- This file contains the rules related to social security.
9
+ This program is free software: you can redistribute it and/or modify
10
+ it under the terms of the GNU General Public License as published by
11
+ the Free Software Foundation, either version 3 of the License, or
12
+ (at your option) any later version.
9
13
 
10
- Copyright © 2025 - Martin-D. Lacasse
11
-
12
- Disclaimers: This code is for educational purposes only and does not constitute financial advice.
14
+ This program is distributed in the hope that it will be useful,
15
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ GNU General Public License for more details.
13
18
 
19
+ You should have received a copy of the GNU General Public License
20
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
14
21
  """
15
22
 
16
23
  import numpy as np
@@ -18,8 +25,23 @@ import numpy as np
18
25
 
19
26
  def getFRAs(yobs):
20
27
  """
21
- Return full retirement age based on birth year.
22
- Returns an array of fractional age.
28
+ Return full retirement age (FRA) based on birth year.
29
+
30
+ The FRA is determined by birth year according to Social Security rules:
31
+ - Birth year >= 1960: FRA is 67
32
+ - Birth year < 1960: FRA increases by 2 months for each year after 1954
33
+
34
+ Parameters
35
+ ----------
36
+ yobs : array-like
37
+ Array of birth years, one for each individual.
38
+
39
+ Returns
40
+ -------
41
+ numpy.ndarray
42
+ Array of FRA values in fractional years (1/12 increments), one for each individual.
43
+ Ages are returned in Social Security age format. Comparisons to FRA should be
44
+ done using Social Security age (which accounts for birthday-on-first adjustments).
23
45
  """
24
46
  fras = np.zeros(len(yobs))
25
47
 
@@ -35,7 +57,32 @@ def getFRAs(yobs):
35
57
 
36
58
  def getSpousalBenefits(pias):
37
59
  """
38
- Compute spousal benefit. Returns an array.
60
+ Compute the maximum spousal benefit amount for each individual.
61
+
62
+ The spousal benefit is calculated as 50% of the spouse's Primary Insurance Amount (PIA),
63
+ minus the individual's own PIA. The result is the additional benefit the individual
64
+ would receive as a spouse, which cannot be negative.
65
+
66
+ Note: This calculation is not affected by which day of the month is the birthday.
67
+
68
+ Parameters
69
+ ----------
70
+ pias : array-like
71
+ Array of Primary Insurance Amounts (monthly benefit at FRA), one for each individual.
72
+ Must have exactly 1 or 2 entries.
73
+
74
+ Returns
75
+ -------
76
+ numpy.ndarray
77
+ Array of spousal benefit amounts (monthly), one for each individual.
78
+ For a single individual, returns [0].
79
+ For two individuals, returns the additional spousal benefit each would receive
80
+ (which is max(0, 0.5 * spouse_PIA - own_PIA)).
81
+
82
+ Raises
83
+ ------
84
+ ValueError
85
+ If the pias array does not have exactly 1 or 2 entries.
39
86
  """
40
87
  icount = len(pias)
41
88
  benefits = np.zeros(icount)
@@ -51,15 +98,52 @@ def getSpousalBenefits(pias):
51
98
  return benefits
52
99
 
53
100
 
54
- def getSelfFactor(fra, age):
101
+ def getSelfFactor(fra, convage, bornOnFirstDays):
55
102
  """
56
- Return factor to multiply PIA given the age when SS starts.
57
- Year of FRA and age can be fractional.
103
+ Return the reduction/increase factor to multiply PIA based on claiming age.
104
+
105
+ This function calculates the adjustment factor for self benefits based on when
106
+ Social Security benefits start relative to Full Retirement Age (FRA):
107
+ - Before FRA: Benefits are reduced (minimum 70% at age 62)
108
+ - At FRA: Full benefit (100% of PIA)
109
+ - After FRA: Benefits are increased by 8% per year (up to 132% at age 70)
110
+
111
+ The function automatically adjusts for Social Security age if the birthday is on
112
+ the 1st or 2nd day of the month (adds 1/12 year to conventional age), consistent
113
+ with SSA rules that treat both days the same for age calculation purposes.
114
+
115
+ Parameters
116
+ ----------
117
+ fra : float
118
+ Full Retirement Age in years (can be fractional with 1/12 increments).
119
+ convage : float
120
+ Conventional age when benefits start, in years (can be fractional with 1/12 increments).
121
+ Must be between 62 and 70 inclusive.
122
+ bornOnFirstDays : bool
123
+ True if birthday is on the 1st or 2nd day of the month, False otherwise.
124
+ If True, the function adds 1/12 year to convert to Social Security age.
125
+
126
+ Returns
127
+ -------
128
+ float
129
+ Factor to multiply PIA. Examples:
130
+ - 0.75 = 75% of PIA (claiming at 62 with FRA of 66)
131
+ - 1.0 = 100% of PIA (claiming at FRA)
132
+ - 1.32 = 132% of PIA (claiming at 70 with FRA of 66)
133
+
134
+ Raises
135
+ ------
136
+ ValueError
137
+ If convage is less than 62 or greater than 70.
58
138
  """
59
- if age < 62 or age > 70:
60
- raise ValueError(f"Age {age} out of range.")
139
+ if convage < 62 or convage > 70:
140
+ raise ValueError(f"Age {convage} out of range.")
141
+
142
+ # Add a month to conventional age if born on the 1st or 2nd (SSA treats both the same).
143
+ offset = 0 if not bornOnFirstDays else 1/12
144
+ ssage = convage + offset
61
145
 
62
- diff = fra - age
146
+ diff = fra - ssage
63
147
  if diff <= 0:
64
148
  return 1. - .08 * diff
65
149
  elif diff <= 3:
@@ -70,15 +154,51 @@ def getSelfFactor(fra, age):
70
154
  return .8 - 0.05 * (diff - 3)
71
155
 
72
156
 
73
- def getSpousalFactor(fra, age):
157
+ def getSpousalFactor(fra, convage, bornOnFirstDays):
74
158
  """
75
- Return factor to multiply spousal benefit given the age when benefit starts.
76
- Year of FRA and age can be fractional.
159
+ Return the reduction factor to multiply spousal benefits based on claiming age.
160
+
161
+ This function calculates the adjustment factor for spousal benefits based on when
162
+ benefits start relative to Full Retirement Age (FRA):
163
+ - Before FRA: Benefits are reduced (minimum 32.5% at age 62)
164
+ - At or after FRA: Full spousal benefit (50% of spouse's PIA, no increase for delay)
165
+
166
+ The function automatically adjusts for Social Security age if the birthday is on
167
+ the 1st or 2nd day of the month (adds 1/12 year to conventional age), consistent
168
+ with SSA rules that treat both days the same for age calculation purposes.
169
+
170
+ Parameters
171
+ ----------
172
+ fra : float
173
+ Full Retirement Age in years (can be fractional with 1/12 increments).
174
+ convage : float
175
+ Conventional age when benefits start, in years (can be fractional with 1/12 increments).
176
+ Must be at least 62 (no maximum, but no increase beyond FRA).
177
+ bornOnFirstDays : bool
178
+ True if birthday is on the 1st or 2nd day of the month, False otherwise.
179
+ If True, the function adds 1/12 year to convert to Social Security age.
180
+
181
+ Returns
182
+ -------
183
+ float
184
+ Factor to multiply spousal benefit. Examples:
185
+ - 0.325 = 32.5% reduction factor (claiming at 62 with FRA of 66)
186
+ - 1.0 = 100% of spousal benefit (claiming at or after FRA)
187
+ Note: Unlike self benefits, spousal benefits do not increase beyond FRA.
188
+
189
+ Raises
190
+ ------
191
+ ValueError
192
+ If convage is less than 62.
77
193
  """
78
- if age < 62:
79
- raise ValueError(f"Age {age} out of range.")
194
+ if convage < 62:
195
+ raise ValueError(f"Age {convage} out of range.")
196
+
197
+ # Add a month to conventional age if born on the 1st or 2nd (SSA treats both the same).
198
+ offset = 0 if not bornOnFirstDays else 1/12
199
+ ssage = convage + offset
80
200
 
81
- diff = fra - age
201
+ diff = fra - ssage
82
202
  if diff <= 0:
83
203
  return 1.
84
204
  elif diff <= 3:
owlplanner/tax2026.py CHANGED
@@ -1,39 +1,35 @@
1
1
  """
2
+ Tax calculation module for 2026 tax year rules.
2
3
 
3
- Owl/tax2026
4
- ---
4
+ This module handles all tax calculations including income tax brackets,
5
+ capital gains tax, and other tax-related computations based on 2026 tax rules.
5
6
 
6
- A retirement planner using linear programming optimization.
7
+ Copyright (C) 2025-2026 The Owlplanner Authors
7
8
 
8
- See companion document for a complete explanation and description
9
- of all variables and parameters.
9
+ This program is free software: you can redistribute it and/or modify
10
+ it under the terms of the GNU General Public License as published by
11
+ the Free Software Foundation, either version 3 of the License, or
12
+ (at your option) any later version.
10
13
 
11
- Module to handle all tax calculations.
12
-
13
- Copyright &copy; 2026 - Martin-D. Lacasse
14
-
15
- Disclaimers: This code is for educational purposes only and does not constitute financial advice.
14
+ This program is distributed in the hope that it will be useful,
15
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ GNU General Public License for more details.
16
18
 
19
+ You should have received a copy of the GNU General Public License
20
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
17
21
  """
18
22
 
19
23
  import numpy as np
20
24
  from datetime import date
21
25
 
22
26
 
23
- ##############################################################################
24
- # Prepare the data.
25
-
26
- taxBracketNames = ["10%", "12/15%", "22/25%", "24/28%", "32/33%", "35%", "37/40%"]
27
-
28
- rates_OBBBA = np.array([0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.370])
29
- rates_preTCJA = np.array([0.10, 0.15, 0.25, 0.28, 0.33, 0.35, 0.396])
30
-
31
27
  ###############################################################################
32
28
  # Start of section where rates need to be actualized every year.
33
29
  ###############################################################################
34
30
  # Single [0] and married filing jointly [1].
35
31
 
36
- # These are 2025 current.
32
+ # These are current for 2026, i.e., applying to tax year 2025.
37
33
  taxBrackets_OBBBA = np.array(
38
34
  [
39
35
  [12400, 50400, 105700, 201775, 256225, 640600, 9999999],
@@ -41,17 +37,20 @@ taxBrackets_OBBBA = np.array(
41
37
  ]
42
38
  )
43
39
 
40
+ # These are current for 2026 (2025TY).
44
41
  irmaaBrackets = np.array(
45
42
  [
46
- [0, 106000, 133000, 167000, 200000, 500000],
47
- [0, 212000, 266000, 334000, 400000, 750000],
43
+ [0, 109000, 137000, 171000, 205000, 500000],
44
+ [0, 218000, 274000, 342000, 410000, 750000],
48
45
  ]
49
46
  )
50
47
 
51
- # Index [0] stores the standard Medicare part B premium.
48
+ # These are current for 2026 (2025TY).
49
+ # Index [0] stores the standard Medicare part B basic premium.
52
50
  # Following values are incremental IRMAA part B monthly fees.
53
- irmaaFees = 12 * np.array([185.00, 74.00, 111.00, 110.90, 111.00, 37.00])
51
+ irmaaFees = 12 * np.array([202.90, 81.20, 121.70, 121.70, 121.70, 40.70])
54
52
 
53
+ #########################################################################
55
54
  # Make projection for pre-TCJA using 2017 to current year.
56
55
  # taxBrackets_2017 = np.array(
57
56
  # [ [9325, 37950, 91900, 191650, 416700, 418400, 9999999],
@@ -60,41 +59,60 @@ irmaaFees = 12 * np.array([185.00, 74.00, 111.00, 110.90, 111.00, 37.00])
60
59
  #
61
60
  # stdDeduction_2017 = [6350, 12700]
62
61
  #
63
- # For 2025, I used a 30.5% adjustment from 2017, rounded to closest 50.
62
+ # COLA from 2017: [2.0, 2.8, 1.6, 1.3, 5.9, 8.7, 3.2, 2.5, 2.8]
63
+ # For 2026, I used a 35.1% adjustment from 2017, rounded to closest 10.
64
64
  #
65
65
  # These are speculated.
66
66
  taxBrackets_preTCJA = np.array(
67
67
  [
68
- [12150, 49550, 119950, 250200, 544000, 546200, 9999999], # Single
69
- [24350, 99100, 199850, 304600, 543950, 614450, 9999999], # MFJ
68
+ [12600, 51270, 124160, 258920, 562960, 565260, 9999999], # Single
69
+ [25200, 102540, 206840, 315260, 562960, 635920, 9999999], # MFJ
70
70
  ]
71
71
  )
72
72
 
73
- # These are 2025 current.
73
+ # These are speculated (adjusted for inflation to 2026).
74
+ stdDeduction_preTCJA = np.array([8580, 17160]) # Single, MFJ
75
+ #########################################################################
76
+
77
+ # These are current for 2026 (2025TY).
74
78
  stdDeduction_OBBBA = np.array([16100, 32200]) # Single, MFJ
75
- # These are speculated (adjusted for inflation to 2026). TODO
76
- stdDeduction_preTCJA = np.array([8300, 16600]) # Single, MFJ
77
79
 
78
- # These are current (adjusted for inflation) per individual.
79
- extra65Deduction = np.array([2000, 1600]) # Single, MFJ
80
+ # These are current for 2026 (2025TY) per individual.
81
+ extra65Deduction = np.array([2000, 1600]) # Single, MFJ
80
82
 
81
- # Thresholds setting capital gains brackets 0%, 15%, 20% (adjusted for inflation).
83
+ # These are current for 2026 (2025TY).
84
+ # Thresholds setting capital gains brackets 0%, 15%, 20%.
82
85
  capGainRates = np.array(
83
86
  [
84
- [48350, 533400],
85
- [96700, 600050],
87
+ [49450, 545500],
88
+ [98900, 613700],
86
89
  ]
87
90
  )
88
91
 
92
+ ###############################################################################
93
+ # End of section where rates need to be actualized every year.
94
+ ###############################################################################
95
+
96
+ ###############################################################################
97
+ # Data that is unlikely to change.
98
+ ###############################################################################
99
+
89
100
  # Thresholds for net investment income tax (not adjusted for inflation).
90
101
  niitThreshold = np.array([200000, 250000])
91
102
  niitRate = 0.038
92
103
 
93
- # Thresholds for 65+ bonus for circumventing tax on social security.
104
+ # Thresholds for 65+ bonus of $6k per individual for circumventing tax
105
+ # on social security for low-income households. This expires in 2029.
106
+ # These numbers are hard-coded below as the tax code will likely change
107
+ # the rules for eligibility and will require a code review.
108
+ # Bonus decreases linearly above threshold by 1% / $1k over threshold.
94
109
  bonusThreshold = np.array([75000, 150000])
95
110
 
96
- ###############################################################################
97
- # End of section where rates need to be actualized every year.
111
+ taxBracketNames = ["10%", "12/15%", "22/25%", "24/28%", "32/33%", "35%", "37/40%"]
112
+
113
+ rates_OBBBA = np.array([0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.370])
114
+ rates_preTCJA = np.array([0.10, 0.15, 0.25, 0.28, 0.33, 0.35, 0.396])
115
+
98
116
  ###############################################################################
99
117
 
100
118
 
@@ -102,25 +120,26 @@ def mediVals(yobs, horizons, gamma_n, Nn, Nq):
102
120
  """
103
121
  Return tuple (nm, L, C) of year index when Medicare starts and vectors L, and C
104
122
  defining end points of constant piecewise linear functions representing IRMAA fees.
123
+ Costs C include the fact that one or two indivuals have to pay. Eligibility is built-in.
105
124
  """
106
125
  thisyear = date.today().year
107
126
  assert Nq == len(irmaaFees), f"Inconsistent value of Nq: {Nq}."
108
127
  assert Nq == len(irmaaBrackets[0]), "Inconsistent IRMAA brackets array."
109
128
  Ni = len(yobs)
110
- # What index year will Medicare start? 65 - age.
111
- nm = 65 - (thisyear - yobs)
112
- nm = np.min(nm)
129
+ # What index year will Medicare start? 65 - age for each individual.
130
+ nm = yobs + 65 - thisyear
131
+ nm = np.maximum(0, nm)
132
+ nmstart = np.min(nm)
113
133
  # Has it already started?
114
- nm = max(0, nm)
115
- Nmed = Nn - nm
134
+ Nmed = Nn - nmstart
116
135
 
117
136
  L = np.zeros((Nmed, Nq-1))
118
137
  C = np.zeros((Nmed, Nq))
119
138
 
120
- # Year starts at offset nm in the plan.
139
+ # Year starts at offset nmstart in the plan. L and C arrays are shorter.
121
140
  for nn in range(Nmed):
122
141
  imed = 0
123
- n = nm + nn
142
+ n = nmstart + nn
124
143
  if thisyear + n - yobs[0] >= 65 and n < horizons[0]:
125
144
  imed += 1
126
145
  if Ni == 2 and thisyear + n - yobs[1] >= 65 and n < horizons[1]:
@@ -132,28 +151,83 @@ def mediVals(yobs, horizons, gamma_n, Nn, Nq):
132
151
  else:
133
152
  raise RuntimeError("mediVals: This should never happen.")
134
153
 
135
- return nm, L, C
154
+ return nmstart, L, C
136
155
 
137
156
 
138
- def capitalGainTaxRate(Ni, magi_n, gamma_n, nd, Nn):
157
+ def capitalGainTax(Ni, txIncome_n, ltcg_n, gamma_n, nd, Nn):
139
158
  """
140
- Return an array of decimal rates for capital gains.
141
- Parameter nd is the index year of first passing of a spouse, if applicable,
142
- nd == Nn for single individuals.
159
+ Return an array of tax on capital gains.
160
+
161
+ Parameters:
162
+ -----------
163
+ Ni : int
164
+ Number of individuals (1 or 2)
165
+ txIncome_n : array
166
+ Array of taxable income for each year (ordinary income + capital gains)
167
+ ltcg_n : array
168
+ Array of long-term capital gains for each year
169
+ gamma_n : array
170
+ Array of inflation adjustment factors for each year
171
+ nd : int
172
+ Index year of first passing of a spouse, if applicable (nd == Nn for single individuals)
173
+ Nn : int
174
+ Total number of years in the plan
175
+
176
+ Returns:
177
+ --------
178
+ cgTax_n : array
179
+ Array of tax on capital gains for each year
180
+
181
+ Notes:
182
+ ------
183
+ Thresholds are determined by the taxable income which is roughly AGI - (standard/itemized) deductions.
184
+ Taxable income can also be thought of as taxable ordinary income + capital gains.
185
+
186
+ Long-term capital gains are taxed at 0%, 15%, or 20% based on total taxable income.
187
+ Capital gains "stack on top" of ordinary income, so the portion of gains that
188
+ pushes total income above each threshold is taxed at the corresponding rate.
143
189
  """
144
190
  status = Ni - 1
145
- cgRate_n = np.zeros(Nn)
191
+ cgTax_n = np.zeros(Nn)
146
192
 
147
193
  for n in range(Nn):
148
194
  if status and n == nd:
149
195
  status -= 1
150
196
 
151
- if magi_n[n] > gamma_n[n] * capGainRates[status][1]:
152
- cgRate_n[n] = 0.20
153
- elif magi_n[n] > gamma_n[n] * capGainRates[status][0]:
154
- cgRate_n[n] = 0.15
197
+ # Calculate ordinary income (taxable income minus capital gains).
198
+ ordIncome = txIncome_n[n] - ltcg_n[n]
199
+
200
+ # Get inflation-adjusted thresholds for this year.
201
+ threshold15 = gamma_n[n] * capGainRates[status][0] # 0% to 15% threshold
202
+ threshold20 = gamma_n[n] * capGainRates[status][1] # 15% to 20% threshold
155
203
 
156
- return cgRate_n
204
+ # Calculate how much LTCG falls in the 20% bracket.
205
+ # This is the portion of LTCG that pushes total income above threshold20.
206
+ if txIncome_n[n] > threshold20:
207
+ ltcg20 = min(ltcg_n[n], txIncome_n[n] - threshold20)
208
+ else:
209
+ ltcg20 = 0
210
+
211
+ # Calculate how much LTCG falls in the 15% bracket.
212
+ # This is the portion of LTCG in the range [threshold15, threshold20].
213
+ if ordIncome >= threshold20:
214
+ # All LTCG is already in the 20% bracket.
215
+ ltcg15 = 0
216
+ elif txIncome_n[n] > threshold15:
217
+ # Some LTCG falls in the 15% bracket.
218
+ # The 15% bracket spans from threshold15 to threshold20.
219
+ bracket_top = min(threshold20, txIncome_n[n])
220
+ bracket_bottom = max(threshold15, ordIncome)
221
+ ltcg15 = min(ltcg_n[n] - ltcg20, bracket_top - bracket_bottom)
222
+ else:
223
+ # Total income is below the 15% threshold.
224
+ ltcg15 = 0
225
+
226
+ # Remaining LTCG is in the 0% bracket (ltcg0 = ltcg_n[n] - ltcg20 - ltcg15).
227
+ # Calculate tax: 20% on ltcg20, 15% on ltcg15, 0% on remainder.
228
+ cgTax_n[n] = 0.20 * ltcg20 + 0.15 * ltcg15
229
+
230
+ return cgTax_n
157
231
 
158
232
 
159
233
  def mediCosts(yobs, horizons, magi, prevmagi, gamma_n, Nn):
@@ -257,7 +331,7 @@ def taxBrackets(N_i, n_d, N_n, yOBBBA=2099):
257
331
  # Number of years left in OBBBA from this year.
258
332
  thisyear = date.today().year
259
333
  if yOBBBA < thisyear:
260
- raise ValueError(f"Expiration year {yOBBBA} cannot be in the past.")
334
+ raise ValueError(f"OBBBA expiration year {yOBBBA} cannot be in the past.")
261
335
 
262
336
  ytc = yOBBBA - thisyear
263
337
 
@@ -273,7 +347,27 @@ def taxBrackets(N_i, n_d, N_n, yOBBBA=2099):
273
347
  return data
274
348
 
275
349
 
276
- def rho_in(yobs, N_n):
350
+ def computeNIIT(N_i, MAGI_n, I_n, Q_n, n_d, N_n):
351
+ """
352
+ Compute ACA tax on Dividends (Q) and Interests (I).
353
+ For accounting for rent and/or trust income, one can easily add a column
354
+ to the Wages and Contributions file and add yearly amount to Q_n + I_n below.
355
+ """
356
+ J_n = np.zeros(N_n)
357
+ status = N_i - 1
358
+
359
+ for n in range(N_n):
360
+ if status and n == n_d:
361
+ status -= 1
362
+
363
+ Gmax = niitThreshold[status]
364
+ if MAGI_n[n] > Gmax:
365
+ J_n[n] = niitRate * min(MAGI_n[n] - Gmax, I_n[n] + Q_n[n])
366
+
367
+ return J_n
368
+
369
+
370
+ def rho_in(yobs, longevity, N_n):
277
371
  """
278
372
  Return Required Minimum Distribution fractions for each individual.
279
373
  This implementation does not support spouses with more than
@@ -316,11 +410,30 @@ def rho_in(yobs, N_n):
316
410
  5.2,
317
411
  4.9,
318
412
  4.6,
413
+ 4.3,
414
+ 4.1,
415
+ 3.9,
416
+ 3.7,
417
+ 3.5,
418
+ 3.4,
419
+ 3.3,
420
+ 3.1,
421
+ 3.0,
422
+ 2.9,
423
+ 2.8,
424
+ 2.7,
425
+ 2.5,
426
+ 2.3,
427
+ 2.0
319
428
  ]
320
429
 
321
430
  N_i = len(yobs)
322
431
  if N_i == 2 and abs(yobs[0] - yobs[1]) > 10:
323
432
  raise RuntimeError("RMD: Unsupported age difference of more than 10 years.")
433
+ if np.any(np.array(longevity) > 120):
434
+ raise RuntimeError(
435
+ "RMD: Unsupported life expectancy over 120 years."
436
+ )
324
437
 
325
438
  rho = np.zeros((N_i, N_n))
326
439
  thisyear = date.today().year