owlplanner 2025.5.3__py3-none-any.whl → 2025.5.12__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/__init__.py +8 -6
- owlplanner/abcapi.py +14 -7
- owlplanner/config.py +7 -7
- owlplanner/plan.py +435 -829
- owlplanner/plotting/__init__.py +7 -0
- owlplanner/plotting/base.py +76 -0
- owlplanner/plotting/factory.py +32 -0
- owlplanner/plotting/matplotlib_backend.py +432 -0
- owlplanner/plotting/plotly_backend.py +980 -0
- owlplanner/rates.py +40 -68
- owlplanner/tax2025.py +2 -1
- owlplanner/timelists.py +3 -8
- owlplanner/version.py +1 -1
- {owlplanner-2025.5.3.dist-info → owlplanner-2025.5.12.dist-info}/METADATA +8 -5
- owlplanner-2025.5.12.dist-info/RECORD +22 -0
- owlplanner-2025.5.3.dist-info/RECORD +0 -17
- /owlplanner/{logging.py → mylogging.py} +0 -0
- {owlplanner-2025.5.3.dist-info → owlplanner-2025.5.12.dist-info}/WHEEL +0 -0
- {owlplanner-2025.5.3.dist-info → owlplanner-2025.5.12.dist-info}/licenses/LICENSE +0 -0
owlplanner/rates.py
CHANGED
|
@@ -35,7 +35,7 @@ import os
|
|
|
35
35
|
import sys
|
|
36
36
|
from datetime import date
|
|
37
37
|
|
|
38
|
-
from owlplanner import
|
|
38
|
+
from owlplanner import mylogging as log
|
|
39
39
|
from owlplanner import utils as u
|
|
40
40
|
|
|
41
41
|
# All data goes from 1928 to 2024. Update the TO value when data
|
|
@@ -48,7 +48,7 @@ file = os.path.join(where, "data/rates.csv")
|
|
|
48
48
|
try:
|
|
49
49
|
df = pd.read_csv(file)
|
|
50
50
|
except Exception as e:
|
|
51
|
-
raise RuntimeError(f"Could not find rates data file: {e}")
|
|
51
|
+
raise RuntimeError(f"Could not find rates data file: {e}") from e
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
# Annual rate of return (%) of S&P 500 since 1928, including dividends.
|
|
@@ -77,14 +77,17 @@ def getRatesDistributions(frm, to, mylog=None):
|
|
|
77
77
|
the different rates. Function returns means and covariance matrix.
|
|
78
78
|
"""
|
|
79
79
|
if mylog is None:
|
|
80
|
-
mylog =
|
|
80
|
+
mylog = log.Logger()
|
|
81
81
|
|
|
82
82
|
# Convert years to index and check range.
|
|
83
83
|
frm -= FROM
|
|
84
84
|
to -= FROM
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
if not (0 <= frm and frm <= len(SP500)):
|
|
86
|
+
raise ValueError(f"Range 'from' {frm} out of bounds.")
|
|
87
|
+
if not (0 <= to and to <= len(SP500)):
|
|
88
|
+
raise ValueError(f"Range 'to' {to} out of bounds.")
|
|
89
|
+
if frm >= to:
|
|
90
|
+
raise ValueError(f'"from" {frm} must be smaller than "to" {to}.')
|
|
88
91
|
|
|
89
92
|
series = {
|
|
90
93
|
"SP500": SP500,
|
|
@@ -124,9 +127,12 @@ def historicalValue(amount, year):
|
|
|
124
127
|
valued at the beginning of the year specified.
|
|
125
128
|
"""
|
|
126
129
|
thisyear = date.today().year
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
if TO != thisyear - 1:
|
|
131
|
+
raise RuntimeError(f"Rates file needs to be updated to be current to {thisyear}.")
|
|
132
|
+
if year < FROM:
|
|
133
|
+
raise ValueError(f"Only data from {FROM} is available.")
|
|
134
|
+
if year > thisyear:
|
|
135
|
+
raise ValueError(f"Year must be < {thisyear} for historical data.")
|
|
130
136
|
|
|
131
137
|
span = thisyear - year
|
|
132
138
|
ub = len(Inflation)
|
|
@@ -153,7 +159,7 @@ class Rates(object):
|
|
|
153
159
|
Default constructor.
|
|
154
160
|
"""
|
|
155
161
|
if mylog is None:
|
|
156
|
-
self.mylog =
|
|
162
|
+
self.mylog = log.Logger()
|
|
157
163
|
else:
|
|
158
164
|
self.mylog = mylog
|
|
159
165
|
|
|
@@ -223,18 +229,24 @@ class Rates(object):
|
|
|
223
229
|
self.mylog.vprint("Using conservative fixed rates values:", *[u.pc(k) for k in self.means])
|
|
224
230
|
self._setFixedRates(self._conservRates)
|
|
225
231
|
elif method == "user":
|
|
226
|
-
|
|
227
|
-
|
|
232
|
+
if values is None:
|
|
233
|
+
raise ValueError("Fixed values must be provided with the user option.")
|
|
234
|
+
if len(values) != Nk:
|
|
235
|
+
raise ValueError(f"Values must have {Nk} items.")
|
|
228
236
|
self.means = np.array(values, dtype=float)
|
|
229
237
|
# Convert percent to decimal for storing.
|
|
230
238
|
self.means /= 100.0
|
|
231
239
|
self.mylog.vprint("Setting rates using fixed user values:", *[u.pc(k) for k in self.means])
|
|
232
240
|
self._setFixedRates(self.means)
|
|
233
241
|
elif method == "stochastic":
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
242
|
+
if values is None:
|
|
243
|
+
raise ValueError("Mean values must be provided with the stochastic option.")
|
|
244
|
+
if stdev is None:
|
|
245
|
+
raise ValueError("Standard deviations must be provided with the stochastic option.")
|
|
246
|
+
if len(values) != Nk:
|
|
247
|
+
raise ValueError(f"Values must have {Nk} items.")
|
|
248
|
+
if len(stdev) != Nk:
|
|
249
|
+
raise ValueError(f"stdev must have {Nk} items.")
|
|
238
250
|
self.means = np.array(values, dtype=float)
|
|
239
251
|
self.stdev = np.array(stdev, dtype=float)
|
|
240
252
|
# Convert percent to decimal for storing.
|
|
@@ -262,7 +274,8 @@ class Rates(object):
|
|
|
262
274
|
raise RuntimeError(f"Unable to process correlation shape of {corrarr.shape}.")
|
|
263
275
|
|
|
264
276
|
self.corr = corrarr
|
|
265
|
-
|
|
277
|
+
if not np.array_equal(self.corr, self.corr.T):
|
|
278
|
+
raise ValueError("Correlation matrix must be symmetric.")
|
|
266
279
|
# Now build covariance matrix from stdev and correlation matrix.
|
|
267
280
|
# Multiply each row by a vector element-wise. Then columns.
|
|
268
281
|
covar = self.corr * self.stdev
|
|
@@ -273,10 +286,14 @@ class Rates(object):
|
|
|
273
286
|
self.mylog.vprint("\t and correlation matrix:\n\t\t", str(self.corr).replace("\n", "\n\t\t"))
|
|
274
287
|
else:
|
|
275
288
|
# Then methods relying on historical data range.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
289
|
+
if frm is None:
|
|
290
|
+
raise ValueError("From year must be provided with this option.")
|
|
291
|
+
if not (FROM <= frm <= TO):
|
|
292
|
+
raise ValueError(f"Lower range 'frm={frm}' out of bounds.")
|
|
293
|
+
if not (FROM <= to <= TO):
|
|
294
|
+
raise ValueError(f"Upper range 'to={to}' out of bounds.")
|
|
295
|
+
if not (frm < to):
|
|
296
|
+
raise ValueError("Unacceptable range.")
|
|
280
297
|
self.frm = frm
|
|
281
298
|
self.to = to
|
|
282
299
|
|
|
@@ -300,7 +317,8 @@ class Rates(object):
|
|
|
300
317
|
|
|
301
318
|
def _setFixedRates(self, rates):
|
|
302
319
|
Nk = len(self._defRates)
|
|
303
|
-
|
|
320
|
+
if len(rates) != Nk:
|
|
321
|
+
raise ValueError(f"Rate list provided must have {Nk} entries.")
|
|
304
322
|
self._myRates = np.array(rates)
|
|
305
323
|
self._rateMethod = self._fixedRates
|
|
306
324
|
|
|
@@ -364,49 +382,3 @@ class Rates(object):
|
|
|
364
382
|
srates = np.random.multivariate_normal(self.means, self.covar)
|
|
365
383
|
|
|
366
384
|
return srates
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
def showRatesDistributions(frm=FROM, to=TO):
|
|
370
|
-
"""
|
|
371
|
-
Plot histograms of the rates distributions.
|
|
372
|
-
"""
|
|
373
|
-
import matplotlib.pyplot as plt
|
|
374
|
-
|
|
375
|
-
title = "Rates from " + str(frm) + " to " + str(to)
|
|
376
|
-
# Bring year values to indices.
|
|
377
|
-
frm -= FROM
|
|
378
|
-
to -= FROM
|
|
379
|
-
|
|
380
|
-
nbins = int((to - frm) / 4)
|
|
381
|
-
fig, ax = plt.subplots(1, 4, sharey=True, sharex=True, tight_layout=True)
|
|
382
|
-
|
|
383
|
-
dat0 = np.array(SP500[frm:to])
|
|
384
|
-
dat1 = np.array(BondsBaa[frm:to])
|
|
385
|
-
dat2 = np.array(TNotes[frm:to])
|
|
386
|
-
dat3 = np.array(Inflation[frm:to])
|
|
387
|
-
|
|
388
|
-
fig.suptitle(title)
|
|
389
|
-
ax[0].set_title("S&P500")
|
|
390
|
-
label = "<>: " + u.pc(np.mean(dat0), 2, 1)
|
|
391
|
-
ax[0].hist(dat0, bins=nbins, label=label)
|
|
392
|
-
ax[0].legend(loc="upper left", fontsize=8, framealpha=0.7)
|
|
393
|
-
|
|
394
|
-
ax[1].set_title("BondsBaa")
|
|
395
|
-
label = "<>: " + u.pc(np.mean(dat1), 2, 1)
|
|
396
|
-
ax[1].hist(dat1, bins=nbins, label=label)
|
|
397
|
-
ax[1].legend(loc="upper left", fontsize=8, framealpha=0.7)
|
|
398
|
-
|
|
399
|
-
ax[2].set_title("TNotes")
|
|
400
|
-
label = "<>: " + u.pc(np.mean(dat2), 2, 1)
|
|
401
|
-
ax[2].hist(dat1, bins=nbins, label=label)
|
|
402
|
-
ax[2].legend(loc="upper left", fontsize=8, framealpha=0.7)
|
|
403
|
-
|
|
404
|
-
ax[3].set_title("Inflation")
|
|
405
|
-
label = "<>: " + u.pc(np.mean(dat3), 2, 1)
|
|
406
|
-
ax[3].hist(dat3, bins=nbins, label=label)
|
|
407
|
-
ax[3].legend(loc="upper left", fontsize=8, framealpha=0.7)
|
|
408
|
-
|
|
409
|
-
plt.show()
|
|
410
|
-
|
|
411
|
-
# return fig, ax
|
|
412
|
-
return None
|
owlplanner/tax2025.py
CHANGED
|
@@ -172,7 +172,8 @@ def taxBrackets(N_i, n_d, N_n, y_TCJA):
|
|
|
172
172
|
Return dictionary containing future tax brackets
|
|
173
173
|
unadjusted for inflation for plotting.
|
|
174
174
|
"""
|
|
175
|
-
|
|
175
|
+
if not (0 < N_i <= 2):
|
|
176
|
+
raise ValueError(f"Cannot process {N_i} individuals.")
|
|
176
177
|
n_d = min(n_d, N_n)
|
|
177
178
|
status = N_i - 1
|
|
178
179
|
|
owlplanner/timelists.py
CHANGED
|
@@ -55,7 +55,7 @@ def read(finput, inames, horizons, mylog):
|
|
|
55
55
|
try:
|
|
56
56
|
dfDict = pd.read_excel(finput, sheet_name=None)
|
|
57
57
|
except Exception as e:
|
|
58
|
-
raise Exception(f"Could not read file {finput}: {e}.")
|
|
58
|
+
raise Exception(f"Could not read file {finput}: {e}.") from e
|
|
59
59
|
streamName = f"file '{finput}'"
|
|
60
60
|
|
|
61
61
|
timeLists = condition(dfDict, inames, horizons, mylog)
|
|
@@ -83,7 +83,7 @@ def condition(dfDict, inames, horizons, mylog):
|
|
|
83
83
|
df = df.loc[:, ~df.columns.str.contains("^Unnamed")]
|
|
84
84
|
for col in df.columns:
|
|
85
85
|
if col == "" or col not in timeHorizonItems:
|
|
86
|
-
df.drop(col, axis=1)
|
|
86
|
+
df.drop(col, axis=1, inplace=True)
|
|
87
87
|
|
|
88
88
|
for item in timeHorizonItems:
|
|
89
89
|
if item not in df.columns:
|
|
@@ -114,12 +114,7 @@ def condition(dfDict, inames, horizons, mylog):
|
|
|
114
114
|
|
|
115
115
|
if df["year"].iloc[-1] != endyear - 1:
|
|
116
116
|
raise ValueError(
|
|
117
|
-
"Time horizon for"
|
|
118
|
-
iname,
|
|
119
|
-
"is too short.\n\tIt should end in",
|
|
120
|
-
endyear,
|
|
121
|
-
"but ends in",
|
|
122
|
-
df["year"].iloc[-1],
|
|
117
|
+
f"Time horizon for {iname} too short.\n\tIt should end in {endyear}, not {df['year'].iloc[-1]}"
|
|
123
118
|
)
|
|
124
119
|
|
|
125
120
|
return timeLists
|
owlplanner/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2025.05.
|
|
1
|
+
__version__ = "2025.05.12"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: owlplanner
|
|
3
|
-
Version: 2025.5.
|
|
3
|
+
Version: 2025.5.12
|
|
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
|
|
@@ -695,6 +695,7 @@ Requires-Dist: matplotlib
|
|
|
695
695
|
Requires-Dist: numpy
|
|
696
696
|
Requires-Dist: openpyxl
|
|
697
697
|
Requires-Dist: pandas
|
|
698
|
+
Requires-Dist: plotly
|
|
698
699
|
Requires-Dist: scipy
|
|
699
700
|
Requires-Dist: seaborn
|
|
700
701
|
Requires-Dist: streamlit
|
|
@@ -711,15 +712,17 @@ Description-Content-Type: text/markdown
|
|
|
711
712
|
-------------------------------------------------------------------------------------
|
|
712
713
|
|
|
713
714
|
### TL;DR
|
|
714
|
-
Owl is a retirement planning tool that uses a linear programming
|
|
715
|
-
to provide guidance on retirement decisions
|
|
715
|
+
Owl is a financial retirement planning tool that uses a linear programming
|
|
716
|
+
optimization algorithm to provide guidance on retirement decisions
|
|
717
|
+
such as contributions, withdrawals, Roth conversions, and more.
|
|
716
718
|
Users can select varying return rates to perform historical back testing,
|
|
717
719
|
stochastic rates for performing Monte Carlo analyses,
|
|
718
720
|
or fixed rates either derived from historical averages, or set by the user.
|
|
719
721
|
|
|
720
722
|
There are a few ways to run Owl:
|
|
721
723
|
|
|
722
|
-
- Run Owl directly on the Streamlit Community Server at
|
|
724
|
+
- Run Owl directly on the Streamlit Community Server at
|
|
725
|
+
[owlplanner.streamlit.app](https://owlplanner.streamlit.app).
|
|
723
726
|
|
|
724
727
|
- Run locally on your computer using a Docker image.
|
|
725
728
|
Follow these [instructions](docker/README.md) for this option.
|
|
@@ -729,7 +732,7 @@ Follow these [instructions](INSTALL.md) to install Owl from the source code and
|
|
|
729
732
|
|
|
730
733
|
-------------------------------------------------------------------------------------
|
|
731
734
|
## Overview
|
|
732
|
-
This package is a
|
|
735
|
+
This package is a modeling framework for exploring the sensitivity of retirement financial decisions.
|
|
733
736
|
Strictly speaking, it is not a planning tool, but more an environment for exploring *what if* scenarios.
|
|
734
737
|
It provides different realizations of a financial strategy through the rigorous
|
|
735
738
|
mathematical optimization of relevant decision variables. Two major objective goals can be set: either
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
owlplanner/__init__.py,sha256=hJ2i4m2JpHPAKyQLjYOXpJzeEsgcTcKD-Vhm0AIjjWg,592
|
|
2
|
+
owlplanner/abcapi.py,sha256=m0vtoEzz9HJV7fOK_d7OnK7ha2Qbf7wLLPCJ9YZzR1k,6851
|
|
3
|
+
owlplanner/config.py,sha256=qIWzj3Tbz_jhhFqIkaMzXzgWQBN4Uk2km_VIMZSh910,12559
|
|
4
|
+
owlplanner/mylogging.py,sha256=tYMw04O-XYSzjTj36fmKJGLcE1VkK6k6oJNeqtKXzuc,2530
|
|
5
|
+
owlplanner/plan.py,sha256=_Oq2UomWzteYVlT6zJC_A2zXO3YVlXG9xgUMcQfWBJg,106742
|
|
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=5ZqN4YDTyjz76bfj2w5OuF_20x5Fau01NRLJ8nXtxx8,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=1bU6iM1pGIE-l9p0GuulX4gRK_7ds96784Wb5oVUUR0,2449
|
|
16
|
+
owlplanner/plotting/factory.py,sha256=i1k8m_ISnJw06f_JWlMvOQ7Q0PgV_BoLm05uLwFPvOQ,883
|
|
17
|
+
owlplanner/plotting/matplotlib_backend.py,sha256=-M4Am7N0D8Nfv_tKNA1TneFYU_DuW_ZsoUBHRQWD_ok,17887
|
|
18
|
+
owlplanner/plotting/plotly_backend.py,sha256=LUmeWYAA_F1vdl-GPehzprVx3oFWehGmc1Oir-kNJ_w,33093
|
|
19
|
+
owlplanner-2025.5.12.dist-info/METADATA,sha256=w0R8yDGSwrRGCouu2Hbe0Cx4tvQInuwm6VcvHcjFN9Q,53983
|
|
20
|
+
owlplanner-2025.5.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
+
owlplanner-2025.5.12.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
22
|
+
owlplanner-2025.5.12.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
owlplanner/__init__.py,sha256=QqrdT0Qks20osBTg7h0vJHAxpP9lL7DA99xb0nYbtw4,254
|
|
2
|
-
owlplanner/abcapi.py,sha256=Lt8OUgbrfOPzAw0HyxyT2wT-IXI3d9Zo26MwyqdX56Y,6617
|
|
3
|
-
owlplanner/config.py,sha256=F6GS3n02VeFX0GCVeM4J7Ra0in4N632W6TZIXk7Yj2w,12519
|
|
4
|
-
owlplanner/logging.py,sha256=tYMw04O-XYSzjTj36fmKJGLcE1VkK6k6oJNeqtKXzuc,2530
|
|
5
|
-
owlplanner/plan.py,sha256=mKSfHqFdG8P9Dlse-VbTnWSEgO2ZDcqAfK3gHYNEIho,119112
|
|
6
|
-
owlplanner/progress.py,sha256=8jlCvvtgDI89zXVNMBg1-lnEyhpPvKQS2X5oAIpoOVQ,384
|
|
7
|
-
owlplanner/rates.py,sha256=gJaoe-gJqWCQV5qVLlHp-Yn9TSJs-PJzeTbOwMCbqWs,15682
|
|
8
|
-
owlplanner/tax2025.py,sha256=JDBtFFAf2bWtKUMuE3W5F0nBhYaKBjmdJj0iayM2iGA,7829
|
|
9
|
-
owlplanner/timelists.py,sha256=tYieZU67FT6TCcQQis36JaXGI7dT6NqD7RvdEjgJL4M,4026
|
|
10
|
-
owlplanner/utils.py,sha256=WpJgn79YZfH8UCkcmhd-AZlxlGuz1i1-UDBRXImsY6I,2485
|
|
11
|
-
owlplanner/version.py,sha256=NZ889ANsXH5BzjEmfYGvqRdWan3eQSUgEGwPnMds764,28
|
|
12
|
-
owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
|
|
14
|
-
owlplanner-2025.5.3.dist-info/METADATA,sha256=eGnBmMVTpNCvwzG153HRjN1rzzrD0zu1hQn1tV0SroA,53926
|
|
15
|
-
owlplanner-2025.5.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
16
|
-
owlplanner-2025.5.3.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
17
|
-
owlplanner-2025.5.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|