owlplanner 2025.12.20__py3-none-any.whl → 2026.2.2__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.
owlplanner/tax2025.py DELETED
@@ -1,359 +0,0 @@
1
- """
2
-
3
- Owl/tax2025
4
- ---
5
-
6
- A retirement planner using linear programming optimization.
7
-
8
- See companion document for a complete explanation and description
9
- of all variables and parameters.
10
-
11
- Module to handle all tax calculations.
12
-
13
- Copyright © 2024 - Martin-D. Lacasse
14
-
15
- Disclaimers: This code is for educational purposes only and does not constitute financial advice.
16
-
17
- """
18
-
19
- import numpy as np
20
- from datetime import date
21
-
22
-
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
- ###############################################################################
32
- # Start of section where rates need to be actualized every year.
33
- ###############################################################################
34
- # Single [0] and married filing jointly [1].
35
-
36
- # These are 2025 current.
37
- taxBrackets_OBBBA = np.array(
38
- [
39
- [11925, 48475, 103350, 197300, 250525, 626350, 9999999],
40
- [23850, 96950, 206700, 394600, 501050, 751600, 9999999],
41
- ]
42
- )
43
-
44
- irmaaBrackets = np.array(
45
- [
46
- [0, 106000, 133000, 167000, 200000, 500000],
47
- [0, 212000, 266000, 334000, 400000, 750000],
48
- ]
49
- )
50
-
51
- # Index [0] stores the standard Medicare part B premium.
52
- # 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])
54
-
55
- # Make projection for pre-TCJA using 2017 to current year.
56
- # taxBrackets_2017 = np.array(
57
- # [ [9325, 37950, 91900, 191650, 416700, 418400, 9999999],
58
- # [18650, 75900, 153100, 233350, 416700, 470700, 9999999],
59
- # ])
60
- #
61
- # stdDeduction_2017 = [6350, 12700]
62
- #
63
- # For 2025, I used a 30.5% adjustment from 2017, rounded to closest 50.
64
- #
65
- # These are speculated.
66
- taxBrackets_preTCJA = np.array(
67
- [
68
- [12150, 49550, 119950, 250200, 544000, 546200, 9999999], # Single
69
- [24350, 99100, 199850, 304600, 543950, 614450, 9999999], # MFJ
70
- ]
71
- )
72
-
73
- # These are 2025 current.
74
- stdDeduction_OBBBA = np.array([15750, 31500]) # Single, MFJ
75
- # These are speculated (adjusted for inflation).
76
- stdDeduction_preTCJA = np.array([8300, 16600]) # Single, MFJ
77
-
78
- # These are current (adjusted for inflation) per individual.
79
- extra65Deduction = np.array([2000, 1600]) # Single, MFJ
80
-
81
- # Thresholds setting capital gains brackets 0%, 15%, 20% (adjusted for inflation).
82
- capGainRates = np.array(
83
- [
84
- [48350, 533400],
85
- [96700, 600050],
86
- ]
87
- )
88
-
89
- # Thresholds for net investment income tax (not adjusted for inflation).
90
- niitThreshold = np.array([200000, 250000])
91
- niitRate = 0.038
92
-
93
- # Thresholds for 65+ bonus for circumventing tax on social security.
94
- bonusThreshold = np.array([75000, 150000])
95
-
96
- ###############################################################################
97
- # End of section where rates need to be actualized every year.
98
- ###############################################################################
99
-
100
-
101
- def mediVals(yobs, horizons, gamma_n, Nn, Nq):
102
- """
103
- Return tuple (nm, L, C) of year index when Medicare starts and vectors L, and C
104
- defining end points of constant piecewise linear functions representing IRMAA fees.
105
- """
106
- thisyear = date.today().year
107
- assert Nq == len(irmaaFees), f"Inconsistent value of Nq: {Nq}."
108
- assert Nq == len(irmaaBrackets[0]), "Inconsistent IRMAA brackets array."
109
- Ni = len(yobs)
110
- # What index year will Medicare start? 65 - age.
111
- nm = 65 - (thisyear - yobs)
112
- nm = np.min(nm)
113
- # Has it already started?
114
- nm = max(0, nm)
115
- Nmed = Nn - nm
116
-
117
- L = np.zeros((Nmed, Nq-1))
118
- C = np.zeros((Nmed, Nq))
119
-
120
- # Year starts at offset nm in the plan.
121
- for nn in range(Nmed):
122
- imed = 0
123
- n = nm + nn
124
- if thisyear + n - yobs[0] >= 65 and n < horizons[0]:
125
- imed += 1
126
- if Ni == 2 and thisyear + n - yobs[1] >= 65 and n < horizons[1]:
127
- imed += 1
128
- if imed:
129
- status = 0 if Ni == 1 else 1 if n < horizons[0] and n < horizons[1] else 0
130
- L[nn] = gamma_n[n] * irmaaBrackets[status][1:]
131
- C[nn] = imed * gamma_n[n] * irmaaFees
132
- else:
133
- raise RuntimeError("mediVals: This should never happen.")
134
-
135
- return nm, L, C
136
-
137
-
138
- def capitalGainTaxRate(Ni, magi_n, gamma_n, nd, Nn):
139
- """
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.
143
- """
144
- status = Ni - 1
145
- cgRate_n = np.zeros(Nn)
146
-
147
- for n in range(Nn):
148
- if status and n == nd:
149
- status -= 1
150
-
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
155
-
156
- return cgRate_n
157
-
158
-
159
- def mediCosts(yobs, horizons, magi, prevmagi, gamma_n, Nn):
160
- """
161
- Compute Medicare costs directly.
162
- """
163
- thisyear = date.today().year
164
- Ni = len(yobs)
165
- costs = np.zeros(Nn)
166
- for n in range(Nn):
167
- status = 0 if Ni == 1 else 1 if n < horizons[0] and n < horizons[1] else 0
168
- for i in range(Ni):
169
- if thisyear + n - yobs[i] >= 65 and n < horizons[i]:
170
- # Start with the (inflation-adjusted) basic Medicare part B premium.
171
- costs[n] += gamma_n[n] * irmaaFees[0]
172
- if n < 2:
173
- mymagi = prevmagi[n]
174
- else:
175
- mymagi = magi[n - 2]
176
- for q in range(1, 6):
177
- if mymagi > gamma_n[n] * irmaaBrackets[status][q]:
178
- costs[n] += gamma_n[n] * irmaaFees[q]
179
-
180
- return costs
181
-
182
-
183
- def taxParams(yobs, i_d, n_d, N_n, gamma_n, MAGI_n, yOBBBA=2099):
184
- """
185
- Input is year of birth, index of shortest-lived individual,
186
- lifespan of shortest-lived individual, total number of years
187
- in the plan, and the year that preTCJA rates might come back.
188
-
189
- It returns 3 time series:
190
- 1) Standard deductions at year n (sigma_n).
191
- 2) Tax rate in year n (theta_tn)
192
- 3) Delta from top to bottom of tax brackets (Delta_tn)
193
- This is pure speculation on future values.
194
- Returned values are not indexed for inflation.
195
- """
196
- # Compute the deltas in-place between brackets, starting from the end.
197
- deltaBrackets_OBBBA = np.array(taxBrackets_OBBBA)
198
- deltaBrackets_preTCJA = np.array(taxBrackets_preTCJA)
199
- for t in range(6, 0, -1):
200
- for i in range(2):
201
- deltaBrackets_OBBBA[i, t] -= deltaBrackets_OBBBA[i, t - 1]
202
- deltaBrackets_preTCJA[i, t] -= deltaBrackets_preTCJA[i, t - 1]
203
-
204
- # Prepare the 3 arrays to return - use transpose for easy slicing.
205
- sigmaBar = np.zeros((N_n))
206
- Delta = np.zeros((N_n, 7))
207
- theta = np.zeros((N_n, 7))
208
-
209
- filingStatus = len(yobs) - 1
210
- souls = list(range(len(yobs)))
211
- thisyear = date.today().year
212
-
213
- for n in range(N_n):
214
- # First check if shortest-lived individual is still with us.
215
- if n == n_d:
216
- souls.remove(i_d)
217
- filingStatus -= 1
218
-
219
- if thisyear + n < yOBBBA:
220
- sigmaBar[n] = stdDeduction_OBBBA[filingStatus] * gamma_n[n]
221
- Delta[n, :] = deltaBrackets_OBBBA[filingStatus, :]
222
- else:
223
- sigmaBar[n] = stdDeduction_preTCJA[filingStatus] * gamma_n[n]
224
- Delta[n, :] = deltaBrackets_preTCJA[filingStatus, :]
225
-
226
- # Add 65+ additional exemption(s) and "bonus" phasing out.
227
- for i in souls:
228
- if thisyear + n - yobs[i] >= 65:
229
- sigmaBar[n] += extra65Deduction[filingStatus] * gamma_n[n]
230
- if thisyear + n <= 2028:
231
- sigmaBar[n] += 6000 * max(0, 1 - 0.06*max(0, MAGI_n[n] - bonusThreshold[filingStatus]))
232
-
233
- # Fill in future tax rates for year n.
234
- if thisyear + n < yOBBBA:
235
- theta[n, :] = rates_OBBBA[:]
236
- else:
237
- theta[n, :] = rates_preTCJA[:]
238
-
239
- Delta = Delta.transpose()
240
- theta = theta.transpose()
241
-
242
- # Return series unadjusted for inflation, except for sigmaBar, in STD order.
243
- return sigmaBar, theta, Delta
244
-
245
-
246
- def taxBrackets(N_i, n_d, N_n, yOBBBA=2099):
247
- """
248
- Return dictionary containing future tax brackets
249
- unadjusted for inflation for plotting.
250
- """
251
- if not (0 < N_i <= 2):
252
- raise ValueError(f"Cannot process {N_i} individuals.")
253
-
254
- n_d = min(n_d, N_n)
255
- status = N_i - 1
256
-
257
- # Number of years left in OBBBA from this year.
258
- thisyear = date.today().year
259
- if yOBBBA < thisyear:
260
- raise ValueError(f"Expiration year {yOBBBA} cannot be in the past.")
261
-
262
- ytc = yOBBBA - thisyear
263
-
264
- data = {}
265
- for t in range(len(taxBracketNames) - 1):
266
- array = np.zeros(N_n)
267
- for n in range(N_n):
268
- stat = status if n < n_d else 0
269
- array[n] = taxBrackets_OBBBA[stat][t] if n < ytc else taxBrackets_preTCJA[stat][t]
270
-
271
- data[taxBracketNames[t]] = array
272
-
273
- return data
274
-
275
-
276
- def computeNIIT(N_i, MAGI_n, I_n, Q_n, n_d, N_n):
277
- """
278
- Compute ACA tax on Dividends (Q) and Interests (I).
279
- For accounting for rent and/or trust income, one can easily add a column
280
- to the Wages and Contributions file and add yearly amount to Q_n + I_n below.
281
- """
282
- J_n = np.zeros(N_n)
283
- status = N_i - 1
284
-
285
- for n in range(N_n):
286
- if status and n == n_d:
287
- status -= 1
288
-
289
- Gmax = niitThreshold[status]
290
- if MAGI_n[n] > Gmax:
291
- J_n[n] = niitRate * min(MAGI_n[n] - Gmax, I_n[n] + Q_n[n])
292
-
293
- return J_n
294
-
295
-
296
- def rho_in(yobs, N_n):
297
- """
298
- Return Required Minimum Distribution fractions for each individual.
299
- This implementation does not support spouses with more than
300
- 10-year difference.
301
- It starts at age 73 until it goes to 75 in 2033.
302
- """
303
- # Notice that table starts at age 72.
304
- rmdTable = [
305
- 27.4,
306
- 26.5,
307
- 25.5,
308
- 24.6,
309
- 23.7,
310
- 22.9,
311
- 22.0,
312
- 21.1,
313
- 20.2,
314
- 19.4,
315
- 18.5,
316
- 17.7,
317
- 16.8,
318
- 16.0,
319
- 15.2,
320
- 14.4,
321
- 13.7,
322
- 12.9,
323
- 12.2,
324
- 11.5,
325
- 10.8,
326
- 10.1,
327
- 9.5,
328
- 8.9,
329
- 8.4,
330
- 7.8,
331
- 7.3,
332
- 6.8,
333
- 6.4,
334
- 6.0,
335
- 5.6,
336
- 5.2,
337
- 4.9,
338
- 4.6,
339
- ]
340
-
341
- N_i = len(yobs)
342
- if N_i == 2 and abs(yobs[0] - yobs[1]) > 10:
343
- raise RuntimeError("RMD: Unsupported age difference of more than 10 years.")
344
-
345
- rho = np.zeros((N_i, N_n))
346
- thisyear = date.today().year
347
- for i in range(N_i):
348
- agenow = thisyear - yobs[i]
349
- # Account for increase of RMD age between 2023 and 2032.
350
- yrmd = 70 if yobs[i] < 1949 else 72 if 1949 <= yobs[i] <= 1950 else 73 if 1951 <= yobs[i] <= 1959 else 75
351
- for n in range(N_n):
352
- yage = agenow + n
353
-
354
- if yage < yrmd:
355
- pass # rho[i][n] = 0
356
- else:
357
- rho[i][n] = 1.0 / rmdTable[yage - 72]
358
-
359
- return rho
@@ -1,29 +0,0 @@
1
- owlplanner/__init__.py,sha256=hJ2i4m2JpHPAKyQLjYOXpJzeEsgcTcKD-Vhm0AIjjWg,592
2
- owlplanner/abcapi.py,sha256=Eo2Z2doVsaao3HPZ0IICwqjhcgeerxrjSTSt-7f-BFY,6849
3
- owlplanner/config.py,sha256=HzPLp9queRw_q73QMXlRTEkdif7XPBAAXG8g3eBXNLM,12527
4
- owlplanner/debts.py,sha256=H3VrRZCjjpP4tsjiy2YRrJawypxE_8W2SSCHMTuUOVE,8628
5
- owlplanner/fixedassets.py,sha256=BbnLEAMkThWx7DnQ8xFV7xtwHpIhaC1b9UkUoMNeHgM,7742
6
- owlplanner/mylogging.py,sha256=OVGeDFO7LIZG91R6HMpZBzjno-B8PH8Fo00Jw2Pdgqw,2558
7
- owlplanner/plan.py,sha256=x7oiw6dgMPPnLZZSOXJ1Wx1fT_SW6_MBfYsDACPlpEA,129482
8
- owlplanner/progress.py,sha256=J1iaYUC6OqHDew-KdoTUWHgAagBV8Gvptlghts-WFx8,1843
9
- owlplanner/rates.py,sha256=aBFYCD6U3St--SvL6nG46wfKIA1YAey4-LHKza-jQUs,14115
10
- owlplanner/socialsecurity.py,sha256=0QizQjO-hzJQzQstqqkteSX8qzHXUiNOps4xzcz2GoA,6715
11
- owlplanner/tax2025.py,sha256=qUKhVIN1KJdbiVGPWtp0ftbV5sMuP3aK_WwJZBgLDhE,11418
12
- owlplanner/tax2026.py,sha256=IKILbI_waAnzSXDZmcbdB-R8tq2VjEKFmQNQ_La9TAM,12252
13
- owlplanner/timelists.py,sha256=0n_sDwVWaBa6JErz2mFEWz89s2QpC4qY8hvkkC2EzqU,7783
14
- owlplanner/utils.py,sha256=wnniCgvMl-0W68W6mjeCBWFlt1tuaGc03pt5KPXRCIs,3266
15
- owlplanner/version.py,sha256=iVYPFVMpNOEb6-f3knyQzB17C4qKOmp_ApxaCpjAWCw,28
16
- owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- owlplanner/data/awi.csv,sha256=w9YwPEqhxqo1Czumh3ihLXLPtk8kJQgqWKClMysOq-c,1625
18
- owlplanner/data/bendpoints.csv,sha256=h0a7_XqTuyHWe5ZlS3mjIcAWqOMG_93GoiETS-1xxUY,746
19
- owlplanner/data/newawi.csv,sha256=w9YwPEqhxqo1Czumh3ihLXLPtk8kJQgqWKClMysOq-c,1625
20
- owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
21
- owlplanner/plotting/__init__.py,sha256=uhxqtUi0OI-QWNOO2LkXgQViW_9yM3rYb-204Wit974,250
22
- owlplanner/plotting/base.py,sha256=UimGKpMTV-dVm3BX5Apr_Ltorc7dlDLCRPRQ3RF_v7c,2578
23
- owlplanner/plotting/factory.py,sha256=EDopIAPQr9zHRgemObko18FlCeRNhNCoLNNFAOq-X6s,1030
24
- owlplanner/plotting/matplotlib_backend.py,sha256=AOEkapD94U5hGNoS0EdbRoe8mgdMHH4oOvkXADZS914,17957
25
- owlplanner/plotting/plotly_backend.py,sha256=lv4AzrsHSOWzRgIFcaSYF5FfbkyGouX8dDpumLFXQdw,33151
26
- owlplanner-2025.12.20.dist-info/METADATA,sha256=ZNqby1H7IzvVeXv4NvYBp_WlybFxb-Qr_rwSr0NptkY,46190
27
- owlplanner-2025.12.20.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
28
- owlplanner-2025.12.20.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
29
- owlplanner-2025.12.20.dist-info/RECORD,,