owlplanner 2025.7.1__py3-none-any.whl → 2025.8.1__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/abcapi.py +1 -1
- owlplanner/config.py +3 -3
- owlplanner/mylogging.py +1 -1
- owlplanner/plan.py +226 -158
- owlplanner/progress.py +1 -1
- owlplanner/rates.py +1 -23
- owlplanner/tax2025.py +82 -36
- owlplanner/timelists.py +1 -1
- owlplanner/utils.py +1 -1
- owlplanner/version.py +1 -1
- {owlplanner-2025.7.1.dist-info → owlplanner-2025.8.1.dist-info}/METADATA +1 -1
- owlplanner-2025.8.1.dist-info/RECORD +22 -0
- owlplanner-2025.7.1.dist-info/RECORD +0 -22
- {owlplanner-2025.7.1.dist-info → owlplanner-2025.8.1.dist-info}/WHEEL +0 -0
- {owlplanner-2025.7.1.dist-info → owlplanner-2025.8.1.dist-info}/licenses/LICENSE +0 -0
owlplanner/abcapi.py
CHANGED
|
@@ -18,7 +18,7 @@ A for matrix, B for bounds, C for constraints. Thus the name ABCAPI.
|
|
|
18
18
|
|
|
19
19
|
Copyright © 2024 - Martin-D. Lacasse
|
|
20
20
|
|
|
21
|
-
Disclaimers: This code is for
|
|
21
|
+
Disclaimers: This code is for educational purposes only and does not constitute financial advice.
|
|
22
22
|
|
|
23
23
|
"""
|
|
24
24
|
|
owlplanner/config.py
CHANGED
|
@@ -6,7 +6,7 @@ This file contains utility functions to save case parameters.
|
|
|
6
6
|
|
|
7
7
|
Copyright © 2024 - Martin-D. Lacasse
|
|
8
8
|
|
|
9
|
-
Disclaimers: This code is for
|
|
9
|
+
Disclaimers: This code is for educational purposes only and does not constitute financial advice.
|
|
10
10
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
@@ -66,7 +66,7 @@ def saveConfig(myplan, file, mylog):
|
|
|
66
66
|
diconf["Rates Selection"] = {
|
|
67
67
|
"Heirs rate on tax-deferred estate": float(100 * myplan.nu),
|
|
68
68
|
"Dividend rate": float(100 * myplan.mu),
|
|
69
|
-
"
|
|
69
|
+
"OBBBA expiration year": myplan.yOBBBA,
|
|
70
70
|
"Method": myplan.rateMethod,
|
|
71
71
|
}
|
|
72
72
|
if myplan.rateMethod in ["user", "stochastic"]:
|
|
@@ -228,7 +228,7 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
|
|
|
228
228
|
# Rates Selection.
|
|
229
229
|
p.setDividendRate(float(diconf["Rates Selection"].get("Dividend rate", 1.8))) # Fix for mod.
|
|
230
230
|
p.setHeirsTaxRate(float(diconf["Rates Selection"]["Heirs rate on tax-deferred estate"]))
|
|
231
|
-
p.
|
|
231
|
+
p.yOBBBA = int(diconf["Rates Selection"].get("OBBBA expiration year", 2032))
|
|
232
232
|
|
|
233
233
|
frm = None
|
|
234
234
|
to = None
|
owlplanner/mylogging.py
CHANGED
|
@@ -6,7 +6,7 @@ This file contains routines for handling error messages.
|
|
|
6
6
|
|
|
7
7
|
Copyright © 2024 - Martin-D. Lacasse
|
|
8
8
|
|
|
9
|
-
Disclaimers: This code is for
|
|
9
|
+
Disclaimers: This code is for educational purposes only and does not constitute financial advice.
|
|
10
10
|
|
|
11
11
|
"""
|
|
12
12
|
|
owlplanner/plan.py
CHANGED
|
@@ -10,7 +10,7 @@ mathematical model and a description of all variables and parameters.
|
|
|
10
10
|
|
|
11
11
|
Copyright © 2024 - Martin-D. Lacasse
|
|
12
12
|
|
|
13
|
-
Disclaimers: This code is for
|
|
13
|
+
Disclaimers: This code is for educational purposes only and does not constitute financial advice.
|
|
14
14
|
|
|
15
15
|
"""
|
|
16
16
|
|
|
@@ -226,12 +226,13 @@ class Plan(object):
|
|
|
226
226
|
self._name = name
|
|
227
227
|
self.setLogstreams(verbose, logstreams)
|
|
228
228
|
|
|
229
|
-
# 7 tax brackets, 3 types of accounts, 4 classes of assets.
|
|
229
|
+
# 7 tax brackets, 6 Medicare brackets, 3 types of accounts, 4 classes of assets.
|
|
230
230
|
self.N_t = 7
|
|
231
|
+
self.N_q = 6
|
|
231
232
|
self.N_j = 3
|
|
232
233
|
self.N_k = 4
|
|
233
234
|
# 2 binary variables.
|
|
234
|
-
self.
|
|
235
|
+
self.N_zx = 2
|
|
235
236
|
|
|
236
237
|
# Default interpolation parameters for allocation ratios.
|
|
237
238
|
self.interpMethod = "linear"
|
|
@@ -258,8 +259,9 @@ class Plan(object):
|
|
|
258
259
|
raise ValueError("Name for each individual must be provided.")
|
|
259
260
|
|
|
260
261
|
self.filingStatus = ("single", "married")[self.N_i - 1]
|
|
261
|
-
|
|
262
|
-
|
|
262
|
+
|
|
263
|
+
# Default year OBBBA speculated to be expired and replaced by pre-TCJA rates.
|
|
264
|
+
self.yOBBBA = 2032
|
|
263
265
|
self.inames = inames
|
|
264
266
|
self.yobs = np.array(yobs, dtype=np.int32)
|
|
265
267
|
self.expectancy = np.array(expectancy, dtype=np.int32)
|
|
@@ -297,20 +299,22 @@ class Plan(object):
|
|
|
297
299
|
self.zeta_in = np.zeros((self.N_i, self.N_n))
|
|
298
300
|
self.pensionAmounts = np.zeros(self.N_i)
|
|
299
301
|
self.pensionAges = 65 * np.ones(self.N_i, dtype=np.int32)
|
|
300
|
-
self.pensionIsIndexed = [False
|
|
302
|
+
self.pensionIsIndexed = [False] * self.N_i
|
|
301
303
|
self.ssecAmounts = np.zeros(self.N_i)
|
|
302
304
|
self.ssecAges = 67 * np.ones(self.N_i, dtype=np.int32)
|
|
303
305
|
|
|
304
306
|
# Parameters from timeLists initialized to zero.
|
|
305
307
|
self.omega_in = np.zeros((self.N_i, self.N_n))
|
|
306
308
|
self.Lambda_in = np.zeros((self.N_i, self.N_n))
|
|
309
|
+
# Go back 5 years for maturation rules on IRA and Roth.
|
|
307
310
|
self.myRothX_in = np.zeros((self.N_i, self.N_n + 5))
|
|
308
311
|
self.kappa_ijn = np.zeros((self.N_i, self.N_j, self.N_n + 5))
|
|
309
312
|
|
|
310
|
-
# Previous
|
|
313
|
+
# Previous 2 years of MAGI needed for Medicare.
|
|
311
314
|
self.prevMAGI = np.zeros((2))
|
|
315
|
+
self.MAGI_n = np.zeros(self.N_n)
|
|
312
316
|
|
|
313
|
-
# Init
|
|
317
|
+
# Init current balances to none.
|
|
314
318
|
self.beta_ij = None
|
|
315
319
|
self.startDate = None
|
|
316
320
|
|
|
@@ -327,8 +331,6 @@ class Plan(object):
|
|
|
327
331
|
# Prepare RMD time series.
|
|
328
332
|
self.rho_in = tx.rho_in(self.yobs, self.N_n)
|
|
329
333
|
|
|
330
|
-
self._buildOffsetMap()
|
|
331
|
-
|
|
332
334
|
# Initialize guardrails to ensure proper configuration.
|
|
333
335
|
self._adjustedParameters = False
|
|
334
336
|
self.timeListsFileName = "None"
|
|
@@ -340,7 +342,7 @@ class Plan(object):
|
|
|
340
342
|
self.ARCoord = None
|
|
341
343
|
self.objective = "unknown"
|
|
342
344
|
|
|
343
|
-
# Placeholders to check if properly configured.
|
|
345
|
+
# Placeholders values used to check if properly configured.
|
|
344
346
|
self.xi_n = None
|
|
345
347
|
self.alpha_ijkn = None
|
|
346
348
|
|
|
@@ -368,7 +370,7 @@ class Plan(object):
|
|
|
368
370
|
"""
|
|
369
371
|
Set the date when the plan starts in the current year.
|
|
370
372
|
This is mostly for reproducibility purposes and back projecting known balances to Jan 1st.
|
|
371
|
-
String format of mydate is '
|
|
373
|
+
String format of mydate is 'MM/DD', 'MM-DD', 'YYYY-MM-DD', or 'YYYY/MM/DD'. Year is ignored.
|
|
372
374
|
"""
|
|
373
375
|
import calendar
|
|
374
376
|
|
|
@@ -397,7 +399,7 @@ class Plan(object):
|
|
|
397
399
|
|
|
398
400
|
return None
|
|
399
401
|
|
|
400
|
-
def
|
|
402
|
+
def _checkValueType(self, value):
|
|
401
403
|
"""
|
|
402
404
|
Short utility function to parse and check arguments for plotting.
|
|
403
405
|
"""
|
|
@@ -450,7 +452,7 @@ class Plan(object):
|
|
|
450
452
|
Set plots between nominal values or today's $.
|
|
451
453
|
"""
|
|
452
454
|
|
|
453
|
-
self.defaultPlots = self.
|
|
455
|
+
self.defaultPlots = self._checkValueType(value)
|
|
454
456
|
self.mylog.vprint(f"Setting plots default value to {value}.")
|
|
455
457
|
|
|
456
458
|
def setPlotBackend(self, backend: str):
|
|
@@ -463,25 +465,26 @@ class Plan(object):
|
|
|
463
465
|
|
|
464
466
|
if backend != self._plotterName:
|
|
465
467
|
self._plotter = PlotFactory.createBackend(backend)
|
|
468
|
+
self._plotterName = backend
|
|
466
469
|
self.mylog.vprint(f"Setting plotting backend to {backend}.")
|
|
467
470
|
|
|
468
471
|
def setDividendRate(self, mu):
|
|
469
472
|
"""
|
|
470
473
|
Set dividend tax rate. Rate is in percent. Default 1.8%.
|
|
471
474
|
"""
|
|
472
|
-
if not (0 <= mu <=
|
|
473
|
-
raise ValueError("Rate must be between 0 and
|
|
475
|
+
if not (0 <= mu <= 5):
|
|
476
|
+
raise ValueError("Rate must be between 0 and 5.")
|
|
474
477
|
mu /= 100
|
|
475
478
|
self.mylog.vprint(f"Dividend tax rate set to {u.pc(mu, f=0)}.")
|
|
476
479
|
self.mu = mu
|
|
477
480
|
self.caseStatus = "modified"
|
|
478
481
|
|
|
479
|
-
def
|
|
482
|
+
def setExpirationYearOBBBA(self, yOBBBA):
|
|
480
483
|
"""
|
|
481
|
-
Set year at which
|
|
484
|
+
Set year at which OBBBA is speculated to expire and rates go back to something like pre-TCJA.
|
|
482
485
|
"""
|
|
483
|
-
self.mylog.vprint(f"Setting
|
|
484
|
-
self.
|
|
486
|
+
self.mylog.vprint(f"Setting OBBBA expiration year to {yOBBBA}.")
|
|
487
|
+
self.yOBBBA = yOBBBA
|
|
485
488
|
self.caseStatus = "modified"
|
|
486
489
|
self._adjustedParameters = False
|
|
487
490
|
|
|
@@ -516,7 +519,7 @@ class Plan(object):
|
|
|
516
519
|
self.nu = nu
|
|
517
520
|
self.caseStatus = "modified"
|
|
518
521
|
|
|
519
|
-
def setPension(self, amounts, ages, indexed=
|
|
522
|
+
def setPension(self, amounts, ages, indexed=None, units="k"):
|
|
520
523
|
"""
|
|
521
524
|
Set value of pension for each individual and commencement age.
|
|
522
525
|
Units are in $k, unless specified otherwise: 'k', 'M', or '1'.
|
|
@@ -525,8 +528,8 @@ class Plan(object):
|
|
|
525
528
|
raise ValueError(f"Amounts must have {self.N_i} entries.")
|
|
526
529
|
if len(ages) != self.N_i:
|
|
527
530
|
raise ValueError(f"Ages must have {self.N_i} entries.")
|
|
528
|
-
if
|
|
529
|
-
|
|
531
|
+
if indexed is None:
|
|
532
|
+
indexed = [False] * self.N_i
|
|
530
533
|
|
|
531
534
|
fac = u.getUnits(units)
|
|
532
535
|
amounts = u.rescale(amounts, fac)
|
|
@@ -539,7 +542,7 @@ class Plan(object):
|
|
|
539
542
|
self.pi_in = np.zeros((self.N_i, self.N_n))
|
|
540
543
|
for i in range(self.N_i):
|
|
541
544
|
if amounts[i] != 0:
|
|
542
|
-
ns = max(0,
|
|
545
|
+
ns = max(0, ages[i] - thisyear + self.yobs[i])
|
|
543
546
|
nd = self.horizons[i]
|
|
544
547
|
self.pi_in[i, ns:nd] = amounts[i]
|
|
545
548
|
|
|
@@ -570,13 +573,13 @@ class Plan(object):
|
|
|
570
573
|
thisyear = date.today().year
|
|
571
574
|
self.zeta_in = np.zeros((self.N_i, self.N_n))
|
|
572
575
|
for i in range(self.N_i):
|
|
573
|
-
ns = max(0,
|
|
576
|
+
ns = max(0, ages[i] - thisyear + self.yobs[i])
|
|
574
577
|
nd = self.horizons[i]
|
|
575
578
|
self.zeta_in[i, ns:nd] = amounts[i]
|
|
576
579
|
|
|
577
580
|
if self.N_i == 2:
|
|
578
|
-
# Approximate calculation for spousal benefit (only valid at FRA).
|
|
579
|
-
self.zeta_in[self.i_s, self.n_d :] = max(amounts[self.i_s], amounts[self.i_d])
|
|
581
|
+
# Approximate calculation for spousal benefit (only valid at FRA, and if of similar ages).
|
|
582
|
+
self.zeta_in[self.i_s, self.n_d :] = max(amounts[self.i_s], 0.5*amounts[self.i_d])
|
|
580
583
|
|
|
581
584
|
self.ssecAmounts = np.array(amounts)
|
|
582
585
|
self.ssecAges = np.array(ages, dtype=np.int32)
|
|
@@ -658,36 +661,6 @@ class Plan(object):
|
|
|
658
661
|
corr=self.rateCorr,
|
|
659
662
|
)
|
|
660
663
|
|
|
661
|
-
def value(self, amount, year):
|
|
662
|
-
"""
|
|
663
|
-
Return value of amount deflated or inflated at the beginning
|
|
664
|
-
of the year specified.
|
|
665
|
-
If year is in the past, value is made at the beginning of this year.
|
|
666
|
-
If year is in the future, amount is adjusted from a reference time
|
|
667
|
-
aligned with the beginning of the plan to the beginning of the
|
|
668
|
-
year specified.
|
|
669
|
-
"""
|
|
670
|
-
thisyear = date.today().year
|
|
671
|
-
if year <= thisyear:
|
|
672
|
-
return rates.historicalValue(amount, year)
|
|
673
|
-
else:
|
|
674
|
-
return self.forwardValue(amount, year)
|
|
675
|
-
|
|
676
|
-
def forwardValue(self, amount, year):
|
|
677
|
-
"""
|
|
678
|
-
Return the value of amount inflated from beginning of the plan
|
|
679
|
-
to the beginning of the year provided.
|
|
680
|
-
"""
|
|
681
|
-
if self.rateMethod is None:
|
|
682
|
-
raise RuntimeError("A rate method needs to be first selected using setRates(...).")
|
|
683
|
-
|
|
684
|
-
thisyear = date.today().year
|
|
685
|
-
if year <= thisyear:
|
|
686
|
-
raise RuntimeError("Internal error in forwardValue().")
|
|
687
|
-
span = year - thisyear
|
|
688
|
-
|
|
689
|
-
return amount * self.gamma_n[span]
|
|
690
|
-
|
|
691
664
|
def setAccountBalances(self, *, taxable, taxDeferred, taxFree, startDate=None, units="k"):
|
|
692
665
|
"""
|
|
693
666
|
Three lists containing the balance of all assets in each category for
|
|
@@ -977,7 +950,6 @@ class Plan(object):
|
|
|
977
950
|
for i, iname in enumerate(self.inames):
|
|
978
951
|
h = self.horizons[i]
|
|
979
952
|
df = pd.DataFrame(0, index=np.arange(0, h+5), columns=cols)
|
|
980
|
-
# df["year"] = self.year_n[:h]
|
|
981
953
|
df["year"] = np.arange(self.year_n[0] - 5, self.year_n[h-1]+1)
|
|
982
954
|
self.timeLists[iname] = df
|
|
983
955
|
|
|
@@ -1015,62 +987,70 @@ class Plan(object):
|
|
|
1015
987
|
|
|
1016
988
|
return dat
|
|
1017
989
|
|
|
1018
|
-
def _adjustParameters(self):
|
|
990
|
+
def _adjustParameters(self, gamma_n, MAGI_n):
|
|
1019
991
|
"""
|
|
1020
|
-
Adjust parameters that follow inflation.
|
|
992
|
+
Adjust parameters that follow inflation or depend on MAGI.
|
|
993
|
+
Separate variables depending on MAGI (exemptions now depends on MAGI).
|
|
1021
994
|
"""
|
|
1022
995
|
if self.rateMethod is None:
|
|
1023
996
|
raise RuntimeError("A rate method needs to be first selected using setRates(...).")
|
|
1024
997
|
|
|
998
|
+
self.sigmaBar_n, self.theta_tn, self.Delta_tn = tx.taxParams(self.yobs, self.i_d, self.n_d,
|
|
999
|
+
self.N_n, gamma_n,
|
|
1000
|
+
MAGI_n, self.yOBBBA)
|
|
1001
|
+
|
|
1025
1002
|
if not self._adjustedParameters:
|
|
1026
1003
|
self.mylog.vprint("Adjusting parameters for inflation.")
|
|
1027
|
-
self.
|
|
1028
|
-
|
|
1029
|
-
self.
|
|
1030
|
-
self.DeltaBar_tn = self.Delta_tn * self.gamma_n[:-1]
|
|
1031
|
-
self.zetaBar_in = self.zeta_in * self.gamma_n[:-1]
|
|
1032
|
-
self.xiBar_n = self.xi_n * self.gamma_n[:-1]
|
|
1004
|
+
self.DeltaBar_tn = self.Delta_tn * gamma_n[:-1]
|
|
1005
|
+
self.zetaBar_in = self.zeta_in * gamma_n[:-1]
|
|
1006
|
+
self.xiBar_n = self.xi_n * gamma_n[:-1]
|
|
1033
1007
|
self.piBar_in = np.array(self.pi_in)
|
|
1034
1008
|
for i in range(self.N_i):
|
|
1035
1009
|
if self.pensionIsIndexed[i]:
|
|
1036
|
-
self.piBar_in[i] *=
|
|
1010
|
+
self.piBar_in[i] *= gamma_n[:-1]
|
|
1011
|
+
|
|
1012
|
+
self.nm, self.L_nq, self.C_nq = tx.mediVals(self.yobs, self.horizons, gamma_n, self.N_n, self.N_q)
|
|
1037
1013
|
|
|
1038
1014
|
self._adjustedParameters = True
|
|
1039
1015
|
|
|
1040
|
-
return None
|
|
1016
|
+
# return None
|
|
1041
1017
|
|
|
1042
|
-
def _buildOffsetMap(self):
|
|
1018
|
+
def _buildOffsetMap(self, options):
|
|
1043
1019
|
"""
|
|
1044
1020
|
Utility function to map variables to a block vector.
|
|
1045
1021
|
Refer to companion document for explanations.
|
|
1022
|
+
All binary variables must be lumped at the end of the vector.
|
|
1046
1023
|
"""
|
|
1024
|
+
medi = options.get("optimizeMedicare", False)
|
|
1025
|
+
|
|
1047
1026
|
# Stack all variables in a single block vector with all binary variables at the end.
|
|
1048
1027
|
C = {}
|
|
1049
1028
|
C["b"] = 0
|
|
1050
1029
|
C["d"] = _qC(C["b"], self.N_i, self.N_j, self.N_n + 1)
|
|
1051
1030
|
C["e"] = _qC(C["d"], self.N_i, self.N_n)
|
|
1052
|
-
C["
|
|
1053
|
-
C["g"] = _qC(C["
|
|
1054
|
-
C["
|
|
1031
|
+
C["f"] = _qC(C["e"], self.N_n)
|
|
1032
|
+
C["g"] = _qC(C["f"], self.N_t, self.N_n)
|
|
1033
|
+
C["m"] = _qC(C["g"], self.N_n)
|
|
1034
|
+
C["s"] = _qC(C["m"], self.N_n)
|
|
1055
1035
|
C["w"] = _qC(C["s"], self.N_n)
|
|
1056
1036
|
C["x"] = _qC(C["w"], self.N_i, self.N_j, self.N_n)
|
|
1057
|
-
C["
|
|
1058
|
-
|
|
1059
|
-
self.
|
|
1060
|
-
|
|
1061
|
-
# # self.nbins = 0
|
|
1037
|
+
C["zx"] = _qC(C["x"], self.N_i, self.N_n)
|
|
1038
|
+
C["zm"] = _qC(C["zx"], self.N_i, self.N_n, self.N_zx)
|
|
1039
|
+
self.nvars = _qC(C["zm"], self.N_n - self.nm, self.N_q - 1) if medi else C["zm"]
|
|
1040
|
+
self.nbins = self.nvars - C["zx"]
|
|
1062
1041
|
|
|
1063
1042
|
self.C = C
|
|
1064
1043
|
self.mylog.vprint(
|
|
1065
1044
|
f"Problem has {len(C)} distinct series, {self.nvars} decision variables (including {self.nbins} binary).")
|
|
1066
1045
|
|
|
1067
|
-
return None
|
|
1068
|
-
|
|
1069
1046
|
def _buildConstraints(self, objective, options):
|
|
1070
1047
|
"""
|
|
1071
1048
|
Utility function that builds constraint matrix and vectors.
|
|
1072
1049
|
Refactored for clarity and maintainability.
|
|
1073
1050
|
"""
|
|
1051
|
+
# Ensure parameters are adjusted for inflation and MAGI.
|
|
1052
|
+
self._adjustParameters(self.gamma_n, self.MAGI_n)
|
|
1053
|
+
|
|
1074
1054
|
self.A = abc.ConstraintMatrix(self.nvars)
|
|
1075
1055
|
self.B = abc.Bounds(self.nvars, self.nbins)
|
|
1076
1056
|
|
|
@@ -1089,11 +1069,11 @@ class Plan(object):
|
|
|
1089
1069
|
self._add_net_cash_flow()
|
|
1090
1070
|
self._add_income_profile()
|
|
1091
1071
|
self._add_taxable_income()
|
|
1092
|
-
self.
|
|
1072
|
+
self._configure_Medicare_binary_variables(options)
|
|
1073
|
+
self._add_Medicare_costs(options)
|
|
1074
|
+
self._configure_exclusion_binary_variables(options)
|
|
1093
1075
|
self._build_objective_vector(objective)
|
|
1094
1076
|
|
|
1095
|
-
return None
|
|
1096
|
-
|
|
1097
1077
|
def _add_rmd_inequalities(self):
|
|
1098
1078
|
for i in range(self.N_i):
|
|
1099
1079
|
if self.beta_ij[i, 1] > 0:
|
|
@@ -1107,7 +1087,7 @@ class Plan(object):
|
|
|
1107
1087
|
def _add_tax_bracket_bounds(self):
|
|
1108
1088
|
for t in range(self.N_t):
|
|
1109
1089
|
for n in range(self.N_n):
|
|
1110
|
-
self.B.setRange(_q2(self.C["
|
|
1090
|
+
self.B.setRange(_q2(self.C["f"], t, n, self.N_t, self.N_n), 0, self.DeltaBar_tn[t, n])
|
|
1111
1091
|
|
|
1112
1092
|
def _add_standard_exemption_bounds(self):
|
|
1113
1093
|
for n in range(self.N_n):
|
|
@@ -1313,6 +1293,7 @@ class Plan(object):
|
|
|
1313
1293
|
rhs = -self.M_n[n] - self.J_n[n]
|
|
1314
1294
|
row = self.A.newRow({_q1(self.C["g"], n, self.N_n): 1})
|
|
1315
1295
|
row.addElem(_q1(self.C["s"], n, self.N_n), 1)
|
|
1296
|
+
row.addElem(_q1(self.C["m"], n, self.N_n), 1)
|
|
1316
1297
|
for i in range(self.N_i):
|
|
1317
1298
|
fac = self.psi_n[n] * self.alpha_ijkn[i, 0, 0, n]
|
|
1318
1299
|
rhs += (
|
|
@@ -1330,7 +1311,7 @@ class Plan(object):
|
|
|
1330
1311
|
row.addElem(_q2(self.C["d"], i, n, self.N_i, self.N_n), fac * self.mu)
|
|
1331
1312
|
|
|
1332
1313
|
for t in range(self.N_t):
|
|
1333
|
-
row.addElem(_q2(self.C["
|
|
1314
|
+
row.addElem(_q2(self.C["f"], t, n, self.N_t, self.N_n), self.theta_tn[t, n])
|
|
1334
1315
|
|
|
1335
1316
|
self.A.addRow(row, rhs, rhs)
|
|
1336
1317
|
|
|
@@ -1360,10 +1341,13 @@ class Plan(object):
|
|
|
1360
1341
|
row.addElem(_q3(self.C["w"], i, 0, n, self.N_i, self.N_j, self.N_n), fak)
|
|
1361
1342
|
row.addElem(_q2(self.C["d"], i, n, self.N_i, self.N_n), -fak)
|
|
1362
1343
|
for t in range(self.N_t):
|
|
1363
|
-
row.addElem(_q2(self.C["
|
|
1344
|
+
row.addElem(_q2(self.C["f"], t, n, self.N_t, self.N_n), 1)
|
|
1364
1345
|
self.A.addRow(row, rhs, rhs)
|
|
1365
1346
|
|
|
1366
|
-
def
|
|
1347
|
+
def _configure_exclusion_binary_variables(self, options):
|
|
1348
|
+
if not options.get("xorConstraints", True):
|
|
1349
|
+
return
|
|
1350
|
+
|
|
1367
1351
|
bigM = options.get("bigM", 5e6)
|
|
1368
1352
|
if not isinstance(bigM, (int, float)):
|
|
1369
1353
|
raise ValueError(f"bigM {bigM} is not a number.")
|
|
@@ -1371,14 +1355,14 @@ class Plan(object):
|
|
|
1371
1355
|
for i in range(self.N_i):
|
|
1372
1356
|
for n in range(self.horizons[i]):
|
|
1373
1357
|
self.A.addNewRow(
|
|
1374
|
-
{_q3(self.C["
|
|
1358
|
+
{_q3(self.C["zx"], i, n, 0, self.N_i, self.N_n, self.N_zx): bigM,
|
|
1375
1359
|
_q1(self.C["s"], n, self.N_n): -1},
|
|
1376
1360
|
0,
|
|
1377
1361
|
bigM,
|
|
1378
1362
|
)
|
|
1379
1363
|
self.A.addNewRow(
|
|
1380
1364
|
{
|
|
1381
|
-
_q3(self.C["
|
|
1365
|
+
_q3(self.C["zx"], i, n, 0, self.N_i, self.N_n, self.N_zx): bigM,
|
|
1382
1366
|
_q3(self.C["w"], i, 0, n, self.N_i, self.N_j, self.N_n): 1,
|
|
1383
1367
|
_q3(self.C["w"], i, 2, n, self.N_i, self.N_j, self.N_n): 1,
|
|
1384
1368
|
},
|
|
@@ -1386,20 +1370,94 @@ class Plan(object):
|
|
|
1386
1370
|
bigM,
|
|
1387
1371
|
)
|
|
1388
1372
|
self.A.addNewRow(
|
|
1389
|
-
{_q3(self.C["
|
|
1373
|
+
{_q3(self.C["zx"], i, n, 1, self.N_i, self.N_n, self.N_zx): bigM,
|
|
1390
1374
|
_q2(self.C["x"], i, n, self.N_i, self.N_n): -1},
|
|
1391
1375
|
0,
|
|
1392
1376
|
bigM,
|
|
1393
1377
|
)
|
|
1394
1378
|
self.A.addNewRow(
|
|
1395
|
-
{_q3(self.C["
|
|
1379
|
+
{_q3(self.C["zx"], i, n, 1, self.N_i, self.N_n, self.N_zx): bigM,
|
|
1396
1380
|
_q3(self.C["w"], i, 2, n, self.N_i, self.N_j, self.N_n): 1},
|
|
1397
1381
|
0,
|
|
1398
1382
|
bigM,
|
|
1399
1383
|
)
|
|
1400
1384
|
for n in range(self.horizons[i], self.N_n):
|
|
1401
|
-
self.B.setRange(_q3(self.C["
|
|
1402
|
-
self.B.setRange(_q3(self.C["
|
|
1385
|
+
self.B.setRange(_q3(self.C["zx"], i, n, 0, self.N_i, self.N_n, self.N_zx), 0, 0)
|
|
1386
|
+
self.B.setRange(_q3(self.C["zx"], i, n, 1, self.N_i, self.N_n, self.N_zx), 0, 0)
|
|
1387
|
+
|
|
1388
|
+
def _configure_Medicare_binary_variables(self, options):
|
|
1389
|
+
if not options.get("optimizeMedicare", False):
|
|
1390
|
+
return
|
|
1391
|
+
|
|
1392
|
+
bigM = options.get("bigM", 5e6)
|
|
1393
|
+
if not isinstance(bigM, (int, float)):
|
|
1394
|
+
raise ValueError(f"bigM {bigM} is not a number.")
|
|
1395
|
+
|
|
1396
|
+
Nmed = self.N_n - self.nm
|
|
1397
|
+
offset = 0
|
|
1398
|
+
if self.nm < 2:
|
|
1399
|
+
offset = 2 - self.nm
|
|
1400
|
+
for nn in range(offset):
|
|
1401
|
+
n = self.nm + nn
|
|
1402
|
+
for q in range(self.N_q - 1):
|
|
1403
|
+
self.A.addNewRow({_q2(self.C["zm"], nn, q, Nmed, self.N_q - 1): bigM},
|
|
1404
|
+
-np.inf, bigM - self.L_nq[nn, q] + self.prevMAGI[n])
|
|
1405
|
+
self.A.addNewRow({_q2(self.C["zm"], nn, q, Nmed, self.N_q - 1): -bigM},
|
|
1406
|
+
-np.inf, self.L_nq[nn, q] - self.prevMAGI[n])
|
|
1407
|
+
|
|
1408
|
+
for nn in range(offset, Nmed):
|
|
1409
|
+
n2 = self.nm + nn - 2 # n - 2
|
|
1410
|
+
for q in range(self.N_q - 1):
|
|
1411
|
+
rhs1 = bigM - self.L_nq[nn, q]
|
|
1412
|
+
rhs2 = self.L_nq[nn, q]
|
|
1413
|
+
row1 = self.A.newRow()
|
|
1414
|
+
row2 = self.A.newRow()
|
|
1415
|
+
|
|
1416
|
+
row1.addElem(_q2(self.C["zm"], nn, q, Nmed, self.N_q - 1), +bigM)
|
|
1417
|
+
row2.addElem(_q2(self.C["zm"], nn, q, Nmed, self.N_q - 1), -bigM)
|
|
1418
|
+
for i in range(self.N_i):
|
|
1419
|
+
row1.addElem(_q3(self.C["w"], i, 1, n2, self.N_i, self.N_j, self.N_n), -1)
|
|
1420
|
+
row2.addElem(_q3(self.C["w"], i, 1, n2, self.N_i, self.N_j, self.N_n), +1)
|
|
1421
|
+
|
|
1422
|
+
row1.addElem(_q2(self.C["x"], i, n2, self.N_i, self.N_n), -1)
|
|
1423
|
+
row2.addElem(_q2(self.C["x"], i, n2, self.N_i, self.N_n), +1)
|
|
1424
|
+
|
|
1425
|
+
afac = (self.mu*self.alpha_ijkn[i, 0, 0, n2]
|
|
1426
|
+
+ np.sum(self.alpha_ijkn[i, 0, 1:, n2]*self.tau_kn[1:, n2]))
|
|
1427
|
+
afac = 0
|
|
1428
|
+
row1.addElem(_q3(self.C["b"], i, 0, n2, self.N_i, self.N_j, self.N_n + 1), -afac)
|
|
1429
|
+
row2.addElem(_q3(self.C["b"], i, 0, n2, self.N_i, self.N_j, self.N_n + 1), +afac)
|
|
1430
|
+
|
|
1431
|
+
row1.addElem(_q2(self.C["d"], i, n2, self.N_i, self.N_n), -afac)
|
|
1432
|
+
row2.addElem(_q2(self.C["d"], i, n2, self.N_i, self.N_n), +afac)
|
|
1433
|
+
|
|
1434
|
+
bfac = self.alpha_ijkn[i, 0, 0, n2] * max(0, self.tau_kn[0, max(0, n2-1)])
|
|
1435
|
+
row1.addElem(_q3(self.C["w"], i, 0, n2, self.N_i, self.N_j, self.N_n), +afac - bfac)
|
|
1436
|
+
row2.addElem(_q3(self.C["w"], i, 0, n2, self.N_i, self.N_j, self.N_n), -afac + bfac)
|
|
1437
|
+
|
|
1438
|
+
sumoni = (self.omega_in[i, n2] + self.psi_n[n2] * self.zetaBar_in[i, n2] + self.piBar_in[i, n2]
|
|
1439
|
+
+ 0.5 * self.kappa_ijn[i, 0, n2] * afac)
|
|
1440
|
+
rhs1 += sumoni
|
|
1441
|
+
rhs2 -= sumoni
|
|
1442
|
+
|
|
1443
|
+
self.A.addRow(row1, -np.inf, rhs1)
|
|
1444
|
+
self.A.addRow(row2, -np.inf, rhs2)
|
|
1445
|
+
|
|
1446
|
+
def _add_Medicare_costs(self, options):
|
|
1447
|
+
if not options.get("optimizeMedicare", False):
|
|
1448
|
+
return
|
|
1449
|
+
|
|
1450
|
+
for n in range(self.nm):
|
|
1451
|
+
self.B.setRange(_q1(self.C["m"], n, self.N_n), 0, 0)
|
|
1452
|
+
|
|
1453
|
+
Nmed = self.N_n - self.nm
|
|
1454
|
+
for nn in range(Nmed):
|
|
1455
|
+
n = self.nm + nn
|
|
1456
|
+
row = self.A.newRow()
|
|
1457
|
+
row.addElem(_q1(self.C["m"], n, self.N_n), 1)
|
|
1458
|
+
for q in range(self.N_q - 1):
|
|
1459
|
+
row.addElem(_q2(self.C["zm"], nn, q, Nmed, self.N_q - 1), -self.C_nq[nn, q+1])
|
|
1460
|
+
self.A.addRow(row, self.C_nq[nn, 0], self.C_nq[nn, 0])
|
|
1403
1461
|
|
|
1404
1462
|
def _build_objective_vector(self, objective):
|
|
1405
1463
|
c = abc.Objective(self.nvars)
|
|
@@ -1484,12 +1542,7 @@ class Plan(object):
|
|
|
1484
1542
|
self.mylog.vprint(f"Running {N} Monte Carlo simulations.")
|
|
1485
1543
|
self.mylog.setVerbose(verbose)
|
|
1486
1544
|
|
|
1487
|
-
|
|
1488
|
-
if "withMedicare" not in options:
|
|
1489
|
-
myoptions = dict(options)
|
|
1490
|
-
myoptions["withMedicare"] = False
|
|
1491
|
-
else:
|
|
1492
|
-
myoptions = options
|
|
1545
|
+
myoptions = options
|
|
1493
1546
|
|
|
1494
1547
|
if objective == "maxSpending":
|
|
1495
1548
|
columns = ["partial", objective]
|
|
@@ -1573,13 +1626,16 @@ class Plan(object):
|
|
|
1573
1626
|
"maxRothConversion",
|
|
1574
1627
|
"netSpending",
|
|
1575
1628
|
"noRothConversions",
|
|
1629
|
+
"oppCostX",
|
|
1630
|
+
"optimizeMedicare",
|
|
1576
1631
|
"previousMAGIs",
|
|
1577
1632
|
"solver",
|
|
1578
1633
|
"spendingSlack",
|
|
1579
1634
|
"startRothConversions",
|
|
1580
1635
|
"units",
|
|
1581
|
-
"
|
|
1582
|
-
"
|
|
1636
|
+
"xorConstraints",
|
|
1637
|
+
"withSCLoop",
|
|
1638
|
+
"withMedicare", # Ignore keyword.
|
|
1583
1639
|
]
|
|
1584
1640
|
# We might modify options if required.
|
|
1585
1641
|
options = {} if options is None else options
|
|
@@ -1624,11 +1680,14 @@ class Plan(object):
|
|
|
1624
1680
|
raise ValueError(f"Slack value out of range {lambdha}.")
|
|
1625
1681
|
self.lambdha = lambdha / 100
|
|
1626
1682
|
|
|
1627
|
-
#
|
|
1628
|
-
self.
|
|
1683
|
+
# Reset long-term capital gain tax rate and MAGI to zero.
|
|
1684
|
+
self.psi_n = np.zeros(self.N_n)
|
|
1685
|
+
self.MAGI_n = np.zeros(self.N_n)
|
|
1686
|
+
self.J_n = np.zeros(self.N_n)
|
|
1687
|
+
self.M_n = np.zeros(self.N_n)
|
|
1629
1688
|
|
|
1630
|
-
|
|
1631
|
-
self.
|
|
1689
|
+
self._adjustParameters(self.gamma_n, self.MAGI_n)
|
|
1690
|
+
self._buildOffsetMap(options)
|
|
1632
1691
|
|
|
1633
1692
|
solver = myoptions.get("solver", self.defaultSolver)
|
|
1634
1693
|
if solver not in knownSolvers:
|
|
@@ -1654,7 +1713,8 @@ class Plan(object):
|
|
|
1654
1713
|
"""
|
|
1655
1714
|
Self-consistent loop, regardless of solver.
|
|
1656
1715
|
"""
|
|
1657
|
-
|
|
1716
|
+
optimizeMedicare = options.get("optimizeMedicare", False)
|
|
1717
|
+
withSCLoop = options.get("withSCLoop", True)
|
|
1658
1718
|
|
|
1659
1719
|
if objective == "maxSpending":
|
|
1660
1720
|
objFac = -1 / self.xi_n[0]
|
|
@@ -1662,32 +1722,34 @@ class Plan(object):
|
|
|
1662
1722
|
objFac = -1 / self.gamma_n[-1]
|
|
1663
1723
|
|
|
1664
1724
|
it = 0
|
|
1665
|
-
absdiff = np.inf
|
|
1666
1725
|
old_x = np.zeros(self.nvars)
|
|
1667
|
-
|
|
1668
|
-
self.
|
|
1726
|
+
old_objfns = [np.inf]
|
|
1727
|
+
self._computeNLstuff(None, optimizeMedicare)
|
|
1669
1728
|
while True:
|
|
1670
|
-
|
|
1729
|
+
objfn, xx, solverSuccess, solverMsg = solverMethod(objective, options)
|
|
1671
1730
|
|
|
1672
|
-
if not solverSuccess or
|
|
1731
|
+
if not solverSuccess or objfn is None:
|
|
1673
1732
|
self.mylog.vprint("Solver failed:", solverMsg, solverSuccess)
|
|
1674
1733
|
break
|
|
1675
1734
|
|
|
1676
|
-
if not
|
|
1735
|
+
if not withSCLoop:
|
|
1677
1736
|
break
|
|
1678
1737
|
|
|
1679
|
-
self.
|
|
1680
|
-
|
|
1681
|
-
self.mylog.vprint(f"Iteration: {it} objective: {u.d(solution * objFac, f=2)}")
|
|
1738
|
+
self._computeNLstuff(xx, optimizeMedicare)
|
|
1682
1739
|
|
|
1683
1740
|
delta = xx - old_x
|
|
1684
|
-
|
|
1685
|
-
|
|
1741
|
+
absSolDiff = np.sum(np.abs(delta), axis=0)/100
|
|
1742
|
+
absObjDiff = abs(objFac*(objfn + old_objfns[-1]))/100
|
|
1743
|
+
self.mylog.vprint(f"Iteration: {it} objective: {u.d(objfn * objFac, f=2)},"
|
|
1744
|
+
f" |dX|: {absSolDiff:.2f}, |df|: {u.d(absObjDiff, f=2)}")
|
|
1745
|
+
|
|
1746
|
+
# 50 cents accuracy.
|
|
1747
|
+
if absSolDiff < .5 and absObjDiff < .5:
|
|
1686
1748
|
self.mylog.vprint("Converged on full solution.")
|
|
1687
1749
|
break
|
|
1688
1750
|
|
|
1689
1751
|
# Avoid oscillatory solutions. Look only at most recent solutions. Within $10.
|
|
1690
|
-
isclosenough = abs(-
|
|
1752
|
+
isclosenough = abs(-objfn - min(old_objfns[int(it / 2) :])) < 10 * self.xi_n[0]
|
|
1691
1753
|
if isclosenough:
|
|
1692
1754
|
self.mylog.vprint("Converged through selecting minimum oscillating objective.")
|
|
1693
1755
|
break
|
|
@@ -1697,13 +1759,13 @@ class Plan(object):
|
|
|
1697
1759
|
break
|
|
1698
1760
|
|
|
1699
1761
|
it += 1
|
|
1700
|
-
|
|
1762
|
+
old_objfns.append(-objfn)
|
|
1701
1763
|
old_x = xx
|
|
1702
1764
|
|
|
1703
1765
|
if solverSuccess:
|
|
1704
|
-
self.mylog.vprint(f"Self-consistent
|
|
1766
|
+
self.mylog.vprint(f"Self-consistent loop returned after {it+1} iterations.")
|
|
1705
1767
|
self.mylog.vprint(solverMsg)
|
|
1706
|
-
self.mylog.vprint(f"Objective: {u.d(
|
|
1768
|
+
self.mylog.vprint(f"Objective: {u.d(objfn * objFac)}")
|
|
1707
1769
|
# self.mylog.vprint('Upper bound:', u.d(-solution.mip_dual_bound))
|
|
1708
1770
|
self._aggregateResults(xx)
|
|
1709
1771
|
self._timestamp = datetime.now().strftime("%Y-%m-%d at %H:%M:%S")
|
|
@@ -1725,7 +1787,7 @@ class Plan(object):
|
|
|
1725
1787
|
"disp": False,
|
|
1726
1788
|
"mip_rel_gap": 1e-7,
|
|
1727
1789
|
"presolve": True,
|
|
1728
|
-
"node_limit": 10000 # Limit search nodes for faster solutions
|
|
1790
|
+
# "node_limit": 10000 # Limit search nodes for faster solutions
|
|
1729
1791
|
}
|
|
1730
1792
|
|
|
1731
1793
|
self._buildConstraints(objective, options)
|
|
@@ -1873,10 +1935,11 @@ class Plan(object):
|
|
|
1873
1935
|
|
|
1874
1936
|
return solution, xx, solverSuccess, solverMsg
|
|
1875
1937
|
|
|
1876
|
-
def _computeNIIT(self):
|
|
1938
|
+
def _computeNIIT(self, MAGI_n, I_n, Q_n):
|
|
1877
1939
|
"""
|
|
1878
|
-
Compute
|
|
1879
|
-
|
|
1940
|
+
Compute ACA tax on Dividends (Q) and Interests (I).
|
|
1941
|
+
Pass arguments to better understand dependencies.
|
|
1942
|
+
For accounting for rent and/or trust income, one can easily add a column
|
|
1880
1943
|
to the Wages and Contributions file and add yearly amount to Q_n + I_n below.
|
|
1881
1944
|
"""
|
|
1882
1945
|
J_n = np.zeros(self.N_n)
|
|
@@ -1887,17 +1950,17 @@ class Plan(object):
|
|
|
1887
1950
|
status -= 1
|
|
1888
1951
|
|
|
1889
1952
|
Gmax = tx.niitThreshold[status]
|
|
1890
|
-
if
|
|
1891
|
-
J_n[n] = tx.niitRate * min(
|
|
1953
|
+
if MAGI_n[n] > Gmax:
|
|
1954
|
+
J_n[n] = tx.niitRate * min(MAGI_n[n] - Gmax, I_n[n] + Q_n[n])
|
|
1892
1955
|
|
|
1893
1956
|
return J_n
|
|
1894
1957
|
|
|
1895
|
-
def
|
|
1958
|
+
def _computeNLstuff(self, x, optimizeMedicare):
|
|
1896
1959
|
"""
|
|
1897
1960
|
Compute MAGI, Medicare costs, long-term capital gain tax rate, and
|
|
1898
1961
|
net investment income tax (NIIT).
|
|
1899
1962
|
"""
|
|
1900
|
-
if x is None
|
|
1963
|
+
if x is None:
|
|
1901
1964
|
self.MAGI_n = np.zeros(self.N_n)
|
|
1902
1965
|
self.J_n = np.zeros(self.N_n)
|
|
1903
1966
|
self.M_n = np.zeros(self.N_n)
|
|
@@ -1906,9 +1969,11 @@ class Plan(object):
|
|
|
1906
1969
|
|
|
1907
1970
|
self._aggregateResults(x, short=True)
|
|
1908
1971
|
|
|
1909
|
-
self.J_n = self._computeNIIT()
|
|
1910
|
-
self.M_n = tx.mediCosts(self.yobs, self.horizons, self.MAGI_n, self.prevMAGI, self.gamma_n[:-1], self.N_n)
|
|
1972
|
+
self.J_n = self._computeNIIT(self.MAGI_n, self.I_n, self.Q_n)
|
|
1911
1973
|
self.psi_n = tx.capitalGainTaxRate(self.N_i, self.MAGI_n, self.gamma_n[:-1], self.n_d, self.N_n)
|
|
1974
|
+
# Compute Medicare through self-consistent loop.
|
|
1975
|
+
if not optimizeMedicare:
|
|
1976
|
+
self.M_n = tx.mediCosts(self.yobs, self.horizons, self.MAGI_n, self.prevMAGI, self.gamma_n[:-1], self.N_n)
|
|
1912
1977
|
|
|
1913
1978
|
return None
|
|
1914
1979
|
|
|
@@ -1923,18 +1988,19 @@ class Plan(object):
|
|
|
1923
1988
|
Nk = self.N_k
|
|
1924
1989
|
Nn = self.N_n
|
|
1925
1990
|
Nt = self.N_t
|
|
1926
|
-
#
|
|
1991
|
+
# Nzx = self.N_zx
|
|
1927
1992
|
n_d = self.n_d
|
|
1928
1993
|
|
|
1929
1994
|
Cb = self.C["b"]
|
|
1930
1995
|
Cd = self.C["d"]
|
|
1931
1996
|
Ce = self.C["e"]
|
|
1932
|
-
|
|
1997
|
+
Cf = self.C["f"]
|
|
1933
1998
|
Cg = self.C["g"]
|
|
1999
|
+
Cm = self.C["m"]
|
|
1934
2000
|
Cs = self.C["s"]
|
|
1935
2001
|
Cw = self.C["w"]
|
|
1936
2002
|
Cx = self.C["x"]
|
|
1937
|
-
|
|
2003
|
+
Czx = self.C["zx"]
|
|
1938
2004
|
|
|
1939
2005
|
x = u.roundCents(x)
|
|
1940
2006
|
|
|
@@ -1948,26 +2014,28 @@ class Plan(object):
|
|
|
1948
2014
|
self.d_in = np.array(x[Cd:Ce])
|
|
1949
2015
|
self.d_in = self.d_in.reshape((Ni, Nn))
|
|
1950
2016
|
|
|
1951
|
-
self.e_n = np.array(x[Ce:
|
|
2017
|
+
self.e_n = np.array(x[Ce:Cf])
|
|
2018
|
+
|
|
2019
|
+
self.f_tn = np.array(x[Cf:Cg])
|
|
2020
|
+
self.f_tn = self.f_tn.reshape((Nt, Nn))
|
|
1952
2021
|
|
|
1953
|
-
self.
|
|
1954
|
-
self.F_tn = self.F_tn.reshape((Nt, Nn))
|
|
2022
|
+
self.g_n = np.array(x[Cg:Cm])
|
|
1955
2023
|
|
|
1956
|
-
self.
|
|
2024
|
+
self.m_n = np.array(x[Cm:Cs])
|
|
1957
2025
|
|
|
1958
2026
|
self.s_n = np.array(x[Cs:Cw])
|
|
1959
2027
|
|
|
1960
2028
|
self.w_ijn = np.array(x[Cw:Cx])
|
|
1961
2029
|
self.w_ijn = self.w_ijn.reshape((Ni, Nj, Nn))
|
|
1962
2030
|
|
|
1963
|
-
self.x_in = np.array(x[Cx:
|
|
2031
|
+
self.x_in = np.array(x[Cx:Czx])
|
|
1964
2032
|
self.x_in = self.x_in.reshape((Ni, Nn))
|
|
1965
2033
|
|
|
1966
|
-
# self.z_inz = np.array(x[
|
|
1967
|
-
# self.z_inz = self.z_inz.reshape((Ni, Nn,
|
|
2034
|
+
# self.z_inz = np.array(x[Czx:])
|
|
2035
|
+
# self.z_inz = self.z_inz.reshape((Ni, Nn, Nzx))
|
|
1968
2036
|
# print(self.z_inz)
|
|
1969
2037
|
|
|
1970
|
-
self.G_n = np.sum(self.
|
|
2038
|
+
self.G_n = np.sum(self.f_tn, axis=0)
|
|
1971
2039
|
|
|
1972
2040
|
tau_0 = np.array(self.tau_kn[0, :])
|
|
1973
2041
|
tau_0[tau_0 < 0] = 0
|
|
@@ -1976,10 +2044,10 @@ class Plan(object):
|
|
|
1976
2044
|
self.Q_n = np.sum(
|
|
1977
2045
|
(
|
|
1978
2046
|
self.mu
|
|
1979
|
-
* (self.b_ijn[:, 0,
|
|
2047
|
+
* (self.b_ijn[:, 0, :Nn] - self.w_ijn[:, 0, :] + self.d_in[:, :] + 0.5 * self.kappa_ijn[:, 0, :Nn])
|
|
1980
2048
|
+ tau_0prev * self.w_ijn[:, 0, :]
|
|
1981
2049
|
)
|
|
1982
|
-
* self.alpha_ijkn[:, 0, 0,
|
|
2050
|
+
* self.alpha_ijkn[:, 0, 0, :Nn],
|
|
1983
2051
|
axis=0,
|
|
1984
2052
|
)
|
|
1985
2053
|
self.U_n = self.psi_n * self.Q_n
|
|
@@ -1987,14 +2055,14 @@ class Plan(object):
|
|
|
1987
2055
|
self.MAGI_n = self.G_n + self.e_n + self.Q_n
|
|
1988
2056
|
|
|
1989
2057
|
I_in = ((self.b_ijn[:, 0, :-1] + self.d_in - self.w_ijn[:, 0, :])
|
|
1990
|
-
* np.sum(self.alpha_ijkn[:, 0, 1:,
|
|
2058
|
+
* np.sum(self.alpha_ijkn[:, 0, 1:, :Nn] * self.tau_kn[1:, :], axis=1))
|
|
1991
2059
|
self.I_n = np.sum(I_in, axis=0)
|
|
1992
2060
|
|
|
1993
2061
|
# Stop after building minimu required for self-consistent loop.
|
|
1994
2062
|
if short:
|
|
1995
2063
|
return
|
|
1996
2064
|
|
|
1997
|
-
self.T_tn = self.
|
|
2065
|
+
self.T_tn = self.f_tn * self.theta_tn
|
|
1998
2066
|
self.T_n = np.sum(self.T_tn, axis=0)
|
|
1999
2067
|
self.P_n = np.zeros(Nn)
|
|
2000
2068
|
# Add early withdrawal penalty if any.
|
|
@@ -2170,8 +2238,8 @@ class Plan(object):
|
|
|
2170
2238
|
dic[" Total net investment income tax paid"] = f"{u.d(taxPaidNow)}"
|
|
2171
2239
|
dic["[Total net investment income tax paid]"] = f"{u.d(taxPaid)}"
|
|
2172
2240
|
|
|
2173
|
-
taxPaid = np.sum(self.M_n, axis=0)
|
|
2174
|
-
taxPaidNow = np.sum(self.M_n / self.gamma_n[:-1], axis=0)
|
|
2241
|
+
taxPaid = np.sum(self.m_n + self.M_n, axis=0)
|
|
2242
|
+
taxPaidNow = np.sum((self.m_n + self.M_n) / self.gamma_n[:-1], axis=0)
|
|
2175
2243
|
dic[" Total Medicare premiums paid"] = f"{u.d(taxPaidNow)}"
|
|
2176
2244
|
dic["[Total Medicare premiums paid]"] = f"{u.d(taxPaid)}"
|
|
2177
2245
|
|
|
@@ -2313,7 +2381,7 @@ class Plan(object):
|
|
|
2313
2381
|
The value parameter can be set to *nominal* or *today*, overriding
|
|
2314
2382
|
the default behavior of setDefaultPlots().
|
|
2315
2383
|
"""
|
|
2316
|
-
value = self.
|
|
2384
|
+
value = self._checkValueType(value)
|
|
2317
2385
|
title = self._name + "\nNet Available Spending"
|
|
2318
2386
|
if tag:
|
|
2319
2387
|
title += " - " + tag
|
|
@@ -2338,7 +2406,7 @@ class Plan(object):
|
|
|
2338
2406
|
The value parameter can be set to *nominal* or *today*, overriding
|
|
2339
2407
|
the default behavior of setDefaultPlots().
|
|
2340
2408
|
"""
|
|
2341
|
-
value = self.
|
|
2409
|
+
value = self._checkValueType(value)
|
|
2342
2410
|
figures = self._plotter.plot_asset_composition(self.year_n, self.inames, self.b_ijkn,
|
|
2343
2411
|
self.gamma_n, value, self._name, tag)
|
|
2344
2412
|
if figure:
|
|
@@ -2358,8 +2426,8 @@ class Plan(object):
|
|
|
2358
2426
|
The value parameter can be set to *nominal* or *today*, overriding
|
|
2359
2427
|
the default behavior of setDefaultPlots().
|
|
2360
2428
|
"""
|
|
2361
|
-
value = self.
|
|
2362
|
-
tax_brackets = tx.taxBrackets(self.N_i, self.n_d, self.N_n, self.
|
|
2429
|
+
value = self._checkValueType(value)
|
|
2430
|
+
tax_brackets = tx.taxBrackets(self.N_i, self.n_d, self.N_n, self.yOBBBA)
|
|
2363
2431
|
title = self._name + "\nTaxable Ordinary Income vs. Tax Brackets"
|
|
2364
2432
|
if tag:
|
|
2365
2433
|
title += " - " + tag
|
|
@@ -2402,7 +2470,7 @@ class Plan(object):
|
|
|
2402
2470
|
The value parameter can be set to *nominal* or *today*, overriding
|
|
2403
2471
|
the default behavior of setDefaultPlots().
|
|
2404
2472
|
"""
|
|
2405
|
-
value = self.
|
|
2473
|
+
value = self._checkValueType(value)
|
|
2406
2474
|
title = self._name + "\nSavings Balance"
|
|
2407
2475
|
if tag:
|
|
2408
2476
|
title += " - " + tag
|
|
@@ -2424,7 +2492,7 @@ class Plan(object):
|
|
|
2424
2492
|
The value parameter can be set to *nominal* or *today*, overriding
|
|
2425
2493
|
the default behavior of setDefaultPlots().
|
|
2426
2494
|
"""
|
|
2427
|
-
value = self.
|
|
2495
|
+
value = self._checkValueType(value)
|
|
2428
2496
|
title = self._name + "\nRaw Income Sources"
|
|
2429
2497
|
if tag:
|
|
2430
2498
|
title += " - " + tag
|
|
@@ -2446,13 +2514,13 @@ class Plan(object):
|
|
|
2446
2514
|
The value parameter can be set to *nominal* or *today*, overriding
|
|
2447
2515
|
the default behavior of setDefaultPlots().
|
|
2448
2516
|
"""
|
|
2449
|
-
value = self.
|
|
2517
|
+
value = self._checkValueType(value)
|
|
2450
2518
|
title = self._name + "\nFederal Income Tax"
|
|
2451
2519
|
if tag:
|
|
2452
2520
|
title += " - " + tag
|
|
2453
2521
|
# All taxes: ordinary income, dividends, and NIIT.
|
|
2454
2522
|
allTaxes = self.T_n + self.U_n + self.J_n
|
|
2455
|
-
fig = self._plotter.plot_taxes(self.year_n, allTaxes, self.M_n, self.gamma_n,
|
|
2523
|
+
fig = self._plotter.plot_taxes(self.year_n, allTaxes, self.m_n + self.M_n, self.gamma_n,
|
|
2456
2524
|
value, title, self.inames)
|
|
2457
2525
|
if figure:
|
|
2458
2526
|
return fig
|
|
@@ -2523,7 +2591,7 @@ class Plan(object):
|
|
|
2523
2591
|
"net spending": self.g_n,
|
|
2524
2592
|
"taxable ord. income": self.G_n,
|
|
2525
2593
|
"taxable gains/divs": self.Q_n,
|
|
2526
|
-
"Tax bills + Med.": self.T_n + self.U_n + self.M_n + self.J_n,
|
|
2594
|
+
"Tax bills + Med.": self.T_n + self.U_n + self.m_n + self.M_n + self.J_n,
|
|
2527
2595
|
}
|
|
2528
2596
|
|
|
2529
2597
|
fillsheet(ws, incomeDic, "currency")
|
|
@@ -2539,7 +2607,7 @@ class Plan(object):
|
|
|
2539
2607
|
"all deposits": -np.sum(self.d_in, axis=0),
|
|
2540
2608
|
"ord taxes": -self.T_n - self.J_n,
|
|
2541
2609
|
"div taxes": -self.U_n,
|
|
2542
|
-
"Medicare": -self.M_n,
|
|
2610
|
+
"Medicare": -self.m_n - self.M_n,
|
|
2543
2611
|
}
|
|
2544
2612
|
sname = "Cash Flow"
|
|
2545
2613
|
ws = wb.create_sheet(sname)
|
|
@@ -2726,7 +2794,7 @@ def _saveWorkbook(wb, basename, overwrite, mylog):
|
|
|
2726
2794
|
else:
|
|
2727
2795
|
fname = basename
|
|
2728
2796
|
|
|
2729
|
-
if overwrite
|
|
2797
|
+
if not overwrite and isfile(fname):
|
|
2730
2798
|
mylog.print(f'File "{fname}" already exists.')
|
|
2731
2799
|
key = input("Overwrite? [Ny] ")
|
|
2732
2800
|
if key != "y":
|
owlplanner/progress.py
CHANGED
|
@@ -3,7 +3,7 @@ A simple object to display progress.
|
|
|
3
3
|
|
|
4
4
|
Copyright © 2024 - Martin-D. Lacasse
|
|
5
5
|
|
|
6
|
-
Disclaimers: This code is for
|
|
6
|
+
Disclaimers: This code is for educational purposes only and does not constitute financial advice.
|
|
7
7
|
|
|
8
8
|
"""
|
|
9
9
|
|
owlplanner/rates.py
CHANGED
|
@@ -23,7 +23,7 @@ to the last current data year.
|
|
|
23
23
|
|
|
24
24
|
Copyright © 2024 - Martin-D. Lacasse
|
|
25
25
|
|
|
26
|
-
Disclaimers: This code is for
|
|
26
|
+
Disclaimers: This code is for educational purposes only and does not constitute financial advice.
|
|
27
27
|
|
|
28
28
|
"""
|
|
29
29
|
|
|
@@ -32,7 +32,6 @@ import numpy as np
|
|
|
32
32
|
import pandas as pd
|
|
33
33
|
import os
|
|
34
34
|
import sys
|
|
35
|
-
from datetime import date
|
|
36
35
|
|
|
37
36
|
from owlplanner import mylogging as log
|
|
38
37
|
from owlplanner import utils as u
|
|
@@ -120,27 +119,6 @@ def getRatesDistributions(frm, to, mylog=None):
|
|
|
120
119
|
return means, stdev, corr, covar
|
|
121
120
|
|
|
122
121
|
|
|
123
|
-
def historicalValue(amount, year):
|
|
124
|
-
"""
|
|
125
|
-
Return the deflated value of amount given in this year's dollars as
|
|
126
|
-
valued at the beginning of the year specified.
|
|
127
|
-
"""
|
|
128
|
-
thisyear = date.today().year
|
|
129
|
-
if TO != thisyear - 1:
|
|
130
|
-
raise RuntimeError(f"Rates file needs to be updated to be current to {thisyear}.")
|
|
131
|
-
if year < FROM:
|
|
132
|
-
raise ValueError(f"Only data from {FROM} is available.")
|
|
133
|
-
if year > thisyear:
|
|
134
|
-
raise ValueError(f"Year must be < {thisyear} for historical data.")
|
|
135
|
-
|
|
136
|
-
span = thisyear - year
|
|
137
|
-
ub = len(Inflation)
|
|
138
|
-
for n in range(ub - span, ub):
|
|
139
|
-
amount /= 1 + Inflation[n] / 100
|
|
140
|
-
|
|
141
|
-
return amount
|
|
142
|
-
|
|
143
|
-
|
|
144
122
|
class Rates(object):
|
|
145
123
|
"""
|
|
146
124
|
Rates are stored in a 4-array in the following order:
|
owlplanner/tax2025.py
CHANGED
|
@@ -12,7 +12,7 @@ Module to handle all tax calculations.
|
|
|
12
12
|
|
|
13
13
|
Copyright © 2024 - Martin-D. Lacasse
|
|
14
14
|
|
|
15
|
-
Disclaimers: This code is for
|
|
15
|
+
Disclaimers: This code is for educational purposes only and does not constitute financial advice.
|
|
16
16
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
@@ -25,8 +25,8 @@ from datetime import date
|
|
|
25
25
|
|
|
26
26
|
taxBracketNames = ["10%", "12/15%", "22/25%", "24/28%", "32/33%", "35%", "37/40%"]
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
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
30
|
|
|
31
31
|
###############################################################################
|
|
32
32
|
# Start of section where rates need to be actualized every year.
|
|
@@ -34,7 +34,7 @@ rates_nonTCJA = np.array([0.10, 0.15, 0.25, 0.28, 0.33, 0.35, 0.396])
|
|
|
34
34
|
# Single [0] and married filing jointly [1].
|
|
35
35
|
|
|
36
36
|
# These are 2025 current.
|
|
37
|
-
|
|
37
|
+
taxBrackets_OBBBA = np.array(
|
|
38
38
|
[
|
|
39
39
|
[11925, 48475, 103350, 197300, 250525, 626350, 9999999],
|
|
40
40
|
[23850, 96950, 206700, 394600, 501050, 751600, 9999999],
|
|
@@ -52,7 +52,7 @@ irmaaBrackets = np.array(
|
|
|
52
52
|
# Following values are incremental IRMAA part B monthly fees.
|
|
53
53
|
irmaaFees = 12 * np.array([185.00, 74.00, 111.00, 110.90, 111.00, 37.00])
|
|
54
54
|
|
|
55
|
-
# Make projection for
|
|
55
|
+
# Make projection for pre-TCJA using 2017 to current year.
|
|
56
56
|
# taxBrackets_2017 = np.array(
|
|
57
57
|
# [ [9325, 37950, 91900, 191650, 416700, 418400, 9999999],
|
|
58
58
|
# [18650, 75900, 153100, 233350, 416700, 470700, 9999999],
|
|
@@ -63,7 +63,7 @@ irmaaFees = 12 * np.array([185.00, 74.00, 111.00, 110.90, 111.00, 37.00])
|
|
|
63
63
|
# For 2025, I used a 30.5% adjustment from 2017, rounded to closest 50.
|
|
64
64
|
#
|
|
65
65
|
# These are speculated.
|
|
66
|
-
|
|
66
|
+
taxBrackets_preTCJA = np.array(
|
|
67
67
|
[
|
|
68
68
|
[12150, 49550, 119950, 250200, 544000, 546200, 9999999], # Single
|
|
69
69
|
[24350, 99100, 199850, 304600, 543950, 614450, 9999999], # MFJ
|
|
@@ -71,11 +71,11 @@ taxBrackets_nonTCJA = np.array(
|
|
|
71
71
|
)
|
|
72
72
|
|
|
73
73
|
# These are 2025 current (adjusted for inflation).
|
|
74
|
-
|
|
74
|
+
stdDeduction_OBBBA = np.array([15750, 31500]) # Single, MFJ
|
|
75
75
|
# These are speculated (adjusted for inflation).
|
|
76
|
-
|
|
76
|
+
stdDeduction_preTCJA = np.array([8300, 16600]) # Single, MFJ
|
|
77
77
|
|
|
78
|
-
# These are current (adjusted for inflation).
|
|
78
|
+
# These are current (adjusted for inflation) per individual.
|
|
79
79
|
extra65Deduction = np.array([2000, 1600]) # Single, MFJ
|
|
80
80
|
|
|
81
81
|
# Thresholds for capital gains (adjusted for inflation).
|
|
@@ -90,11 +90,51 @@ capGainRates = np.array(
|
|
|
90
90
|
niitThreshold = np.array([200000, 250000])
|
|
91
91
|
niitRate = 0.038
|
|
92
92
|
|
|
93
|
+
# Thresholds for 65+ bonus for circumventing tax on social security.
|
|
94
|
+
bonusThreshold = np.array([75000, 150000])
|
|
95
|
+
|
|
93
96
|
###############################################################################
|
|
94
97
|
# End of section where rates need to be actualized every year.
|
|
95
98
|
###############################################################################
|
|
96
99
|
|
|
97
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
|
+
|
|
98
138
|
def capitalGainTaxRate(Ni, magi_n, gamma_n, nd, Nn):
|
|
99
139
|
"""
|
|
100
140
|
Return an array of decimal rates for capital gains.
|
|
@@ -105,7 +145,7 @@ def capitalGainTaxRate(Ni, magi_n, gamma_n, nd, Nn):
|
|
|
105
145
|
cgRate_n = np.zeros(Nn)
|
|
106
146
|
|
|
107
147
|
for n in range(Nn):
|
|
108
|
-
if n == nd:
|
|
148
|
+
if status and n == nd:
|
|
109
149
|
status -= 1
|
|
110
150
|
|
|
111
151
|
if magi_n[n] > gamma_n[n] * capGainRates[status][1]:
|
|
@@ -140,11 +180,11 @@ def mediCosts(yobs, horizons, magi, prevmagi, gamma_n, Nn):
|
|
|
140
180
|
return costs
|
|
141
181
|
|
|
142
182
|
|
|
143
|
-
def taxParams(yobs, i_d, n_d, N_n,
|
|
183
|
+
def taxParams(yobs, i_d, n_d, N_n, gamma_n, MAGI_n, yOBBBA=2099):
|
|
144
184
|
"""
|
|
145
185
|
Input is year of birth, index of shortest-lived individual,
|
|
146
186
|
lifespan of shortest-lived individual, total number of years
|
|
147
|
-
in the plan, and the year that
|
|
187
|
+
in the plan, and the year that preTCJA rates might come back.
|
|
148
188
|
|
|
149
189
|
It returns 3 time series:
|
|
150
190
|
1) Standard deductions at year n (sigma_n).
|
|
@@ -154,15 +194,15 @@ def taxParams(yobs, i_d, n_d, N_n, y_TCJA=2026):
|
|
|
154
194
|
Returned values are not indexed for inflation.
|
|
155
195
|
"""
|
|
156
196
|
# Compute the deltas in-place between brackets, starting from the end.
|
|
157
|
-
|
|
158
|
-
|
|
197
|
+
deltaBrackets_OBBBA = np.array(taxBrackets_OBBBA)
|
|
198
|
+
deltaBrackets_preTCJA = np.array(taxBrackets_preTCJA)
|
|
159
199
|
for t in range(6, 0, -1):
|
|
160
200
|
for i in range(2):
|
|
161
|
-
|
|
162
|
-
|
|
201
|
+
deltaBrackets_OBBBA[i, t] -= deltaBrackets_OBBBA[i, t - 1]
|
|
202
|
+
deltaBrackets_preTCJA[i, t] -= deltaBrackets_preTCJA[i, t - 1]
|
|
163
203
|
|
|
164
204
|
# Prepare the 3 arrays to return - use transpose for easy slicing.
|
|
165
|
-
|
|
205
|
+
sigmaBar = np.zeros((N_n))
|
|
166
206
|
Delta = np.zeros((N_n, 7))
|
|
167
207
|
theta = np.zeros((N_n, 7))
|
|
168
208
|
|
|
@@ -176,51 +216,57 @@ def taxParams(yobs, i_d, n_d, N_n, y_TCJA=2026):
|
|
|
176
216
|
souls.remove(i_d)
|
|
177
217
|
filingStatus -= 1
|
|
178
218
|
|
|
179
|
-
if thisyear + n <
|
|
180
|
-
|
|
181
|
-
Delta[n, :] =
|
|
219
|
+
if thisyear + n < yOBBBA:
|
|
220
|
+
sigmaBar[n] = stdDeduction_OBBBA[filingStatus] * gamma_n[n]
|
|
221
|
+
Delta[n, :] = deltaBrackets_OBBBA[filingStatus, :]
|
|
182
222
|
else:
|
|
183
|
-
|
|
184
|
-
Delta[n, :] =
|
|
223
|
+
sigmaBar[n] = stdDeduction_preTCJA[filingStatus] * gamma_n[n]
|
|
224
|
+
Delta[n, :] = deltaBrackets_preTCJA[filingStatus, :]
|
|
185
225
|
|
|
186
|
-
# Add 65+ additional exemption(s).
|
|
226
|
+
# Add 65+ additional exemption(s) and "bonus" phasing out.
|
|
187
227
|
for i in souls:
|
|
188
228
|
if thisyear + n - yobs[i] >= 65:
|
|
189
|
-
|
|
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]))
|
|
190
232
|
|
|
191
233
|
# Fill in future tax rates for year n.
|
|
192
|
-
if thisyear + n <
|
|
193
|
-
theta[n, :] =
|
|
234
|
+
if thisyear + n < yOBBBA:
|
|
235
|
+
theta[n, :] = rates_OBBBA[:]
|
|
194
236
|
else:
|
|
195
|
-
theta[n, :] =
|
|
237
|
+
theta[n, :] = rates_preTCJA[:]
|
|
196
238
|
|
|
197
239
|
Delta = Delta.transpose()
|
|
198
240
|
theta = theta.transpose()
|
|
199
241
|
|
|
200
|
-
# Return series unadjusted for inflation, in STD order.
|
|
201
|
-
return
|
|
242
|
+
# Return series unadjusted for inflation, except for sigmaBar, in STD order.
|
|
243
|
+
return sigmaBar, theta, Delta
|
|
202
244
|
|
|
203
245
|
|
|
204
|
-
def taxBrackets(N_i, n_d, N_n,
|
|
246
|
+
def taxBrackets(N_i, n_d, N_n, yOBBBA=2099):
|
|
205
247
|
"""
|
|
206
248
|
Return dictionary containing future tax brackets
|
|
207
249
|
unadjusted for inflation for plotting.
|
|
208
250
|
"""
|
|
209
251
|
if not (0 < N_i <= 2):
|
|
210
252
|
raise ValueError(f"Cannot process {N_i} individuals.")
|
|
253
|
+
|
|
211
254
|
n_d = min(n_d, N_n)
|
|
212
255
|
status = N_i - 1
|
|
213
256
|
|
|
214
|
-
# Number of years left in
|
|
257
|
+
# Number of years left in OBBBA from this year.
|
|
215
258
|
thisyear = date.today().year
|
|
216
|
-
|
|
259
|
+
if yOBBBA < thisyear:
|
|
260
|
+
raise ValueError(f"Expiration year {yOBBBA} cannot be in the past.")
|
|
261
|
+
|
|
262
|
+
ytc = yOBBBA - thisyear
|
|
217
263
|
|
|
218
264
|
data = {}
|
|
219
265
|
for t in range(len(taxBracketNames) - 1):
|
|
220
266
|
array = np.zeros(N_n)
|
|
221
267
|
for n in range(N_n):
|
|
222
268
|
stat = status if n < n_d else 0
|
|
223
|
-
array[n] =
|
|
269
|
+
array[n] = taxBrackets_OBBBA[stat][t] if n < ytc else taxBrackets_preTCJA[stat][t]
|
|
224
270
|
|
|
225
271
|
data[taxBracketNames[t]] = array
|
|
226
272
|
|
|
@@ -280,12 +326,12 @@ def rho_in(yobs, N_n):
|
|
|
280
326
|
thisyear = date.today().year
|
|
281
327
|
for i in range(N_i):
|
|
282
328
|
agenow = thisyear - yobs[i]
|
|
329
|
+
# Account for increase of RMD age between 2023 and 2032.
|
|
330
|
+
yrmd = 70 if yobs[i] < 1949 else 72 if 1949 <= yobs[i] <= 1950 else 73 if 1951 <= yobs[i] <= 1959 else 75
|
|
283
331
|
for n in range(N_n):
|
|
284
|
-
year = thisyear + n
|
|
285
332
|
yage = agenow + n
|
|
286
333
|
|
|
287
|
-
|
|
288
|
-
if (yage < 73) or (year > 2032 and yage < 75):
|
|
334
|
+
if yage < yrmd:
|
|
289
335
|
pass # rho[i][n] = 0
|
|
290
336
|
else:
|
|
291
337
|
rho[i][n] = 1.0 / rmdTable[yage - 72]
|
owlplanner/timelists.py
CHANGED
|
@@ -12,7 +12,7 @@ Utility functions to read and check timelists.
|
|
|
12
12
|
|
|
13
13
|
Copyright © 2024 - Martin-D. Lacasse
|
|
14
14
|
|
|
15
|
-
Disclaimers: This code is for
|
|
15
|
+
Disclaimers: This code is for educational purposes only and does not constitute financial advice.
|
|
16
16
|
|
|
17
17
|
"""
|
|
18
18
|
|
owlplanner/utils.py
CHANGED
|
@@ -6,7 +6,7 @@ This file contains functions for handling data.
|
|
|
6
6
|
|
|
7
7
|
Copyright © 2024 - Martin-D. Lacasse
|
|
8
8
|
|
|
9
|
-
Disclaimers: This code is for
|
|
9
|
+
Disclaimers: This code is for educational purposes only and does not constitute financial advice.
|
|
10
10
|
|
|
11
11
|
"""
|
|
12
12
|
|
owlplanner/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2025.
|
|
1
|
+
__version__ = "2025.08.01"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
owlplanner/__init__.py,sha256=hJ2i4m2JpHPAKyQLjYOXpJzeEsgcTcKD-Vhm0AIjjWg,592
|
|
2
|
+
owlplanner/abcapi.py,sha256=rtg7d0UbftinokR9VlB49VUjDjzUq3ONnJbhMXVIrgo,6879
|
|
3
|
+
owlplanner/config.py,sha256=JJOtS6HyqA4qUHUZydSGG_RMWaCfVMRSOAfWbt4evMI,12461
|
|
4
|
+
owlplanner/mylogging.py,sha256=OVGeDFO7LIZG91R6HMpZBzjno-B8PH8Fo00Jw2Pdgqw,2558
|
|
5
|
+
owlplanner/plan.py,sha256=keKC9-XeQxza9--D7TaQfcZCLkGDy5yVV9D5pN25MHg,115211
|
|
6
|
+
owlplanner/progress.py,sha256=dUUlFmSAKUei36rUj2BINRY10f_YEUo_e23d0es6nrc,530
|
|
7
|
+
owlplanner/rates.py,sha256=9Nmo8AKsyi5PoCUrzhr06phkSlNTv-TXzj5iYFU76AY,14113
|
|
8
|
+
owlplanner/tax2025.py,sha256=2Jb_UbPT6ye-znRjA0nSaF8T8M17QW4MoRPDoW9XJ8s,10833
|
|
9
|
+
owlplanner/timelists.py,sha256=Q4kBt9kKAa5qxsvOe9wfyUtCQVgiwPmJXTwXUPRBBv8,4066
|
|
10
|
+
owlplanner/utils.py,sha256=afAjeO6Msf6Rn4jwz_7Ody9rHGWlBR7iQFqe1xzLNQc,2513
|
|
11
|
+
owlplanner/version.py,sha256=Or3KaHd8BXidAsAkScGwQRvtWIF5uufpsnfgNdR-Kpw,28
|
|
12
|
+
owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
|
|
14
|
+
owlplanner/plotting/__init__.py,sha256=uhxqtUi0OI-QWNOO2LkXgQViW_9yM3rYb-204Wit974,250
|
|
15
|
+
owlplanner/plotting/base.py,sha256=UimGKpMTV-dVm3BX5Apr_Ltorc7dlDLCRPRQ3RF_v7c,2578
|
|
16
|
+
owlplanner/plotting/factory.py,sha256=EDopIAPQr9zHRgemObko18FlCeRNhNCoLNNFAOq-X6s,1030
|
|
17
|
+
owlplanner/plotting/matplotlib_backend.py,sha256=AOEkapD94U5hGNoS0EdbRoe8mgdMHH4oOvkXADZS914,17957
|
|
18
|
+
owlplanner/plotting/plotly_backend.py,sha256=AO33GxBHGYG5osir_H1iRRtGxdhs4AjfLV2d_xm35nY,33138
|
|
19
|
+
owlplanner-2025.8.1.dist-info/METADATA,sha256=9WRoRbNMdk2kxbT4-Coq912VJixT9rlF1Cv48Po4VJw,54044
|
|
20
|
+
owlplanner-2025.8.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
+
owlplanner-2025.8.1.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
22
|
+
owlplanner-2025.8.1.dist-info/RECORD,,
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
owlplanner/__init__.py,sha256=hJ2i4m2JpHPAKyQLjYOXpJzeEsgcTcKD-Vhm0AIjjWg,592
|
|
2
|
-
owlplanner/abcapi.py,sha256=8VCXS7nH_QZYxCUU3lwO0_UPR9Q5fuYQ6DHDLvHVLPg,6878
|
|
3
|
-
owlplanner/config.py,sha256=-sSz37hwlnmI9_oXZn-R1rpmY0Vyk5L4X--NxGpgEMA,12446
|
|
4
|
-
owlplanner/mylogging.py,sha256=RKUr-y-1XvKZzLMcfdtm4IM30LuRpJwb2qUeXmAWqME,2557
|
|
5
|
-
owlplanner/plan.py,sha256=rivQ9lSFJx6Eahx83VyTOm6n4uxjbr6LiwiyChRhAnc,111133
|
|
6
|
-
owlplanner/progress.py,sha256=2DOjOLo6Mo7m21wY-9iZhoUksAyi4VCbb6UL2RegNCw,529
|
|
7
|
-
owlplanner/rates.py,sha256=7jXcuHbkJ3AVIeBYZdwme18rdYslIzCuT-c0cLzvKUU,14819
|
|
8
|
-
owlplanner/tax2025.py,sha256=3uDJfKiSRFUp5WDcouAnTEQqEY7LnDKxqxDSKiTsOSQ,8927
|
|
9
|
-
owlplanner/timelists.py,sha256=4pRumdoFlEmmh07wpGhDqauHl2doLG5JcRkvi41fvR4,4065
|
|
10
|
-
owlplanner/utils.py,sha256=6Ky8ZKfNE9x--3znsZ8VZaT2PptDinszRxWsOCPanu8,2512
|
|
11
|
-
owlplanner/version.py,sha256=J00-UKwWWUboYr10jyhzSLOwp2iWtGoC1DRPgnuINV0,28
|
|
12
|
-
owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
|
|
14
|
-
owlplanner/plotting/__init__.py,sha256=uhxqtUi0OI-QWNOO2LkXgQViW_9yM3rYb-204Wit974,250
|
|
15
|
-
owlplanner/plotting/base.py,sha256=UimGKpMTV-dVm3BX5Apr_Ltorc7dlDLCRPRQ3RF_v7c,2578
|
|
16
|
-
owlplanner/plotting/factory.py,sha256=EDopIAPQr9zHRgemObko18FlCeRNhNCoLNNFAOq-X6s,1030
|
|
17
|
-
owlplanner/plotting/matplotlib_backend.py,sha256=AOEkapD94U5hGNoS0EdbRoe8mgdMHH4oOvkXADZS914,17957
|
|
18
|
-
owlplanner/plotting/plotly_backend.py,sha256=AO33GxBHGYG5osir_H1iRRtGxdhs4AjfLV2d_xm35nY,33138
|
|
19
|
-
owlplanner-2025.7.1.dist-info/METADATA,sha256=_T6vNe7aESAIt668fK9IVb0VDEv5P3Z16bYPyTvr9QY,54044
|
|
20
|
-
owlplanner-2025.7.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
-
owlplanner-2025.7.1.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
22
|
-
owlplanner-2025.7.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|