owlplanner 2025.5.30__py3-none-any.whl → 2025.6.21__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 +2 -2
- owlplanner/config.py +3 -2
- owlplanner/mylogging.py +2 -2
- owlplanner/plan.py +82 -38
- owlplanner/plotting/__init__.py +5 -0
- owlplanner/plotting/base.py +5 -0
- owlplanner/plotting/factory.py +5 -0
- owlplanner/plotting/matplotlib_backend.py +10 -3
- owlplanner/plotting/plotly_backend.py +8 -2
- owlplanner/progress.py +4 -0
- owlplanner/rates.py +2 -3
- owlplanner/tax2025.py +5 -4
- owlplanner/timelists.py +13 -12
- owlplanner/utils.py +2 -2
- owlplanner/version.py +1 -1
- {owlplanner-2025.5.30.dist-info → owlplanner-2025.6.21.dist-info}/METADATA +9 -12
- owlplanner-2025.6.21.dist-info/RECORD +22 -0
- owlplanner-2025.5.30.dist-info/RECORD +0 -22
- {owlplanner-2025.5.30.dist-info → owlplanner-2025.6.21.dist-info}/WHEEL +0 -0
- {owlplanner-2025.5.30.dist-info → owlplanner-2025.6.21.dist-info}/licenses/LICENSE +0 -0
owlplanner/abcapi.py
CHANGED
|
@@ -16,9 +16,9 @@ solvers for comparison.
|
|
|
16
16
|
This approach has been successful with the MOSEK and the HiGHS solvers.
|
|
17
17
|
A for matrix, B for bounds, C for constraints. Thus the name ABCAPI.
|
|
18
18
|
|
|
19
|
-
Copyright
|
|
19
|
+
Copyright © 2024 - Martin-D. Lacasse
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
22
22
|
|
|
23
23
|
"""
|
|
24
24
|
|
owlplanner/config.py
CHANGED
|
@@ -4,9 +4,10 @@ Owl/conftoml
|
|
|
4
4
|
|
|
5
5
|
This file contains utility functions to save case parameters.
|
|
6
6
|
|
|
7
|
-
Copyright
|
|
7
|
+
Copyright © 2024 - Martin-D. Lacasse
|
|
8
|
+
|
|
9
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
8
10
|
|
|
9
|
-
Disclaimer: This program comes with no guarantee. Use at your own risk.
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
13
|
import toml as toml
|
owlplanner/mylogging.py
CHANGED
|
@@ -4,9 +4,9 @@ Owl/logging
|
|
|
4
4
|
|
|
5
5
|
This file contains routines for handling error messages.
|
|
6
6
|
|
|
7
|
-
Copyright
|
|
7
|
+
Copyright © 2024 - Martin-D. Lacasse
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
10
10
|
|
|
11
11
|
"""
|
|
12
12
|
|
owlplanner/plan.py
CHANGED
|
@@ -8,9 +8,10 @@ A retirement planner using linear programming optimization.
|
|
|
8
8
|
See companion PDF document for an explanation of the underlying
|
|
9
9
|
mathematical model and a description of all variables and parameters.
|
|
10
10
|
|
|
11
|
-
Copyright
|
|
11
|
+
Copyright © 2024 - Martin-D. Lacasse
|
|
12
|
+
|
|
13
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
12
14
|
|
|
13
|
-
Disclaimer: This program comes with no guarantee. Use at your own risk.
|
|
14
15
|
"""
|
|
15
16
|
|
|
16
17
|
###########################################################################
|
|
@@ -303,11 +304,11 @@ class Plan(object):
|
|
|
303
304
|
# Parameters from timeLists initialized to zero.
|
|
304
305
|
self.omega_in = np.zeros((self.N_i, self.N_n))
|
|
305
306
|
self.Lambda_in = np.zeros((self.N_i, self.N_n))
|
|
306
|
-
self.myRothX_in = np.zeros((self.N_i, self.N_n))
|
|
307
|
-
self.kappa_ijn = np.zeros((self.N_i, self.N_j, self.N_n))
|
|
307
|
+
self.myRothX_in = np.zeros((self.N_i, self.N_n + 5))
|
|
308
|
+
self.kappa_ijn = np.zeros((self.N_i, self.N_j, self.N_n + 5))
|
|
308
309
|
|
|
309
310
|
# Previous 3 years for Medicare.
|
|
310
|
-
self.prevMAGI = np.zeros((
|
|
311
|
+
self.prevMAGI = np.zeros((2))
|
|
311
312
|
|
|
312
313
|
# Init previous balance to none.
|
|
313
314
|
self.beta_ij = None
|
|
@@ -498,7 +499,7 @@ class Plan(object):
|
|
|
498
499
|
def setBeneficiaryFractions(self, phi):
|
|
499
500
|
"""
|
|
500
501
|
Set fractions of savings accounts that is left to surviving spouse.
|
|
501
|
-
Default is [1, 1, 1] for taxable, tax-deferred,
|
|
502
|
+
Default is [1, 1, 1] for taxable, tax-deferred, and tax-free accounts.
|
|
502
503
|
"""
|
|
503
504
|
if len(phi) != self.N_j:
|
|
504
505
|
raise ValueError(f"Fractions must have {self.N_j} entries.")
|
|
@@ -901,7 +902,7 @@ class Plan(object):
|
|
|
901
902
|
try:
|
|
902
903
|
filename, self.timeLists = timelists.read(filename, self.inames, self.horizons, self.mylog)
|
|
903
904
|
except Exception as e:
|
|
904
|
-
raise Exception(f"Unsuccessful read of
|
|
905
|
+
raise Exception(f"Unsuccessful read of Wages and Contributions: {e}") from e
|
|
905
906
|
|
|
906
907
|
self.timeListsFileName = filename
|
|
907
908
|
self.setContributions()
|
|
@@ -909,6 +910,9 @@ class Plan(object):
|
|
|
909
910
|
return True
|
|
910
911
|
|
|
911
912
|
def setContributions(self, timeLists=None):
|
|
913
|
+
"""
|
|
914
|
+
If no argument is given, use the values that have been stored in self.timeLists.
|
|
915
|
+
"""
|
|
912
916
|
if timeLists is not None:
|
|
913
917
|
timelists.check(timeLists, self.inames, self.horizons)
|
|
914
918
|
self.timeLists = timeLists
|
|
@@ -916,14 +920,17 @@ class Plan(object):
|
|
|
916
920
|
# Now fill in parameters which are in $.
|
|
917
921
|
for i, iname in enumerate(self.inames):
|
|
918
922
|
h = self.horizons[i]
|
|
919
|
-
self.omega_in[i, :h] = self.timeLists[iname]["anticipated wages"].iloc[:h]
|
|
920
|
-
self.
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
self.kappa_ijn[i,
|
|
925
|
-
self.
|
|
926
|
-
self.
|
|
923
|
+
self.omega_in[i, :h] = self.timeLists[iname]["anticipated wages"].iloc[5:5+h]
|
|
924
|
+
self.Lambda_in[i, :h] = self.timeLists[iname]["big-ticket items"].iloc[5:5+h]
|
|
925
|
+
|
|
926
|
+
# Values for last 5 years of Roth conversion and contributions stored at the end
|
|
927
|
+
# of array and accessed with negative index.
|
|
928
|
+
self.kappa_ijn[i, 0, :h+5] = np.roll(self.timeLists[iname]["taxable ctrb"], -5)
|
|
929
|
+
self.kappa_ijn[i, 1, :h+5] = np.roll(self.timeLists[iname]["401k ctrb"], -5)
|
|
930
|
+
self.kappa_ijn[i, 1, :h+5] += np.roll(self.timeLists[iname]["IRA ctrb"], -5)
|
|
931
|
+
self.kappa_ijn[i, 2, :h+5] = np.roll(self.timeLists[iname]["Roth 401k ctrb"], -5)
|
|
932
|
+
self.kappa_ijn[i, 2, :h+5] += np.roll(self.timeLists[iname]["Roth IRA ctrb"], -5)
|
|
933
|
+
self.myRothX_in[i, :h+5] = np.roll(self.timeLists[iname]["Roth conv"], -5)
|
|
927
934
|
|
|
928
935
|
self.caseStatus = "modified"
|
|
929
936
|
|
|
@@ -980,8 +987,9 @@ class Plan(object):
|
|
|
980
987
|
]
|
|
981
988
|
for i, iname in enumerate(self.inames):
|
|
982
989
|
h = self.horizons[i]
|
|
983
|
-
df = pd.DataFrame(0, index=np.arange(h), columns=cols)
|
|
984
|
-
df["year"] = self.year_n[:h]
|
|
990
|
+
df = pd.DataFrame(0, index=np.arange(0, h+5), columns=cols)
|
|
991
|
+
# df["year"] = self.year_n[:h]
|
|
992
|
+
df["year"] = np.arange(self.year_n[0] - 5, self.year_n[h-1]+1)
|
|
985
993
|
self.timeLists[iname] = df
|
|
986
994
|
|
|
987
995
|
self.caseStatus = "modified"
|
|
@@ -1074,8 +1082,6 @@ class Plan(object):
|
|
|
1074
1082
|
Utility function that builds constraint matrix and vectors.
|
|
1075
1083
|
Refactored for clarity and maintainability.
|
|
1076
1084
|
"""
|
|
1077
|
-
self._setup_constraint_shortcuts(options)
|
|
1078
|
-
|
|
1079
1085
|
self.A = abc.ConstraintMatrix(self.nvars)
|
|
1080
1086
|
self.B = abc.Bounds(self.nvars, self.nbins)
|
|
1081
1087
|
|
|
@@ -1084,6 +1090,7 @@ class Plan(object):
|
|
|
1084
1090
|
self._add_standard_exemption_bounds()
|
|
1085
1091
|
self._add_defunct_constraints()
|
|
1086
1092
|
self._add_roth_conversion_constraints(options)
|
|
1093
|
+
self._add_roth_maturation_constraints()
|
|
1087
1094
|
self._add_withdrawal_limits()
|
|
1088
1095
|
self._add_conversion_limits()
|
|
1089
1096
|
self._add_objective_constraints(objective, options)
|
|
@@ -1098,12 +1105,6 @@ class Plan(object):
|
|
|
1098
1105
|
|
|
1099
1106
|
return None
|
|
1100
1107
|
|
|
1101
|
-
def _setup_constraint_shortcuts(self, options):
|
|
1102
|
-
# Set up all the local variables as attributes for use in helpers.
|
|
1103
|
-
oppCostX = options.get("oppCostX", 0.)
|
|
1104
|
-
self.xnet = 1 - oppCostX / 100.
|
|
1105
|
-
self.optionsUnits = u.getUnits(options.get("units", "k"))
|
|
1106
|
-
|
|
1107
1108
|
def _add_rmd_inequalities(self):
|
|
1108
1109
|
for i in range(self.N_i):
|
|
1109
1110
|
if self.beta_ij[i, 1] > 0:
|
|
@@ -1131,6 +1132,45 @@ class Plan(object):
|
|
|
1131
1132
|
for j in range(self.N_j):
|
|
1132
1133
|
self.B.setRange(_q3(self.C["w"], self.i_d, j, n, self.N_i, self.N_j, self.N_n), 0, 0)
|
|
1133
1134
|
|
|
1135
|
+
def _add_roth_maturation_constraints(self):
|
|
1136
|
+
"""
|
|
1137
|
+
Withdrawals from Roth accounts are subject to the 5-year rule for conversion.
|
|
1138
|
+
Conversions and gains are subject to the 5-year rule since conversion.
|
|
1139
|
+
Contributions can be withdrawn at any time (without 59.5 penalty) but
|
|
1140
|
+
gains on contributions are subject to the 5-year rule since the opening of the account.
|
|
1141
|
+
A retainer is put on all conversions and associated gains, and gains on all recent contributions.
|
|
1142
|
+
"""
|
|
1143
|
+
# Assume 10% per year for contributions and conversions for past 5 years.
|
|
1144
|
+
# Future years will use the assumed returns.
|
|
1145
|
+
oldTau1 = 1.10
|
|
1146
|
+
for i in range(self.N_i):
|
|
1147
|
+
h = self.horizons[i]
|
|
1148
|
+
for n in range(h):
|
|
1149
|
+
rhs = 0
|
|
1150
|
+
# To add compounded gains to original amount.
|
|
1151
|
+
cgains = 1
|
|
1152
|
+
row = self.A.newRow()
|
|
1153
|
+
row.addElem(_q3(self.C["b"], i, 2, n, self.N_i, self.N_j, self.N_n + 1), 1)
|
|
1154
|
+
row.addElem(_q3(self.C["w"], i, 2, n, self.N_i, self.N_j, self.N_n), -1)
|
|
1155
|
+
for dn in range(1, 6):
|
|
1156
|
+
nn = n - dn
|
|
1157
|
+
if nn < 0: # Past of future is in the past:
|
|
1158
|
+
# Parameters are stored at the end of contributions and conversions arrays.
|
|
1159
|
+
cgains *= oldTau1
|
|
1160
|
+
# If only an contribution - without conversion.
|
|
1161
|
+
# rhs += (cgains - 1) * self.kappa_ijn[i, 2, nn] + cgains * self.myRothX_in[i, nn]
|
|
1162
|
+
rhs += cgains * self.kappa_ijn[i, 2, nn] + cgains * self.myRothX_in[i, nn]
|
|
1163
|
+
else: # Past of future is in the future: use variables and parameters.
|
|
1164
|
+
ksum2 = np.sum(self.alpha_ijkn[i, 2, :, nn] * self.tau_kn[:, nn], axis=0)
|
|
1165
|
+
Tau1 = 1 + ksum2
|
|
1166
|
+
cgains *= Tau1
|
|
1167
|
+
row.addElem(_q2(self.C["x"], i, nn, self.N_i, self.N_n), -cgains)
|
|
1168
|
+
# If only a contribution - without conversion.
|
|
1169
|
+
# rhs += (cgains - 1) * self.kappa_ijn[i, 2, nn]
|
|
1170
|
+
rhs += cgains * self.kappa_ijn[i, 2, nn]
|
|
1171
|
+
|
|
1172
|
+
self.A.addRow(row, rhs, np.inf)
|
|
1173
|
+
|
|
1134
1174
|
def _add_roth_conversion_constraints(self, options):
|
|
1135
1175
|
if "maxRothConversion" in options and options["maxRothConversion"] == "file":
|
|
1136
1176
|
for i in range(self.N_i):
|
|
@@ -1579,14 +1619,18 @@ class Plan(object):
|
|
|
1579
1619
|
if objective == "maxSpending" and "bequest" not in myoptions:
|
|
1580
1620
|
self.mylog.vprint("Using bequest of $1.")
|
|
1581
1621
|
|
|
1582
|
-
self.
|
|
1622
|
+
self.optionsUnits = u.getUnits(myoptions.get("units", "k"))
|
|
1623
|
+
|
|
1624
|
+
oppCostX = options.get("oppCostX", 0.)
|
|
1625
|
+
self.xnet = 1 - oppCostX / 100.
|
|
1626
|
+
|
|
1627
|
+
self.prevMAGI = np.zeros(2)
|
|
1583
1628
|
if "previousMAGIs" in myoptions:
|
|
1584
1629
|
magi = myoptions["previousMAGIs"]
|
|
1585
|
-
if len(magi)
|
|
1586
|
-
raise ValueError("previousMAGIs must have
|
|
1630
|
+
if 3 < len(magi) < 2:
|
|
1631
|
+
raise ValueError("previousMAGIs must have 2 values.")
|
|
1587
1632
|
|
|
1588
|
-
|
|
1589
|
-
self.prevMAGI = units * np.array(magi)
|
|
1633
|
+
self.prevMAGI = self.optionsUnits * np.array(magi)
|
|
1590
1634
|
|
|
1591
1635
|
lambdha = myoptions.get("spendingSlack", 0)
|
|
1592
1636
|
if lambdha < 0 or lambdha > 50:
|
|
@@ -1949,7 +1993,7 @@ class Plan(object):
|
|
|
1949
1993
|
self.Q_n = np.sum(
|
|
1950
1994
|
(
|
|
1951
1995
|
self.mu
|
|
1952
|
-
* (self.b_ijn[:, 0, :-1] - self.w_ijn[:, 0, :] + self.d_in[:, :] + 0.5 * self.kappa_ijn[:, 0, :])
|
|
1996
|
+
* (self.b_ijn[:, 0, :-1] - self.w_ijn[:, 0, :] + self.d_in[:, :] + 0.5 * self.kappa_ijn[:, 0, :Nn])
|
|
1953
1997
|
+ tau_0prev * self.w_ijn[:, 0, :]
|
|
1954
1998
|
)
|
|
1955
1999
|
* self.alpha_ijkn[:, 0, 0, :-1],
|
|
@@ -2058,7 +2102,7 @@ class Plan(object):
|
|
|
2058
2102
|
dic = {}
|
|
2059
2103
|
# Results
|
|
2060
2104
|
dic["Plan name"] = self._name
|
|
2061
|
-
dic["Net yearly spending basis"] = u.d(self.g_n[0] / self.xi_n[0])
|
|
2105
|
+
dic["Net yearly spending basis" + 26*" ."] = u.d(self.g_n[0] / self.xi_n[0])
|
|
2062
2106
|
dic[f"Net spending for year {now}"] = u.d(self.g_n[0])
|
|
2063
2107
|
dic[f"Net spending remaining in year {now}"] = u.d(self.g_n[0] * self.yearFracLeft)
|
|
2064
2108
|
|
|
@@ -2370,7 +2414,7 @@ class Plan(object):
|
|
|
2370
2414
|
the default behavior of setDefaultPlots().
|
|
2371
2415
|
"""
|
|
2372
2416
|
value = self._checkValue(value)
|
|
2373
|
-
title = self._name + "\
|
|
2417
|
+
title = self._name + "\nFederal Income Tax"
|
|
2374
2418
|
if tag:
|
|
2375
2419
|
title += " - " + tag
|
|
2376
2420
|
# All taxes: ordinary income and dividends.
|
|
@@ -2489,16 +2533,16 @@ class Plan(object):
|
|
|
2489
2533
|
# Account balances except final year.
|
|
2490
2534
|
accDic = {
|
|
2491
2535
|
"taxable bal": self.b_ijn[:, 0, :-1],
|
|
2492
|
-
"taxable ctrb": self.kappa_ijn[:, 0, :],
|
|
2536
|
+
"taxable ctrb": self.kappa_ijn[:, 0, :self.N_n],
|
|
2493
2537
|
"taxable dep": self.d_in,
|
|
2494
2538
|
"taxable wdrwl": self.w_ijn[:, 0, :],
|
|
2495
2539
|
"tax-deferred bal": self.b_ijn[:, 1, :-1],
|
|
2496
|
-
"tax-deferred ctrb": self.kappa_ijn[:, 1, :],
|
|
2540
|
+
"tax-deferred ctrb": self.kappa_ijn[:, 1, :self.N_n],
|
|
2497
2541
|
"tax-deferred wdrwl": self.w_ijn[:, 1, :],
|
|
2498
2542
|
"(included RMDs)": self.rmd_in[:, :],
|
|
2499
2543
|
"Roth conv": self.x_in,
|
|
2500
2544
|
"tax-free bal": self.b_ijn[:, 2, :-1],
|
|
2501
|
-
"tax-free ctrb": self.kappa_ijn[:, 2, :],
|
|
2545
|
+
"tax-free ctrb": self.kappa_ijn[:, 2, :self.N_n],
|
|
2502
2546
|
"tax-free wdrwl": self.w_ijn[:, 2, :],
|
|
2503
2547
|
}
|
|
2504
2548
|
for i in range(self.N_i):
|
|
@@ -2595,12 +2639,12 @@ class Plan(object):
|
|
|
2595
2639
|
planData[self.inames[i] + " txbl dep"] = self.d_in[i, :]
|
|
2596
2640
|
planData[self.inames[i] + " txbl wrdwl"] = self.w_ijn[i, 0, :]
|
|
2597
2641
|
planData[self.inames[i] + " tx-def bal"] = self.b_ijn[i, 1, :-1]
|
|
2598
|
-
planData[self.inames[i] + " tx-def ctrb"] = self.kappa_ijn[i, 1, :]
|
|
2642
|
+
planData[self.inames[i] + " tx-def ctrb"] = self.kappa_ijn[i, 1, :self.N_n]
|
|
2599
2643
|
planData[self.inames[i] + " tx-def wdrl"] = self.w_ijn[i, 1, :]
|
|
2600
2644
|
planData[self.inames[i] + " (RMD)"] = self.rmd_in[i, :]
|
|
2601
2645
|
planData[self.inames[i] + " Roth conv"] = self.x_in[i, :]
|
|
2602
2646
|
planData[self.inames[i] + " tx-free bal"] = self.b_ijn[i, 2, :-1]
|
|
2603
|
-
planData[self.inames[i] + " tx-free ctrb"] = self.kappa_ijn[i, 2, :]
|
|
2647
|
+
planData[self.inames[i] + " tx-free ctrb"] = self.kappa_ijn[i, 2, :self.N_n]
|
|
2604
2648
|
planData[self.inames[i] + " tax-free wdrwl"] = self.w_ijn[i, 2, :]
|
|
2605
2649
|
planData[self.inames[i] + " big-ticket items"] = self.Lambda_in[i, :]
|
|
2606
2650
|
|
owlplanner/plotting/__init__.py
CHANGED
owlplanner/plotting/base.py
CHANGED
owlplanner/plotting/factory.py
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Matplotlib implementation of plot backend.
|
|
3
|
+
|
|
4
|
+
Copyright © 2025 - Martin-D. Lacasse
|
|
5
|
+
|
|
6
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
7
|
+
|
|
3
8
|
"""
|
|
4
9
|
|
|
5
10
|
import numpy as np
|
|
@@ -372,17 +377,19 @@ class MatplotlibBackend(PlotBackend):
|
|
|
372
377
|
raise ValueError(f"Unknown coordination {ARCoord}.")
|
|
373
378
|
figures = []
|
|
374
379
|
assetDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
|
|
380
|
+
blank = ["", ""]
|
|
375
381
|
for i in range(count):
|
|
376
382
|
y2stack = {}
|
|
377
383
|
for acType in acList:
|
|
378
384
|
stackNames = []
|
|
379
385
|
for key in assetDic:
|
|
380
|
-
aname = key + " / " + acType
|
|
386
|
+
# aname = key + " / " + acType
|
|
387
|
+
aname = key
|
|
381
388
|
stackNames.append(aname)
|
|
382
389
|
y2stack[aname] = np.zeros((count, len(year_n)))
|
|
383
390
|
y2stack[aname][i][:] = alpha_ijkn[i, acList.index(acType), assetDic[key], : len(year_n)]
|
|
384
|
-
t = title + f" - {acType}"
|
|
385
|
-
fig, ax = self._stack_plot(year_n,
|
|
391
|
+
t = title + f" - {acType} {inames[i]}"
|
|
392
|
+
fig, ax = self._stack_plot(year_n, blank, t, [i], y2stack, stackNames, "upper left", "percent")
|
|
386
393
|
figures.append(fig)
|
|
387
394
|
|
|
388
395
|
return figures
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Plotly implementation of plot backend.
|
|
3
|
+
|
|
4
|
+
Copyright © 2025 - Martin-D. Lacasse
|
|
5
|
+
|
|
6
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
7
|
+
|
|
3
8
|
"""
|
|
4
9
|
|
|
5
10
|
import numpy as np
|
|
@@ -815,7 +820,8 @@ class PlotlyBackend(PlotBackend):
|
|
|
815
820
|
stack_data = []
|
|
816
821
|
stack_names = []
|
|
817
822
|
for key in assetDic:
|
|
818
|
-
aname = f"{key} / {acType}"
|
|
823
|
+
# aname = f"{key} / {acType}"
|
|
824
|
+
aname = key
|
|
819
825
|
stack_names.append(aname)
|
|
820
826
|
|
|
821
827
|
# Get allocation data
|
|
@@ -834,7 +840,7 @@ class PlotlyBackend(PlotBackend):
|
|
|
834
840
|
))
|
|
835
841
|
|
|
836
842
|
# Update layout
|
|
837
|
-
plot_title = f"{title} - {acType}"
|
|
843
|
+
plot_title = f"{title} - {acType} {inames[i]}"
|
|
838
844
|
fig.update_layout(
|
|
839
845
|
title=plot_title,
|
|
840
846
|
# xaxis_title="year",
|
owlplanner/progress.py
CHANGED
owlplanner/rates.py
CHANGED
|
@@ -21,11 +21,10 @@ Rate lists will need to be updated with values for current year.
|
|
|
21
21
|
When doing so, the TO bound defined below will need to be adjusted
|
|
22
22
|
to the last current data year.
|
|
23
23
|
|
|
24
|
-
Copyright
|
|
24
|
+
Copyright © 2024 - Martin-D. Lacasse
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
27
27
|
|
|
28
|
-
Disclaimer: This program comes with no guarantee. Use at your own risk.
|
|
29
28
|
"""
|
|
30
29
|
|
|
31
30
|
###################################################################
|
owlplanner/tax2025.py
CHANGED
|
@@ -10,9 +10,10 @@ of all variables and parameters.
|
|
|
10
10
|
|
|
11
11
|
Module to handle all tax calculations.
|
|
12
12
|
|
|
13
|
-
Copyright
|
|
13
|
+
Copyright © 2024 - Martin-D. Lacasse
|
|
14
|
+
|
|
15
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
14
16
|
|
|
15
|
-
Disclaimer: This program comes with no guarantee. Use at your own risk.
|
|
16
17
|
"""
|
|
17
18
|
|
|
18
19
|
import numpy as np
|
|
@@ -93,9 +94,9 @@ def mediCosts(yobs, horizons, magi, prevmagi, gamma_n, Nn):
|
|
|
93
94
|
status = 0 if Ni == 1 else 1 if n < horizons[0] and n < horizons[1] else 0
|
|
94
95
|
for i in range(Ni):
|
|
95
96
|
if thisyear + n - yobs[i] >= 65 and n < horizons[i]:
|
|
96
|
-
# Start with the (
|
|
97
|
+
# Start with the (inflation-adjusted) basic Medicare part B premium.
|
|
97
98
|
costs[n] += gamma_n[n] * irmaaFees[0]
|
|
98
|
-
if n <
|
|
99
|
+
if n < 2:
|
|
99
100
|
mymagi = prevmagi[n]
|
|
100
101
|
else:
|
|
101
102
|
mymagi = magi[n - 2]
|
owlplanner/timelists.py
CHANGED
|
@@ -10,9 +10,10 @@ of all variables and parameters.
|
|
|
10
10
|
|
|
11
11
|
Utility functions to read and check timelists.
|
|
12
12
|
|
|
13
|
-
Copyright
|
|
13
|
+
Copyright © 2024 - Martin-D. Lacasse
|
|
14
|
+
|
|
15
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
14
16
|
|
|
15
|
-
Disclaimer: This program comes with no guarantee. Use at your own risk.
|
|
16
17
|
"""
|
|
17
18
|
|
|
18
19
|
from datetime import date
|
|
@@ -20,7 +21,7 @@ import pandas as pd
|
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
# Expected headers in each excel sheet, one per individual.
|
|
23
|
-
|
|
24
|
+
_timeHorizonItems = [
|
|
24
25
|
"year",
|
|
25
26
|
"anticipated wages",
|
|
26
27
|
"taxable ctrb",
|
|
@@ -47,7 +48,7 @@ def read(finput, inames, horizons, mylog):
|
|
|
47
48
|
mylog.vprint("Reading wages, contributions, conversions, and big-ticket items over time...")
|
|
48
49
|
|
|
49
50
|
if isinstance(finput, dict):
|
|
50
|
-
|
|
51
|
+
dfDict = finput
|
|
51
52
|
finput = "dictionary of DataFrames"
|
|
52
53
|
streamName = "dictionary of DataFrames"
|
|
53
54
|
else:
|
|
@@ -58,14 +59,14 @@ def read(finput, inames, horizons, mylog):
|
|
|
58
59
|
raise Exception(f"Could not read file {finput}: {e}.") from e
|
|
59
60
|
streamName = f"file '{finput}'"
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
timeLists = _condition(dfDict, inames, horizons, mylog)
|
|
62
63
|
|
|
63
64
|
mylog.vprint(f"Successfully read time horizons from {streamName}.")
|
|
64
65
|
|
|
65
66
|
return finput, timeLists
|
|
66
67
|
|
|
67
68
|
|
|
68
|
-
def
|
|
69
|
+
def _condition(dfDict, inames, horizons, mylog):
|
|
69
70
|
"""
|
|
70
71
|
Make sure that time horizons contain all years up to life expectancy,
|
|
71
72
|
and that values are positive (except big-ticket items).
|
|
@@ -82,24 +83,24 @@ def condition(dfDict, inames, horizons, mylog):
|
|
|
82
83
|
|
|
83
84
|
df = df.loc[:, ~df.columns.str.contains("^Unnamed")]
|
|
84
85
|
for col in df.columns:
|
|
85
|
-
if col == "" or col not in
|
|
86
|
+
if col == "" or col not in _timeHorizonItems:
|
|
86
87
|
df.drop(col, axis=1, inplace=True)
|
|
87
88
|
|
|
88
|
-
for item in
|
|
89
|
+
for item in _timeHorizonItems:
|
|
89
90
|
if item not in df.columns:
|
|
90
91
|
raise ValueError(f"Item {item} not found for {iname}.")
|
|
91
92
|
|
|
92
|
-
# Only consider lines in proper year range.
|
|
93
|
-
df = df[df["year"] >= thisyear]
|
|
93
|
+
# Only consider lines in proper year range. Go back 5 years for Roth maturation.
|
|
94
|
+
df = df[df["year"] >= (thisyear - 5)]
|
|
94
95
|
df = df[df["year"] < endyear]
|
|
95
96
|
missing = []
|
|
96
|
-
for n in range(horizons[i]):
|
|
97
|
+
for n in range(-5, horizons[i]):
|
|
97
98
|
year = thisyear + n
|
|
98
99
|
if not (df[df["year"] == year]).any(axis=None):
|
|
99
100
|
df.loc[len(df)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
|
|
100
101
|
missing.append(year)
|
|
101
102
|
else:
|
|
102
|
-
for item in
|
|
103
|
+
for item in _timeHorizonItems:
|
|
103
104
|
if item != "big-ticket items" and df[item].iloc[n] < 0:
|
|
104
105
|
raise ValueError(f"Item {item} for {iname} in year {df['year'].iloc[n]} is < 0.")
|
|
105
106
|
|
owlplanner/utils.py
CHANGED
|
@@ -4,9 +4,9 @@ Owl/utils
|
|
|
4
4
|
|
|
5
5
|
This file contains functions for handling data.
|
|
6
6
|
|
|
7
|
-
Copyright
|
|
7
|
+
Copyright © 2024 - Martin-D. Lacasse
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Disclaimers: This code is for educatonal 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.06.21"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: owlplanner
|
|
3
|
-
Version: 2025.
|
|
3
|
+
Version: 2025.6.21
|
|
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
|
|
@@ -748,13 +748,6 @@ This is exactly where this tool fits it. Given your savings capabilities and spe
|
|
|
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
|
|
|
751
|
-
Disclaimers: I am not a financial planner. You make your own decisions.
|
|
752
|
-
This program comes with no guarantee. Use at your own risk.
|
|
753
|
-
|
|
754
|
-
More disclaimers: While some output of the code has been verified with other approaches,
|
|
755
|
-
this code is still under development and I cannot guarantee the accuracy of the results.
|
|
756
|
-
Use at your own risk.
|
|
757
|
-
|
|
758
751
|
-------------------------------------------------------------------------------------
|
|
759
752
|
## Purpose and vision
|
|
760
753
|
The goal of Owl is to create a free and open-source ecosystem that has cutting-edge optimization capabilities,
|
|
@@ -839,11 +832,13 @@ an Excel spreadsheet that contains future wages, contributions
|
|
|
839
832
|
to savings accounts, and planned *big-ticket items* such as the purchase of a lake house,
|
|
840
833
|
the sale of a boat, large gifts, or inheritance.
|
|
841
834
|
|
|
842
|
-
Three types of savings accounts are considered: taxable, tax-deferred, and tax-
|
|
835
|
+
Three types of savings accounts are considered: taxable, tax-deferred, and tax-free,
|
|
843
836
|
which are all tracked separately for married individuals. Asset transition to the surviving spouse
|
|
844
837
|
is done according to beneficiary fractions for each type of savings account.
|
|
845
838
|
Tax status covers married filing jointly and single, depending on the number of individuals reported.
|
|
846
839
|
|
|
840
|
+
Maturation rules for Roth contributions and conversions are implemented as constraints
|
|
841
|
+
limiting withdrawal amounts to cover Roth account balances for 5 years after the events.
|
|
847
842
|
Medicare and IRMAA calculations are performed through a self-consistent loop on cash flow constraints.
|
|
848
843
|
Future values are simple projections of current values with the assumed inflation rates.
|
|
849
844
|
|
|
@@ -892,14 +887,16 @@ assets to support, even with no estate being left.
|
|
|
892
887
|
- Streamlit Community Cloud [Streamlit](https://streamlit.io)
|
|
893
888
|
- Contributors: Josh (noimjosh@gmail.com) for Docker image code,
|
|
894
889
|
Dale Seng (sengsational) for great insights and suggestions,
|
|
895
|
-
Robert E. Anderson (NH-RedAnt) for bug fixes and suggestions.
|
|
890
|
+
Robert E. Anderson (NH-RedAnt) for bug fixes and suggestions, Clark Jefcoat (hubcity) for fruitful interactions.
|
|
896
891
|
|
|
897
892
|
---------------------------------------------------------------------
|
|
898
893
|
|
|
899
894
|
Copyright © 2024 - Martin-D. Lacasse
|
|
900
895
|
|
|
901
|
-
Disclaimers:
|
|
902
|
-
|
|
896
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
897
|
+
|
|
898
|
+
Code output has been verified with analytical solutions when applicable, and comparative approaches otherwise.
|
|
899
|
+
Nevertheless, accuracy of results is not guaranteed.
|
|
903
900
|
|
|
904
901
|
--------------------------------------------------------
|
|
905
902
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
owlplanner/__init__.py,sha256=hJ2i4m2JpHPAKyQLjYOXpJzeEsgcTcKD-Vhm0AIjjWg,592
|
|
2
|
+
owlplanner/abcapi.py,sha256=8VCXS7nH_QZYxCUU3lwO0_UPR9Q5fuYQ6DHDLvHVLPg,6878
|
|
3
|
+
owlplanner/config.py,sha256=onGIMqW2WwB9_CUZauDL6LtHGvc8O1cPUKKcK7Oh70M,12617
|
|
4
|
+
owlplanner/mylogging.py,sha256=RKUr-y-1XvKZzLMcfdtm4IM30LuRpJwb2qUeXmAWqME,2557
|
|
5
|
+
owlplanner/plan.py,sha256=VVHyKJiooLXXVLiRFJcauZ3oOYU62CCBe4DlpA08P38,109765
|
|
6
|
+
owlplanner/progress.py,sha256=2DOjOLo6Mo7m21wY-9iZhoUksAyi4VCbb6UL2RegNCw,529
|
|
7
|
+
owlplanner/rates.py,sha256=7jXcuHbkJ3AVIeBYZdwme18rdYslIzCuT-c0cLzvKUU,14819
|
|
8
|
+
owlplanner/tax2025.py,sha256=HVYJq8po28jL5Z_il39ZY7qvf2riUEfxio15Zp7TGj8,7890
|
|
9
|
+
owlplanner/timelists.py,sha256=95rKYknGMi1bonDVIc3xNmiwG0zTSejKyQy_uWCLSiA,4024
|
|
10
|
+
owlplanner/utils.py,sha256=6Ky8ZKfNE9x--3znsZ8VZaT2PptDinszRxWsOCPanu8,2512
|
|
11
|
+
owlplanner/version.py,sha256=Q6lAE5sS9Y-tuy3jNnv43HcB70y7l1kmDPNzx4CR9tc,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.6.21.dist-info/METADATA,sha256=bZI1gHxPSbxJvJP2DuSqiB6Y33x4tdiVgHlKK73wGD4,54045
|
|
20
|
+
owlplanner-2025.6.21.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
+
owlplanner-2025.6.21.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
22
|
+
owlplanner-2025.6.21.dist-info/RECORD,,
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
owlplanner/__init__.py,sha256=hJ2i4m2JpHPAKyQLjYOXpJzeEsgcTcKD-Vhm0AIjjWg,592
|
|
2
|
-
owlplanner/abcapi.py,sha256=m0vtoEzz9HJV7fOK_d7OnK7ha2Qbf7wLLPCJ9YZzR1k,6851
|
|
3
|
-
owlplanner/config.py,sha256=v6T6A_90rVyl4sfX8KLpI8wkzt9HCjUiGDsPS-4VTec,12588
|
|
4
|
-
owlplanner/mylogging.py,sha256=tYMw04O-XYSzjTj36fmKJGLcE1VkK6k6oJNeqtKXzuc,2530
|
|
5
|
-
owlplanner/plan.py,sha256=BnojjOQzzFdcT4dL8EALzc_vzXO2qQJJXjY98nRZIyA,107114
|
|
6
|
-
owlplanner/progress.py,sha256=8jlCvvtgDI89zXVNMBg1-lnEyhpPvKQS2X5oAIpoOVQ,384
|
|
7
|
-
owlplanner/rates.py,sha256=MiaibxJY82JGpAhGyF2BJTm5-rmVAUuG8KLApVQhjvU,14816
|
|
8
|
-
owlplanner/tax2025.py,sha256=wmlZpYeeGNrbyn5g7wOFqhWbggppodtHqc-ex5XRooI,7850
|
|
9
|
-
owlplanner/timelists.py,sha256=wNYnJqxJ6QqE6jHh5lfFqYngfw5wUFrI15LSsM5ae8s,3949
|
|
10
|
-
owlplanner/utils.py,sha256=WpJgn79YZfH8UCkcmhd-AZlxlGuz1i1-UDBRXImsY6I,2485
|
|
11
|
-
owlplanner/version.py,sha256=h-tHOHZjN9oy3hW0oN8p91JT5cGNx7ALbl5_KcaZl3g,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=VnF6ui78YrTrg1dA6hBIdI02ahzEaHVR3ZEdDe_i880,103
|
|
15
|
-
owlplanner/plotting/base.py,sha256=LP1TByl1tO4m087O6VpbZ_TTMnErHJGLTxXZXC9cuKQ,2431
|
|
16
|
-
owlplanner/plotting/factory.py,sha256=i1k8m_ISnJw06f_JWlMvOQ7Q0PgV_BoLm05uLwFPvOQ,883
|
|
17
|
-
owlplanner/plotting/matplotlib_backend.py,sha256=iJm3IBeMA5VUYG_zZxKPIzt4Izv2QWtWvlP656zwJVk,17738
|
|
18
|
-
owlplanner/plotting/plotly_backend.py,sha256=5nqEUJXwLPW1vL9hQijxIUK57sWHvya6ZqIIYof-OjE,32944
|
|
19
|
-
owlplanner-2025.5.30.dist-info/METADATA,sha256=lail2zj8dulkBtJ71340wAUjE3u6LZNNaEjgN4FAo0A,54024
|
|
20
|
-
owlplanner-2025.5.30.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
-
owlplanner-2025.5.30.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
22
|
-
owlplanner-2025.5.30.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|