owlplanner 2025.6.3__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/plan.py +63 -19
- owlplanner/timelists.py +9 -9
- owlplanner/version.py +1 -1
- {owlplanner-2025.6.3.dist-info → owlplanner-2025.6.21.dist-info}/METADATA +6 -4
- {owlplanner-2025.6.3.dist-info → owlplanner-2025.6.21.dist-info}/RECORD +7 -7
- {owlplanner-2025.6.3.dist-info → owlplanner-2025.6.21.dist-info}/WHEEL +0 -0
- {owlplanner-2025.6.3.dist-info → owlplanner-2025.6.21.dist-info}/licenses/LICENSE +0 -0
owlplanner/plan.py
CHANGED
|
@@ -304,8 +304,8 @@ class Plan(object):
|
|
|
304
304
|
# Parameters from timeLists initialized to zero.
|
|
305
305
|
self.omega_in = np.zeros((self.N_i, self.N_n))
|
|
306
306
|
self.Lambda_in = np.zeros((self.N_i, self.N_n))
|
|
307
|
-
self.myRothX_in = np.zeros((self.N_i, self.N_n))
|
|
308
|
-
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))
|
|
309
309
|
|
|
310
310
|
# Previous 3 years for Medicare.
|
|
311
311
|
self.prevMAGI = np.zeros((2))
|
|
@@ -920,14 +920,17 @@ class Plan(object):
|
|
|
920
920
|
# Now fill in parameters which are in $.
|
|
921
921
|
for i, iname in enumerate(self.inames):
|
|
922
922
|
h = self.horizons[i]
|
|
923
|
-
self.omega_in[i, :h] = self.timeLists[iname]["anticipated wages"].iloc[:h]
|
|
924
|
-
self.
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
self.kappa_ijn[i,
|
|
929
|
-
self.
|
|
930
|
-
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)
|
|
931
934
|
|
|
932
935
|
self.caseStatus = "modified"
|
|
933
936
|
|
|
@@ -984,8 +987,9 @@ class Plan(object):
|
|
|
984
987
|
]
|
|
985
988
|
for i, iname in enumerate(self.inames):
|
|
986
989
|
h = self.horizons[i]
|
|
987
|
-
df = pd.DataFrame(0, index=np.arange(h), columns=cols)
|
|
988
|
-
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)
|
|
989
993
|
self.timeLists[iname] = df
|
|
990
994
|
|
|
991
995
|
self.caseStatus = "modified"
|
|
@@ -1086,6 +1090,7 @@ class Plan(object):
|
|
|
1086
1090
|
self._add_standard_exemption_bounds()
|
|
1087
1091
|
self._add_defunct_constraints()
|
|
1088
1092
|
self._add_roth_conversion_constraints(options)
|
|
1093
|
+
self._add_roth_maturation_constraints()
|
|
1089
1094
|
self._add_withdrawal_limits()
|
|
1090
1095
|
self._add_conversion_limits()
|
|
1091
1096
|
self._add_objective_constraints(objective, options)
|
|
@@ -1127,6 +1132,45 @@ class Plan(object):
|
|
|
1127
1132
|
for j in range(self.N_j):
|
|
1128
1133
|
self.B.setRange(_q3(self.C["w"], self.i_d, j, n, self.N_i, self.N_j, self.N_n), 0, 0)
|
|
1129
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
|
+
|
|
1130
1174
|
def _add_roth_conversion_constraints(self, options):
|
|
1131
1175
|
if "maxRothConversion" in options and options["maxRothConversion"] == "file":
|
|
1132
1176
|
for i in range(self.N_i):
|
|
@@ -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],
|
|
@@ -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/timelists.py
CHANGED
|
@@ -21,7 +21,7 @@ import pandas as pd
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
# Expected headers in each excel sheet, one per individual.
|
|
24
|
-
|
|
24
|
+
_timeHorizonItems = [
|
|
25
25
|
"year",
|
|
26
26
|
"anticipated wages",
|
|
27
27
|
"taxable ctrb",
|
|
@@ -59,14 +59,14 @@ def read(finput, inames, horizons, mylog):
|
|
|
59
59
|
raise Exception(f"Could not read file {finput}: {e}.") from e
|
|
60
60
|
streamName = f"file '{finput}'"
|
|
61
61
|
|
|
62
|
-
timeLists =
|
|
62
|
+
timeLists = _condition(dfDict, inames, horizons, mylog)
|
|
63
63
|
|
|
64
64
|
mylog.vprint(f"Successfully read time horizons from {streamName}.")
|
|
65
65
|
|
|
66
66
|
return finput, timeLists
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
def
|
|
69
|
+
def _condition(dfDict, inames, horizons, mylog):
|
|
70
70
|
"""
|
|
71
71
|
Make sure that time horizons contain all years up to life expectancy,
|
|
72
72
|
and that values are positive (except big-ticket items).
|
|
@@ -83,24 +83,24 @@ def condition(dfDict, inames, horizons, mylog):
|
|
|
83
83
|
|
|
84
84
|
df = df.loc[:, ~df.columns.str.contains("^Unnamed")]
|
|
85
85
|
for col in df.columns:
|
|
86
|
-
if col == "" or col not in
|
|
86
|
+
if col == "" or col not in _timeHorizonItems:
|
|
87
87
|
df.drop(col, axis=1, inplace=True)
|
|
88
88
|
|
|
89
|
-
for item in
|
|
89
|
+
for item in _timeHorizonItems:
|
|
90
90
|
if item not in df.columns:
|
|
91
91
|
raise ValueError(f"Item {item} not found for {iname}.")
|
|
92
92
|
|
|
93
|
-
# Only consider lines in proper year range.
|
|
94
|
-
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)]
|
|
95
95
|
df = df[df["year"] < endyear]
|
|
96
96
|
missing = []
|
|
97
|
-
for n in range(horizons[i]):
|
|
97
|
+
for n in range(-5, horizons[i]):
|
|
98
98
|
year = thisyear + n
|
|
99
99
|
if not (df[df["year"] == year]).any(axis=None):
|
|
100
100
|
df.loc[len(df)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
|
|
101
101
|
missing.append(year)
|
|
102
102
|
else:
|
|
103
|
-
for item in
|
|
103
|
+
for item in _timeHorizonItems:
|
|
104
104
|
if item != "big-ticket items" and df[item].iloc[n] < 0:
|
|
105
105
|
raise ValueError(f"Item {item} for {iname} in year {df['year'].iloc[n]} is < 0.")
|
|
106
106
|
|
owlplanner/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2025.06.
|
|
1
|
+
__version__ = "2025.06.21"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: owlplanner
|
|
3
|
-
Version: 2025.6.
|
|
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
|
|
@@ -837,6 +837,8 @@ which are all tracked separately for married individuals. Asset transition to th
|
|
|
837
837
|
is done according to beneficiary fractions for each type of savings account.
|
|
838
838
|
Tax status covers married filing jointly and single, depending on the number of individuals reported.
|
|
839
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.
|
|
840
842
|
Medicare and IRMAA calculations are performed through a self-consistent loop on cash flow constraints.
|
|
841
843
|
Future values are simple projections of current values with the assumed inflation rates.
|
|
842
844
|
|
|
@@ -885,7 +887,7 @@ assets to support, even with no estate being left.
|
|
|
885
887
|
- Streamlit Community Cloud [Streamlit](https://streamlit.io)
|
|
886
888
|
- Contributors: Josh (noimjosh@gmail.com) for Docker image code,
|
|
887
889
|
Dale Seng (sengsational) for great insights and suggestions,
|
|
888
|
-
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.
|
|
889
891
|
|
|
890
892
|
---------------------------------------------------------------------
|
|
891
893
|
|
|
@@ -893,8 +895,8 @@ Copyright © 2024 - Martin-D. Lacasse
|
|
|
893
895
|
|
|
894
896
|
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
895
897
|
|
|
896
|
-
Code output has been verified with analytical solutions and
|
|
897
|
-
Nevertheless, accuracy of results
|
|
898
|
+
Code output has been verified with analytical solutions when applicable, and comparative approaches otherwise.
|
|
899
|
+
Nevertheless, accuracy of results is not guaranteed.
|
|
898
900
|
|
|
899
901
|
--------------------------------------------------------
|
|
900
902
|
|
|
@@ -2,13 +2,13 @@ owlplanner/__init__.py,sha256=hJ2i4m2JpHPAKyQLjYOXpJzeEsgcTcKD-Vhm0AIjjWg,592
|
|
|
2
2
|
owlplanner/abcapi.py,sha256=8VCXS7nH_QZYxCUU3lwO0_UPR9Q5fuYQ6DHDLvHVLPg,6878
|
|
3
3
|
owlplanner/config.py,sha256=onGIMqW2WwB9_CUZauDL6LtHGvc8O1cPUKKcK7Oh70M,12617
|
|
4
4
|
owlplanner/mylogging.py,sha256=RKUr-y-1XvKZzLMcfdtm4IM30LuRpJwb2qUeXmAWqME,2557
|
|
5
|
-
owlplanner/plan.py,sha256=
|
|
5
|
+
owlplanner/plan.py,sha256=VVHyKJiooLXXVLiRFJcauZ3oOYU62CCBe4DlpA08P38,109765
|
|
6
6
|
owlplanner/progress.py,sha256=2DOjOLo6Mo7m21wY-9iZhoUksAyi4VCbb6UL2RegNCw,529
|
|
7
7
|
owlplanner/rates.py,sha256=7jXcuHbkJ3AVIeBYZdwme18rdYslIzCuT-c0cLzvKUU,14819
|
|
8
8
|
owlplanner/tax2025.py,sha256=HVYJq8po28jL5Z_il39ZY7qvf2riUEfxio15Zp7TGj8,7890
|
|
9
|
-
owlplanner/timelists.py,sha256=
|
|
9
|
+
owlplanner/timelists.py,sha256=95rKYknGMi1bonDVIc3xNmiwG0zTSejKyQy_uWCLSiA,4024
|
|
10
10
|
owlplanner/utils.py,sha256=6Ky8ZKfNE9x--3znsZ8VZaT2PptDinszRxWsOCPanu8,2512
|
|
11
|
-
owlplanner/version.py,sha256=
|
|
11
|
+
owlplanner/version.py,sha256=Q6lAE5sS9Y-tuy3jNnv43HcB70y7l1kmDPNzx4CR9tc,28
|
|
12
12
|
owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
|
|
14
14
|
owlplanner/plotting/__init__.py,sha256=uhxqtUi0OI-QWNOO2LkXgQViW_9yM3rYb-204Wit974,250
|
|
@@ -16,7 +16,7 @@ owlplanner/plotting/base.py,sha256=UimGKpMTV-dVm3BX5Apr_Ltorc7dlDLCRPRQ3RF_v7c,2
|
|
|
16
16
|
owlplanner/plotting/factory.py,sha256=EDopIAPQr9zHRgemObko18FlCeRNhNCoLNNFAOq-X6s,1030
|
|
17
17
|
owlplanner/plotting/matplotlib_backend.py,sha256=AOEkapD94U5hGNoS0EdbRoe8mgdMHH4oOvkXADZS914,17957
|
|
18
18
|
owlplanner/plotting/plotly_backend.py,sha256=AO33GxBHGYG5osir_H1iRRtGxdhs4AjfLV2d_xm35nY,33138
|
|
19
|
-
owlplanner-2025.6.
|
|
20
|
-
owlplanner-2025.6.
|
|
21
|
-
owlplanner-2025.6.
|
|
22
|
-
owlplanner-2025.6.
|
|
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,,
|
|
File without changes
|
|
File without changes
|