owlplanner 2025.7.1__py3-none-any.whl → 2025.9.15__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 +7 -3
- owlplanner/mylogging.py +1 -1
- owlplanner/plan.py +225 -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.9.15.dist-info}/METADATA +2 -2
- owlplanner-2025.9.15.dist-info/RECORD +22 -0
- owlplanner-2025.7.1.dist-info/RECORD +0 -22
- {owlplanner-2025.7.1.dist-info → owlplanner-2025.9.15.dist-info}/WHEEL +0 -0
- {owlplanner-2025.7.1.dist-info → owlplanner-2025.9.15.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
|
|
@@ -296,6 +296,10 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
|
|
|
296
296
|
# Solver Options.
|
|
297
297
|
p.solverOptions = diconf["Solver Options"]
|
|
298
298
|
|
|
299
|
+
# Address legacy case files.
|
|
300
|
+
if diconf["Solver Options"].get("withMedicare", None) is True:
|
|
301
|
+
p.solverOptions["withMedicare"] = "loop"
|
|
302
|
+
|
|
299
303
|
# Check consistency of noRothConversions.
|
|
300
304
|
name = p.solverOptions.get("noRothConversions", "None")
|
|
301
305
|
if name != "None" and name not in p.inames:
|
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("withMedicare", "loop") == "optimize"
|
|
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 options.get("withMedicare", "loop") != "optimize":
|
|
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 options.get("withMedicare", "loop") != "optimize":
|
|
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,15 @@ class Plan(object):
|
|
|
1573
1626
|
"maxRothConversion",
|
|
1574
1627
|
"netSpending",
|
|
1575
1628
|
"noRothConversions",
|
|
1629
|
+
"oppCostX",
|
|
1630
|
+
"withMedicare",
|
|
1576
1631
|
"previousMAGIs",
|
|
1577
1632
|
"solver",
|
|
1578
1633
|
"spendingSlack",
|
|
1579
1634
|
"startRothConversions",
|
|
1580
1635
|
"units",
|
|
1581
|
-
"
|
|
1582
|
-
"
|
|
1636
|
+
"xorConstraints",
|
|
1637
|
+
"withSCLoop",
|
|
1583
1638
|
]
|
|
1584
1639
|
# We might modify options if required.
|
|
1585
1640
|
options = {} if options is None else options
|
|
@@ -1624,11 +1679,14 @@ class Plan(object):
|
|
|
1624
1679
|
raise ValueError(f"Slack value out of range {lambdha}.")
|
|
1625
1680
|
self.lambdha = lambdha / 100
|
|
1626
1681
|
|
|
1627
|
-
#
|
|
1628
|
-
self.
|
|
1682
|
+
# Reset long-term capital gain tax rate and MAGI to zero.
|
|
1683
|
+
self.psi_n = np.zeros(self.N_n)
|
|
1684
|
+
self.MAGI_n = np.zeros(self.N_n)
|
|
1685
|
+
self.J_n = np.zeros(self.N_n)
|
|
1686
|
+
self.M_n = np.zeros(self.N_n)
|
|
1629
1687
|
|
|
1630
|
-
|
|
1631
|
-
self.
|
|
1688
|
+
self._adjustParameters(self.gamma_n, self.MAGI_n)
|
|
1689
|
+
self._buildOffsetMap(options)
|
|
1632
1690
|
|
|
1633
1691
|
solver = myoptions.get("solver", self.defaultSolver)
|
|
1634
1692
|
if solver not in knownSolvers:
|
|
@@ -1654,7 +1712,8 @@ class Plan(object):
|
|
|
1654
1712
|
"""
|
|
1655
1713
|
Self-consistent loop, regardless of solver.
|
|
1656
1714
|
"""
|
|
1657
|
-
|
|
1715
|
+
includeMedicare = options.get("withMedicare", "loop") == "loop"
|
|
1716
|
+
withSCLoop = options.get("withSCLoop", True)
|
|
1658
1717
|
|
|
1659
1718
|
if objective == "maxSpending":
|
|
1660
1719
|
objFac = -1 / self.xi_n[0]
|
|
@@ -1662,32 +1721,34 @@ class Plan(object):
|
|
|
1662
1721
|
objFac = -1 / self.gamma_n[-1]
|
|
1663
1722
|
|
|
1664
1723
|
it = 0
|
|
1665
|
-
absdiff = np.inf
|
|
1666
1724
|
old_x = np.zeros(self.nvars)
|
|
1667
|
-
|
|
1668
|
-
self.
|
|
1725
|
+
old_objfns = [np.inf]
|
|
1726
|
+
self._computeNLstuff(None, includeMedicare)
|
|
1669
1727
|
while True:
|
|
1670
|
-
|
|
1728
|
+
objfn, xx, solverSuccess, solverMsg = solverMethod(objective, options)
|
|
1671
1729
|
|
|
1672
|
-
if not solverSuccess or
|
|
1730
|
+
if not solverSuccess or objfn is None:
|
|
1673
1731
|
self.mylog.vprint("Solver failed:", solverMsg, solverSuccess)
|
|
1674
1732
|
break
|
|
1675
1733
|
|
|
1676
|
-
if not
|
|
1734
|
+
if not withSCLoop:
|
|
1677
1735
|
break
|
|
1678
1736
|
|
|
1679
|
-
self.
|
|
1680
|
-
|
|
1681
|
-
self.mylog.vprint(f"Iteration: {it} objective: {u.d(solution * objFac, f=2)}")
|
|
1737
|
+
self._computeNLstuff(xx, includeMedicare)
|
|
1682
1738
|
|
|
1683
1739
|
delta = xx - old_x
|
|
1684
|
-
|
|
1685
|
-
|
|
1740
|
+
absSolDiff = np.sum(np.abs(delta), axis=0)/100
|
|
1741
|
+
absObjDiff = abs(objFac*(objfn + old_objfns[-1]))/100
|
|
1742
|
+
self.mylog.vprint(f"Iteration: {it} objective: {u.d(objfn * objFac, f=2)},"
|
|
1743
|
+
f" |dX|: {absSolDiff:.2f}, |df|: {u.d(absObjDiff, f=2)}")
|
|
1744
|
+
|
|
1745
|
+
# 50 cents accuracy.
|
|
1746
|
+
if absSolDiff < .5 and absObjDiff < .5:
|
|
1686
1747
|
self.mylog.vprint("Converged on full solution.")
|
|
1687
1748
|
break
|
|
1688
1749
|
|
|
1689
1750
|
# Avoid oscillatory solutions. Look only at most recent solutions. Within $10.
|
|
1690
|
-
isclosenough = abs(-
|
|
1751
|
+
isclosenough = abs(-objfn - min(old_objfns[int(it / 2) :])) < 10 * self.xi_n[0]
|
|
1691
1752
|
if isclosenough:
|
|
1692
1753
|
self.mylog.vprint("Converged through selecting minimum oscillating objective.")
|
|
1693
1754
|
break
|
|
@@ -1697,13 +1758,13 @@ class Plan(object):
|
|
|
1697
1758
|
break
|
|
1698
1759
|
|
|
1699
1760
|
it += 1
|
|
1700
|
-
|
|
1761
|
+
old_objfns.append(-objfn)
|
|
1701
1762
|
old_x = xx
|
|
1702
1763
|
|
|
1703
1764
|
if solverSuccess:
|
|
1704
|
-
self.mylog.vprint(f"Self-consistent
|
|
1765
|
+
self.mylog.vprint(f"Self-consistent loop returned after {it+1} iterations.")
|
|
1705
1766
|
self.mylog.vprint(solverMsg)
|
|
1706
|
-
self.mylog.vprint(f"Objective: {u.d(
|
|
1767
|
+
self.mylog.vprint(f"Objective: {u.d(objfn * objFac)}")
|
|
1707
1768
|
# self.mylog.vprint('Upper bound:', u.d(-solution.mip_dual_bound))
|
|
1708
1769
|
self._aggregateResults(xx)
|
|
1709
1770
|
self._timestamp = datetime.now().strftime("%Y-%m-%d at %H:%M:%S")
|
|
@@ -1725,7 +1786,7 @@ class Plan(object):
|
|
|
1725
1786
|
"disp": False,
|
|
1726
1787
|
"mip_rel_gap": 1e-7,
|
|
1727
1788
|
"presolve": True,
|
|
1728
|
-
"node_limit": 10000 # Limit search nodes for faster solutions
|
|
1789
|
+
# "node_limit": 10000 # Limit search nodes for faster solutions
|
|
1729
1790
|
}
|
|
1730
1791
|
|
|
1731
1792
|
self._buildConstraints(objective, options)
|
|
@@ -1873,10 +1934,11 @@ class Plan(object):
|
|
|
1873
1934
|
|
|
1874
1935
|
return solution, xx, solverSuccess, solverMsg
|
|
1875
1936
|
|
|
1876
|
-
def _computeNIIT(self):
|
|
1937
|
+
def _computeNIIT(self, MAGI_n, I_n, Q_n):
|
|
1877
1938
|
"""
|
|
1878
|
-
Compute
|
|
1879
|
-
|
|
1939
|
+
Compute ACA tax on Dividends (Q) and Interests (I).
|
|
1940
|
+
Pass arguments to better understand dependencies.
|
|
1941
|
+
For accounting for rent and/or trust income, one can easily add a column
|
|
1880
1942
|
to the Wages and Contributions file and add yearly amount to Q_n + I_n below.
|
|
1881
1943
|
"""
|
|
1882
1944
|
J_n = np.zeros(self.N_n)
|
|
@@ -1887,17 +1949,17 @@ class Plan(object):
|
|
|
1887
1949
|
status -= 1
|
|
1888
1950
|
|
|
1889
1951
|
Gmax = tx.niitThreshold[status]
|
|
1890
|
-
if
|
|
1891
|
-
J_n[n] = tx.niitRate * min(
|
|
1952
|
+
if MAGI_n[n] > Gmax:
|
|
1953
|
+
J_n[n] = tx.niitRate * min(MAGI_n[n] - Gmax, I_n[n] + Q_n[n])
|
|
1892
1954
|
|
|
1893
1955
|
return J_n
|
|
1894
1956
|
|
|
1895
|
-
def
|
|
1957
|
+
def _computeNLstuff(self, x, includeMedicare):
|
|
1896
1958
|
"""
|
|
1897
1959
|
Compute MAGI, Medicare costs, long-term capital gain tax rate, and
|
|
1898
1960
|
net investment income tax (NIIT).
|
|
1899
1961
|
"""
|
|
1900
|
-
if x is None
|
|
1962
|
+
if x is None:
|
|
1901
1963
|
self.MAGI_n = np.zeros(self.N_n)
|
|
1902
1964
|
self.J_n = np.zeros(self.N_n)
|
|
1903
1965
|
self.M_n = np.zeros(self.N_n)
|
|
@@ -1906,9 +1968,11 @@ class Plan(object):
|
|
|
1906
1968
|
|
|
1907
1969
|
self._aggregateResults(x, short=True)
|
|
1908
1970
|
|
|
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)
|
|
1971
|
+
self.J_n = self._computeNIIT(self.MAGI_n, self.I_n, self.Q_n)
|
|
1911
1972
|
self.psi_n = tx.capitalGainTaxRate(self.N_i, self.MAGI_n, self.gamma_n[:-1], self.n_d, self.N_n)
|
|
1973
|
+
# Compute Medicare through self-consistent loop.
|
|
1974
|
+
if includeMedicare:
|
|
1975
|
+
self.M_n = tx.mediCosts(self.yobs, self.horizons, self.MAGI_n, self.prevMAGI, self.gamma_n[:-1], self.N_n)
|
|
1912
1976
|
|
|
1913
1977
|
return None
|
|
1914
1978
|
|
|
@@ -1923,18 +1987,19 @@ class Plan(object):
|
|
|
1923
1987
|
Nk = self.N_k
|
|
1924
1988
|
Nn = self.N_n
|
|
1925
1989
|
Nt = self.N_t
|
|
1926
|
-
#
|
|
1990
|
+
# Nzx = self.N_zx
|
|
1927
1991
|
n_d = self.n_d
|
|
1928
1992
|
|
|
1929
1993
|
Cb = self.C["b"]
|
|
1930
1994
|
Cd = self.C["d"]
|
|
1931
1995
|
Ce = self.C["e"]
|
|
1932
|
-
|
|
1996
|
+
Cf = self.C["f"]
|
|
1933
1997
|
Cg = self.C["g"]
|
|
1998
|
+
Cm = self.C["m"]
|
|
1934
1999
|
Cs = self.C["s"]
|
|
1935
2000
|
Cw = self.C["w"]
|
|
1936
2001
|
Cx = self.C["x"]
|
|
1937
|
-
|
|
2002
|
+
Czx = self.C["zx"]
|
|
1938
2003
|
|
|
1939
2004
|
x = u.roundCents(x)
|
|
1940
2005
|
|
|
@@ -1948,26 +2013,28 @@ class Plan(object):
|
|
|
1948
2013
|
self.d_in = np.array(x[Cd:Ce])
|
|
1949
2014
|
self.d_in = self.d_in.reshape((Ni, Nn))
|
|
1950
2015
|
|
|
1951
|
-
self.e_n = np.array(x[Ce:
|
|
2016
|
+
self.e_n = np.array(x[Ce:Cf])
|
|
2017
|
+
|
|
2018
|
+
self.f_tn = np.array(x[Cf:Cg])
|
|
2019
|
+
self.f_tn = self.f_tn.reshape((Nt, Nn))
|
|
1952
2020
|
|
|
1953
|
-
self.
|
|
1954
|
-
self.F_tn = self.F_tn.reshape((Nt, Nn))
|
|
2021
|
+
self.g_n = np.array(x[Cg:Cm])
|
|
1955
2022
|
|
|
1956
|
-
self.
|
|
2023
|
+
self.m_n = np.array(x[Cm:Cs])
|
|
1957
2024
|
|
|
1958
2025
|
self.s_n = np.array(x[Cs:Cw])
|
|
1959
2026
|
|
|
1960
2027
|
self.w_ijn = np.array(x[Cw:Cx])
|
|
1961
2028
|
self.w_ijn = self.w_ijn.reshape((Ni, Nj, Nn))
|
|
1962
2029
|
|
|
1963
|
-
self.x_in = np.array(x[Cx:
|
|
2030
|
+
self.x_in = np.array(x[Cx:Czx])
|
|
1964
2031
|
self.x_in = self.x_in.reshape((Ni, Nn))
|
|
1965
2032
|
|
|
1966
|
-
# self.z_inz = np.array(x[
|
|
1967
|
-
# self.z_inz = self.z_inz.reshape((Ni, Nn,
|
|
2033
|
+
# self.z_inz = np.array(x[Czx:])
|
|
2034
|
+
# self.z_inz = self.z_inz.reshape((Ni, Nn, Nzx))
|
|
1968
2035
|
# print(self.z_inz)
|
|
1969
2036
|
|
|
1970
|
-
self.G_n = np.sum(self.
|
|
2037
|
+
self.G_n = np.sum(self.f_tn, axis=0)
|
|
1971
2038
|
|
|
1972
2039
|
tau_0 = np.array(self.tau_kn[0, :])
|
|
1973
2040
|
tau_0[tau_0 < 0] = 0
|
|
@@ -1976,10 +2043,10 @@ class Plan(object):
|
|
|
1976
2043
|
self.Q_n = np.sum(
|
|
1977
2044
|
(
|
|
1978
2045
|
self.mu
|
|
1979
|
-
* (self.b_ijn[:, 0,
|
|
2046
|
+
* (self.b_ijn[:, 0, :Nn] - self.w_ijn[:, 0, :] + self.d_in[:, :] + 0.5 * self.kappa_ijn[:, 0, :Nn])
|
|
1980
2047
|
+ tau_0prev * self.w_ijn[:, 0, :]
|
|
1981
2048
|
)
|
|
1982
|
-
* self.alpha_ijkn[:, 0, 0,
|
|
2049
|
+
* self.alpha_ijkn[:, 0, 0, :Nn],
|
|
1983
2050
|
axis=0,
|
|
1984
2051
|
)
|
|
1985
2052
|
self.U_n = self.psi_n * self.Q_n
|
|
@@ -1987,14 +2054,14 @@ class Plan(object):
|
|
|
1987
2054
|
self.MAGI_n = self.G_n + self.e_n + self.Q_n
|
|
1988
2055
|
|
|
1989
2056
|
I_in = ((self.b_ijn[:, 0, :-1] + self.d_in - self.w_ijn[:, 0, :])
|
|
1990
|
-
* np.sum(self.alpha_ijkn[:, 0, 1:,
|
|
2057
|
+
* np.sum(self.alpha_ijkn[:, 0, 1:, :Nn] * self.tau_kn[1:, :], axis=1))
|
|
1991
2058
|
self.I_n = np.sum(I_in, axis=0)
|
|
1992
2059
|
|
|
1993
2060
|
# Stop after building minimu required for self-consistent loop.
|
|
1994
2061
|
if short:
|
|
1995
2062
|
return
|
|
1996
2063
|
|
|
1997
|
-
self.T_tn = self.
|
|
2064
|
+
self.T_tn = self.f_tn * self.theta_tn
|
|
1998
2065
|
self.T_n = np.sum(self.T_tn, axis=0)
|
|
1999
2066
|
self.P_n = np.zeros(Nn)
|
|
2000
2067
|
# Add early withdrawal penalty if any.
|
|
@@ -2170,8 +2237,8 @@ class Plan(object):
|
|
|
2170
2237
|
dic[" Total net investment income tax paid"] = f"{u.d(taxPaidNow)}"
|
|
2171
2238
|
dic["[Total net investment income tax paid]"] = f"{u.d(taxPaid)}"
|
|
2172
2239
|
|
|
2173
|
-
taxPaid = np.sum(self.M_n, axis=0)
|
|
2174
|
-
taxPaidNow = np.sum(self.M_n / self.gamma_n[:-1], axis=0)
|
|
2240
|
+
taxPaid = np.sum(self.m_n + self.M_n, axis=0)
|
|
2241
|
+
taxPaidNow = np.sum((self.m_n + self.M_n) / self.gamma_n[:-1], axis=0)
|
|
2175
2242
|
dic[" Total Medicare premiums paid"] = f"{u.d(taxPaidNow)}"
|
|
2176
2243
|
dic["[Total Medicare premiums paid]"] = f"{u.d(taxPaid)}"
|
|
2177
2244
|
|
|
@@ -2313,7 +2380,7 @@ class Plan(object):
|
|
|
2313
2380
|
The value parameter can be set to *nominal* or *today*, overriding
|
|
2314
2381
|
the default behavior of setDefaultPlots().
|
|
2315
2382
|
"""
|
|
2316
|
-
value = self.
|
|
2383
|
+
value = self._checkValueType(value)
|
|
2317
2384
|
title = self._name + "\nNet Available Spending"
|
|
2318
2385
|
if tag:
|
|
2319
2386
|
title += " - " + tag
|
|
@@ -2338,7 +2405,7 @@ class Plan(object):
|
|
|
2338
2405
|
The value parameter can be set to *nominal* or *today*, overriding
|
|
2339
2406
|
the default behavior of setDefaultPlots().
|
|
2340
2407
|
"""
|
|
2341
|
-
value = self.
|
|
2408
|
+
value = self._checkValueType(value)
|
|
2342
2409
|
figures = self._plotter.plot_asset_composition(self.year_n, self.inames, self.b_ijkn,
|
|
2343
2410
|
self.gamma_n, value, self._name, tag)
|
|
2344
2411
|
if figure:
|
|
@@ -2358,8 +2425,8 @@ class Plan(object):
|
|
|
2358
2425
|
The value parameter can be set to *nominal* or *today*, overriding
|
|
2359
2426
|
the default behavior of setDefaultPlots().
|
|
2360
2427
|
"""
|
|
2361
|
-
value = self.
|
|
2362
|
-
tax_brackets = tx.taxBrackets(self.N_i, self.n_d, self.N_n, self.
|
|
2428
|
+
value = self._checkValueType(value)
|
|
2429
|
+
tax_brackets = tx.taxBrackets(self.N_i, self.n_d, self.N_n, self.yOBBBA)
|
|
2363
2430
|
title = self._name + "\nTaxable Ordinary Income vs. Tax Brackets"
|
|
2364
2431
|
if tag:
|
|
2365
2432
|
title += " - " + tag
|
|
@@ -2402,7 +2469,7 @@ class Plan(object):
|
|
|
2402
2469
|
The value parameter can be set to *nominal* or *today*, overriding
|
|
2403
2470
|
the default behavior of setDefaultPlots().
|
|
2404
2471
|
"""
|
|
2405
|
-
value = self.
|
|
2472
|
+
value = self._checkValueType(value)
|
|
2406
2473
|
title = self._name + "\nSavings Balance"
|
|
2407
2474
|
if tag:
|
|
2408
2475
|
title += " - " + tag
|
|
@@ -2424,7 +2491,7 @@ class Plan(object):
|
|
|
2424
2491
|
The value parameter can be set to *nominal* or *today*, overriding
|
|
2425
2492
|
the default behavior of setDefaultPlots().
|
|
2426
2493
|
"""
|
|
2427
|
-
value = self.
|
|
2494
|
+
value = self._checkValueType(value)
|
|
2428
2495
|
title = self._name + "\nRaw Income Sources"
|
|
2429
2496
|
if tag:
|
|
2430
2497
|
title += " - " + tag
|
|
@@ -2446,13 +2513,13 @@ class Plan(object):
|
|
|
2446
2513
|
The value parameter can be set to *nominal* or *today*, overriding
|
|
2447
2514
|
the default behavior of setDefaultPlots().
|
|
2448
2515
|
"""
|
|
2449
|
-
value = self.
|
|
2516
|
+
value = self._checkValueType(value)
|
|
2450
2517
|
title = self._name + "\nFederal Income Tax"
|
|
2451
2518
|
if tag:
|
|
2452
2519
|
title += " - " + tag
|
|
2453
2520
|
# All taxes: ordinary income, dividends, and NIIT.
|
|
2454
2521
|
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,
|
|
2522
|
+
fig = self._plotter.plot_taxes(self.year_n, allTaxes, self.m_n + self.M_n, self.gamma_n,
|
|
2456
2523
|
value, title, self.inames)
|
|
2457
2524
|
if figure:
|
|
2458
2525
|
return fig
|
|
@@ -2523,7 +2590,7 @@ class Plan(object):
|
|
|
2523
2590
|
"net spending": self.g_n,
|
|
2524
2591
|
"taxable ord. income": self.G_n,
|
|
2525
2592
|
"taxable gains/divs": self.Q_n,
|
|
2526
|
-
"Tax bills + Med.": self.T_n + self.U_n + self.M_n + self.J_n,
|
|
2593
|
+
"Tax bills + Med.": self.T_n + self.U_n + self.m_n + self.M_n + self.J_n,
|
|
2527
2594
|
}
|
|
2528
2595
|
|
|
2529
2596
|
fillsheet(ws, incomeDic, "currency")
|
|
@@ -2539,7 +2606,7 @@ class Plan(object):
|
|
|
2539
2606
|
"all deposits": -np.sum(self.d_in, axis=0),
|
|
2540
2607
|
"ord taxes": -self.T_n - self.J_n,
|
|
2541
2608
|
"div taxes": -self.U_n,
|
|
2542
|
-
"Medicare": -self.M_n,
|
|
2609
|
+
"Medicare": -self.m_n - self.M_n,
|
|
2543
2610
|
}
|
|
2544
2611
|
sname = "Cash Flow"
|
|
2545
2612
|
ws = wb.create_sheet(sname)
|
|
@@ -2726,7 +2793,7 @@ def _saveWorkbook(wb, basename, overwrite, mylog):
|
|
|
2726
2793
|
else:
|
|
2727
2794
|
fname = basename
|
|
2728
2795
|
|
|
2729
|
-
if overwrite
|
|
2796
|
+
if not overwrite and isfile(fname):
|
|
2730
2797
|
mylog.print(f'File "{fname}" already exists.')
|
|
2731
2798
|
key = input("Overwrite? [Ny] ")
|
|
2732
2799
|
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.09.15"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: owlplanner
|
|
3
|
-
Version: 2025.
|
|
3
|
+
Version: 2025.9.15
|
|
4
4
|
Summary: Owl: Retirement planner with great wisdom
|
|
5
5
|
Project-URL: HomePage, https://github.com/mdlacasse/owl
|
|
6
6
|
Project-URL: Repository, https://github.com/mdlacasse/owl
|
|
@@ -744,7 +744,7 @@ Look at *Basic capabilities* below for more detail.
|
|
|
744
744
|
One can certainly have a savings plan, but due to the volatility of financial investments,
|
|
745
745
|
it is impossible to have a certain asset earnings plan. This does not mean one cannot make decisions.
|
|
746
746
|
These decisions need to be guided with an understanding of the sensitivity of the parameters.
|
|
747
|
-
This is exactly where this tool fits
|
|
747
|
+
This is exactly where this tool fits in. Given your savings capabilities and spending desires,
|
|
748
748
|
it can generate different future realizations of
|
|
749
749
|
your strategy under different market assumptions, helping to better understand your financial situation.
|
|
750
750
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
owlplanner/__init__.py,sha256=hJ2i4m2JpHPAKyQLjYOXpJzeEsgcTcKD-Vhm0AIjjWg,592
|
|
2
|
+
owlplanner/abcapi.py,sha256=rtg7d0UbftinokR9VlB49VUjDjzUq3ONnJbhMXVIrgo,6879
|
|
3
|
+
owlplanner/config.py,sha256=UF2Dy6E9PiX6Ua8B1R0aYCNUoIYmY46up8awf_36B_Q,12615
|
|
4
|
+
owlplanner/mylogging.py,sha256=OVGeDFO7LIZG91R6HMpZBzjno-B8PH8Fo00Jw2Pdgqw,2558
|
|
5
|
+
owlplanner/plan.py,sha256=pIKULy5GFo_8xOZByZ9_CXrHx9TcXQpaob8cblK46N8,115180
|
|
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=zy-IhWuHFT4sXG7yuvz7mQvMfgQ6BwWFA7KrAtVvwYg,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.9.15.dist-info/METADATA,sha256=yacvHpuuPAtZ4fvLHH2UcbIdtkWwgVc0zwIvtOBNw64,54045
|
|
20
|
+
owlplanner-2025.9.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
+
owlplanner-2025.9.15.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
22
|
+
owlplanner-2025.9.15.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
|