owlplanner 2025.5.2__py3-none-any.whl → 2025.5.5__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 +6 -6
- owlplanner/abcapi.py +14 -7
- owlplanner/config.py +5 -5
- owlplanner/plan.py +329 -587
- owlplanner/plots.py +296 -0
- owlplanner/rates.py +42 -59
- owlplanner/tax2025.py +2 -1
- owlplanner/timelists.py +1 -1
- owlplanner/version.py +1 -1
- {owlplanner-2025.5.2.dist-info → owlplanner-2025.5.5.dist-info}/METADATA +1 -1
- owlplanner-2025.5.5.dist-info/RECORD +18 -0
- owlplanner-2025.5.2.dist-info/RECORD +0 -17
- {owlplanner-2025.5.2.dist-info → owlplanner-2025.5.5.dist-info}/WHEEL +0 -0
- {owlplanner-2025.5.2.dist-info → owlplanner-2025.5.5.dist-info}/licenses/LICENSE +0 -0
owlplanner/plots.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
Owl/plots
|
|
4
|
+
---------
|
|
5
|
+
|
|
6
|
+
A retirement planner using linear programming optimization.
|
|
7
|
+
|
|
8
|
+
This module contains all plotting functions used by the Owl project.
|
|
9
|
+
|
|
10
|
+
Copyright (C) 2025 -- Martin-D. Lacasse
|
|
11
|
+
|
|
12
|
+
Disclaimer: This program comes with no guarantee. Use at your own risk.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
import matplotlib.pyplot as plt
|
|
18
|
+
import matplotlib.ticker as tk
|
|
19
|
+
import io
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
os.environ["JUPYTER_PLATFORM_DIRS"] = "1"
|
|
23
|
+
import seaborn as sbn
|
|
24
|
+
|
|
25
|
+
from owlplanner import utils as u
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def line_income_plot(x, series, style, title, yformat="\\$k"):
|
|
29
|
+
"""
|
|
30
|
+
Core line plotter function.
|
|
31
|
+
"""
|
|
32
|
+
fig, ax = plt.subplots(figsize=(6, 4))
|
|
33
|
+
plt.grid(visible="both")
|
|
34
|
+
|
|
35
|
+
for sname in series:
|
|
36
|
+
ax.plot(x, series[sname], label=sname, ls=style[sname])
|
|
37
|
+
|
|
38
|
+
ax.legend(loc="upper left", reverse=True, fontsize=8, framealpha=0.3)
|
|
39
|
+
ax.set_title(title)
|
|
40
|
+
ax.set_xlabel("year")
|
|
41
|
+
ax.set_ylabel(yformat)
|
|
42
|
+
ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
|
|
43
|
+
if "k" in yformat:
|
|
44
|
+
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000), ",")))
|
|
45
|
+
# Give range to y values in unindexed flat profiles.
|
|
46
|
+
ymin, ymax = ax.get_ylim()
|
|
47
|
+
if ymax - ymin < 5000:
|
|
48
|
+
ax.set_ylim((ymin * 0.95, ymax * 1.05))
|
|
49
|
+
|
|
50
|
+
return fig, ax
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def stack_plot(x, inames, title, irange, series, snames, location, yformat="\\$k"):
|
|
54
|
+
"""
|
|
55
|
+
Core function for stacked plots.
|
|
56
|
+
"""
|
|
57
|
+
nonzeroSeries = {}
|
|
58
|
+
for sname in snames:
|
|
59
|
+
for i in irange:
|
|
60
|
+
tmp = series[sname][i]
|
|
61
|
+
if sum(tmp) > 1.0:
|
|
62
|
+
nonzeroSeries[sname + " " + inames[i]] = tmp
|
|
63
|
+
|
|
64
|
+
if len(nonzeroSeries) == 0:
|
|
65
|
+
return None, None
|
|
66
|
+
|
|
67
|
+
fig, ax = plt.subplots(figsize=(6, 4))
|
|
68
|
+
plt.grid(visible="both")
|
|
69
|
+
|
|
70
|
+
ax.stackplot(x, nonzeroSeries.values(), labels=nonzeroSeries.keys(), alpha=0.6)
|
|
71
|
+
ax.legend(loc=location, reverse=True, fontsize=8, ncol=2, framealpha=0.5)
|
|
72
|
+
ax.set_title(title)
|
|
73
|
+
ax.set_xlabel("year")
|
|
74
|
+
ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
|
|
75
|
+
if "k" in yformat:
|
|
76
|
+
ax.set_ylabel(yformat)
|
|
77
|
+
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000), ",")))
|
|
78
|
+
elif yformat == "percent":
|
|
79
|
+
ax.set_ylabel("%")
|
|
80
|
+
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(100 * x), ",")))
|
|
81
|
+
else:
|
|
82
|
+
raise RuntimeError(f"Unknown yformat: {yformat}.")
|
|
83
|
+
|
|
84
|
+
return fig, ax
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def show_histogram_results(objective, df, N, year_n, n_d=None, N_i=1, phi_j=None):
|
|
88
|
+
"""
|
|
89
|
+
Show a histogram of values from historical data or Monte Carlo simulations.
|
|
90
|
+
"""
|
|
91
|
+
description = io.StringIO()
|
|
92
|
+
|
|
93
|
+
pSuccess = u.pc(len(df) / N)
|
|
94
|
+
print(f"Success rate: {pSuccess} on {N} samples.", file=description)
|
|
95
|
+
title = f"$N$ = {N}, $P$ = {pSuccess}"
|
|
96
|
+
means = df.mean(axis=0, numeric_only=True)
|
|
97
|
+
medians = df.median(axis=0, numeric_only=True)
|
|
98
|
+
|
|
99
|
+
my = 2 * [year_n[-1]]
|
|
100
|
+
if N_i == 2 and n_d is not None and n_d < len(year_n):
|
|
101
|
+
my[0] = year_n[n_d - 1]
|
|
102
|
+
|
|
103
|
+
# Don't show partial bequest of zero if spouse is full beneficiary,
|
|
104
|
+
# or if solution led to empty accounts at the end of first spouse's life.
|
|
105
|
+
if (phi_j is not None and np.all(phi_j == 1)) or medians.iloc[0] < 1:
|
|
106
|
+
if medians.iloc[0] < 1:
|
|
107
|
+
print(f"Optimized solutions all have null partial bequest in year {my[0]}.", file=description)
|
|
108
|
+
df.drop("partial", axis=1, inplace=True)
|
|
109
|
+
means = df.mean(axis=0, numeric_only=True)
|
|
110
|
+
medians = df.median(axis=0, numeric_only=True)
|
|
111
|
+
|
|
112
|
+
df /= 1000
|
|
113
|
+
if len(df) > 0:
|
|
114
|
+
thisyear = year_n[0]
|
|
115
|
+
if objective == "maxBequest":
|
|
116
|
+
fig, axes = plt.subplots()
|
|
117
|
+
# Show both partial and final bequests in the same histogram.
|
|
118
|
+
sbn.histplot(df, multiple="dodge", kde=True, ax=axes)
|
|
119
|
+
legend = []
|
|
120
|
+
# Don't know why but legend is reversed from df.
|
|
121
|
+
for q in range(len(means) - 1, -1, -1):
|
|
122
|
+
dmedian = u.d(medians.iloc[q], latex=True)
|
|
123
|
+
dmean = u.d(means.iloc[q], latex=True)
|
|
124
|
+
legend.append(f"{my[q]}: $M$: {dmedian}, $\\bar{{x}}$: {dmean}")
|
|
125
|
+
plt.legend(legend, shadow=True)
|
|
126
|
+
plt.xlabel(f"{thisyear} $k")
|
|
127
|
+
plt.title(objective)
|
|
128
|
+
leads = [f"partial {my[0]}", f" final {my[1]}"]
|
|
129
|
+
elif len(means) == 2:
|
|
130
|
+
# Show partial bequest and net spending as two separate histograms.
|
|
131
|
+
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
|
|
132
|
+
cols = ["partial", objective]
|
|
133
|
+
leads = [f"partial {my[0]}", objective]
|
|
134
|
+
for q in range(2):
|
|
135
|
+
sbn.histplot(df[cols[q]], kde=True, ax=axes[q])
|
|
136
|
+
dmedian = u.d(medians.iloc[q], latex=True)
|
|
137
|
+
dmean = u.d(means.iloc[q], latex=True)
|
|
138
|
+
legend = [f"$M$: {dmedian}, $\\bar{{x}}$: {dmean}"]
|
|
139
|
+
axes[q].set_label(legend)
|
|
140
|
+
axes[q].legend(labels=legend)
|
|
141
|
+
axes[q].set_title(leads[q])
|
|
142
|
+
axes[q].set_xlabel(f"{thisyear} $k")
|
|
143
|
+
else:
|
|
144
|
+
# Show net spending as single histogram.
|
|
145
|
+
fig, axes = plt.subplots()
|
|
146
|
+
sbn.histplot(df[objective], kde=True, ax=axes)
|
|
147
|
+
dmedian = u.d(medians.iloc[0], latex=True)
|
|
148
|
+
dmean = u.d(means.iloc[0], latex=True)
|
|
149
|
+
legend = [f"$M$: {dmedian}, $\\bar{{x}}$: {dmean}"]
|
|
150
|
+
plt.legend(legend, shadow=True)
|
|
151
|
+
plt.xlabel(f"{thisyear} $k")
|
|
152
|
+
plt.title(objective)
|
|
153
|
+
leads = [objective]
|
|
154
|
+
|
|
155
|
+
plt.suptitle(title)
|
|
156
|
+
|
|
157
|
+
for q in range(len(means)):
|
|
158
|
+
print(f"{leads[q]:>12}: Median ({thisyear} $): {u.d(medians.iloc[q])}", file=description)
|
|
159
|
+
print(f"{leads[q]:>12}: Mean ({thisyear} $): {u.d(means.iloc[q])}", file=description)
|
|
160
|
+
mmin = 1000 * df.iloc[:, q].min()
|
|
161
|
+
mmax = 1000 * df.iloc[:, q].max()
|
|
162
|
+
print(f"{leads[q]:>12}: Range: {u.d(mmin)} - {u.d(mmax)}", file=description)
|
|
163
|
+
nzeros = len(df.iloc[:, q][df.iloc[:, q] < 0.001])
|
|
164
|
+
print(f"{leads[q]:>12}: N zero solns: {nzeros}", file=description)
|
|
165
|
+
|
|
166
|
+
return fig, description
|
|
167
|
+
|
|
168
|
+
return None, description
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def show_rates_correlations(name, tau_kn, N_n, rate_method, rate_frm=None, rate_to=None, tag="", share_range=False):
|
|
172
|
+
"""
|
|
173
|
+
Plot correlations between various rates.
|
|
174
|
+
"""
|
|
175
|
+
rate_names = [
|
|
176
|
+
"S&P500 (incl. div.)",
|
|
177
|
+
"Baa Corp. Bonds",
|
|
178
|
+
"10-y T-Notes",
|
|
179
|
+
"Inflation",
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
df = pd.DataFrame()
|
|
183
|
+
for k, name in enumerate(rate_names):
|
|
184
|
+
data = 100 * tau_kn[k]
|
|
185
|
+
df[name] = data
|
|
186
|
+
|
|
187
|
+
g = sbn.PairGrid(df, diag_sharey=False, height=1.8, aspect=1)
|
|
188
|
+
if share_range:
|
|
189
|
+
minval = df.min().min() - 5
|
|
190
|
+
maxval = df.max().max() + 5
|
|
191
|
+
g.set(xlim=(minval, maxval), ylim=(minval, maxval))
|
|
192
|
+
g.map_upper(sbn.scatterplot)
|
|
193
|
+
g.map_lower(sbn.kdeplot)
|
|
194
|
+
g.map_diag(sbn.histplot, color="orange")
|
|
195
|
+
|
|
196
|
+
# Put zero axes on off-diagonal plots.
|
|
197
|
+
imod = len(rate_names) + 1
|
|
198
|
+
for i, ax in enumerate(g.axes.flat):
|
|
199
|
+
ax.axvline(x=0, color="grey", linewidth=1, linestyle=":")
|
|
200
|
+
if i % imod != 0:
|
|
201
|
+
ax.axhline(y=0, color="grey", linewidth=1, linestyle=":")
|
|
202
|
+
|
|
203
|
+
title = name + "\n"
|
|
204
|
+
title += f"Rates Correlations (N={N_n}) {rate_method}"
|
|
205
|
+
if rate_method in ["historical", "histochastic"]:
|
|
206
|
+
title += f" ({rate_frm}-{rate_to})"
|
|
207
|
+
|
|
208
|
+
if tag != "":
|
|
209
|
+
title += " - " + tag
|
|
210
|
+
|
|
211
|
+
g.fig.suptitle(title, y=1.08)
|
|
212
|
+
return g.fig
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def show_rates(name, tau_kn, year_n, year_frac_left, N_k, rate_method, rate_frm=None, rate_to=None, tag=""):
|
|
216
|
+
"""
|
|
217
|
+
Plot rate values used over the time horizon.
|
|
218
|
+
"""
|
|
219
|
+
fig, ax = plt.subplots(figsize=(6, 4))
|
|
220
|
+
plt.grid(visible="both")
|
|
221
|
+
title = name + "\nReturn & Inflation Rates (" + str(rate_method)
|
|
222
|
+
if rate_method in ["historical", "histochastic", "historical average"]:
|
|
223
|
+
title += f" {rate_frm}-{rate_to}"
|
|
224
|
+
title += ")"
|
|
225
|
+
|
|
226
|
+
if tag != "":
|
|
227
|
+
title += " - " + tag
|
|
228
|
+
|
|
229
|
+
rate_name = [
|
|
230
|
+
"S&P500 (incl. div.)",
|
|
231
|
+
"Baa Corp. Bonds",
|
|
232
|
+
"10-y T-Notes",
|
|
233
|
+
"Inflation",
|
|
234
|
+
]
|
|
235
|
+
ltype = ["-", "-.", ":", "--"]
|
|
236
|
+
for k in range(N_k):
|
|
237
|
+
if year_frac_left == 1:
|
|
238
|
+
data = 100 * tau_kn[k]
|
|
239
|
+
years = year_n
|
|
240
|
+
else:
|
|
241
|
+
data = 100 * tau_kn[k, 1:]
|
|
242
|
+
years = year_n[1:]
|
|
243
|
+
|
|
244
|
+
# Use ddof=1 to match pandas.
|
|
245
|
+
label = (
|
|
246
|
+
rate_name[k] + " <" + "{:.1f}".format(np.mean(data)) + " +/- {:.1f}".format(np.std(data, ddof=1)) + "%>"
|
|
247
|
+
)
|
|
248
|
+
ax.plot(years, data, label=label, ls=ltype[k % N_k])
|
|
249
|
+
|
|
250
|
+
ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
|
|
251
|
+
ax.legend(loc="best", reverse=False, fontsize=8, framealpha=0.7)
|
|
252
|
+
ax.set_title(title)
|
|
253
|
+
ax.set_xlabel("year")
|
|
254
|
+
ax.set_ylabel("%")
|
|
255
|
+
return fig
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def show_rates_distributions(frm, to, SP500, BondsBaa, TNotes, Inflation, FROM):
|
|
259
|
+
"""
|
|
260
|
+
Plot histograms of the rates distributions.
|
|
261
|
+
"""
|
|
262
|
+
title = f"Rates from {frm} to {to}"
|
|
263
|
+
# Bring year values to indices.
|
|
264
|
+
frm -= FROM
|
|
265
|
+
to -= FROM
|
|
266
|
+
|
|
267
|
+
nbins = int((to - frm) / 4)
|
|
268
|
+
fig, ax = plt.subplots(1, 4, sharey=True, sharex=True, tight_layout=True)
|
|
269
|
+
|
|
270
|
+
dat0 = np.array(SP500[frm:to])
|
|
271
|
+
dat1 = np.array(BondsBaa[frm:to])
|
|
272
|
+
dat2 = np.array(TNotes[frm:to])
|
|
273
|
+
dat3 = np.array(Inflation[frm:to])
|
|
274
|
+
|
|
275
|
+
fig.suptitle(title)
|
|
276
|
+
ax[0].set_title("S&P500")
|
|
277
|
+
label = "<>: " + u.pc(np.mean(dat0), 2, 1)
|
|
278
|
+
ax[0].hist(dat0, bins=nbins, label=label)
|
|
279
|
+
ax[0].legend(loc="upper left", fontsize=8, framealpha=0.7)
|
|
280
|
+
|
|
281
|
+
ax[1].set_title("BondsBaa")
|
|
282
|
+
label = "<>: " + u.pc(np.mean(dat1), 2, 1)
|
|
283
|
+
ax[1].hist(dat1, bins=nbins, label=label)
|
|
284
|
+
ax[1].legend(loc="upper left", fontsize=8, framealpha=0.7)
|
|
285
|
+
|
|
286
|
+
ax[2].set_title("TNotes")
|
|
287
|
+
label = "<>: " + u.pc(np.mean(dat2), 2, 1)
|
|
288
|
+
ax[2].hist(dat2, bins=nbins, label=label)
|
|
289
|
+
ax[2].legend(loc="upper left", fontsize=8, framealpha=0.7)
|
|
290
|
+
|
|
291
|
+
ax[3].set_title("Inflation")
|
|
292
|
+
label = "<>: " + u.pc(np.mean(dat3), 2, 1)
|
|
293
|
+
ax[3].hist(dat3, bins=nbins, label=label)
|
|
294
|
+
ax[3].legend(loc="upper left", fontsize=8, framealpha=0.7)
|
|
295
|
+
|
|
296
|
+
return fig
|
owlplanner/rates.py
CHANGED
|
@@ -37,6 +37,7 @@ from datetime import date
|
|
|
37
37
|
|
|
38
38
|
from owlplanner import logging
|
|
39
39
|
from owlplanner import utils as u
|
|
40
|
+
from owlplanner import plots
|
|
40
41
|
|
|
41
42
|
# All data goes from 1928 to 2024. Update the TO value when data
|
|
42
43
|
# becomes available for subsequent years.
|
|
@@ -48,7 +49,7 @@ file = os.path.join(where, "data/rates.csv")
|
|
|
48
49
|
try:
|
|
49
50
|
df = pd.read_csv(file)
|
|
50
51
|
except Exception as e:
|
|
51
|
-
raise RuntimeError(f"Could not find rates data file: {e}")
|
|
52
|
+
raise RuntimeError(f"Could not find rates data file: {e}") from e
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
# Annual rate of return (%) of S&P 500 since 1928, including dividends.
|
|
@@ -82,9 +83,12 @@ def getRatesDistributions(frm, to, mylog=None):
|
|
|
82
83
|
# Convert years to index and check range.
|
|
83
84
|
frm -= FROM
|
|
84
85
|
to -= FROM
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
if not (0 <= frm and frm <= len(SP500)):
|
|
87
|
+
raise ValueError(f"Range 'from' {frm} out of bounds.")
|
|
88
|
+
if not (0 <= to and to <= len(SP500)):
|
|
89
|
+
raise ValueError(f"Range 'to' {to} out of bounds.")
|
|
90
|
+
if frm >= to:
|
|
91
|
+
raise ValueError(f'"from" {frm} must be smaller than "to" {to}.')
|
|
88
92
|
|
|
89
93
|
series = {
|
|
90
94
|
"SP500": SP500,
|
|
@@ -124,9 +128,12 @@ def historicalValue(amount, year):
|
|
|
124
128
|
valued at the beginning of the year specified.
|
|
125
129
|
"""
|
|
126
130
|
thisyear = date.today().year
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
if TO != thisyear - 1:
|
|
132
|
+
raise RuntimeError(f"Rates file needs to be updated to be current to {thisyear}.")
|
|
133
|
+
if year < FROM:
|
|
134
|
+
raise ValueError(f"Only data from {FROM} is available.")
|
|
135
|
+
if year > thisyear:
|
|
136
|
+
raise ValueError(f"Year must be < {thisyear} for historical data.")
|
|
130
137
|
|
|
131
138
|
span = thisyear - year
|
|
132
139
|
ub = len(Inflation)
|
|
@@ -223,18 +230,24 @@ class Rates(object):
|
|
|
223
230
|
self.mylog.vprint("Using conservative fixed rates values:", *[u.pc(k) for k in self.means])
|
|
224
231
|
self._setFixedRates(self._conservRates)
|
|
225
232
|
elif method == "user":
|
|
226
|
-
|
|
227
|
-
|
|
233
|
+
if values is None:
|
|
234
|
+
raise ValueError("Fixed values must be provided with the user option.")
|
|
235
|
+
if len(values) != Nk:
|
|
236
|
+
raise ValueError(f"Values must have {Nk} items.")
|
|
228
237
|
self.means = np.array(values, dtype=float)
|
|
229
238
|
# Convert percent to decimal for storing.
|
|
230
239
|
self.means /= 100.0
|
|
231
240
|
self.mylog.vprint("Setting rates using fixed user values:", *[u.pc(k) for k in self.means])
|
|
232
241
|
self._setFixedRates(self.means)
|
|
233
242
|
elif method == "stochastic":
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
243
|
+
if values is None:
|
|
244
|
+
raise ValueError("Mean values must be provided with the stochastic option.")
|
|
245
|
+
if stdev is None:
|
|
246
|
+
raise ValueError("Standard deviations must be provided with the stochastic option.")
|
|
247
|
+
if len(values) != Nk:
|
|
248
|
+
raise ValueError(f"Values must have {Nk} items.")
|
|
249
|
+
if len(stdev) != Nk:
|
|
250
|
+
raise ValueError(f"stdev must have {Nk} items.")
|
|
238
251
|
self.means = np.array(values, dtype=float)
|
|
239
252
|
self.stdev = np.array(stdev, dtype=float)
|
|
240
253
|
# Convert percent to decimal for storing.
|
|
@@ -262,7 +275,8 @@ class Rates(object):
|
|
|
262
275
|
raise RuntimeError(f"Unable to process correlation shape of {corrarr.shape}.")
|
|
263
276
|
|
|
264
277
|
self.corr = corrarr
|
|
265
|
-
|
|
278
|
+
if not np.array_equal(self.corr, self.corr.T):
|
|
279
|
+
raise ValueError("Correlation matrix must be symmetric.")
|
|
266
280
|
# Now build covariance matrix from stdev and correlation matrix.
|
|
267
281
|
# Multiply each row by a vector element-wise. Then columns.
|
|
268
282
|
covar = self.corr * self.stdev
|
|
@@ -273,10 +287,14 @@ class Rates(object):
|
|
|
273
287
|
self.mylog.vprint("\t and correlation matrix:\n\t\t", str(self.corr).replace("\n", "\n\t\t"))
|
|
274
288
|
else:
|
|
275
289
|
# Then methods relying on historical data range.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
290
|
+
if frm is None:
|
|
291
|
+
raise ValueError("From year must be provided with this option.")
|
|
292
|
+
if not (FROM <= frm <= TO):
|
|
293
|
+
raise ValueError(f"Lower range 'frm={frm}' out of bounds.")
|
|
294
|
+
if not (FROM <= to <= TO):
|
|
295
|
+
raise ValueError(f"Upper range 'to={to}' out of bounds.")
|
|
296
|
+
if not (frm < to):
|
|
297
|
+
raise ValueError("Unacceptable range.")
|
|
280
298
|
self.frm = frm
|
|
281
299
|
self.to = to
|
|
282
300
|
|
|
@@ -300,7 +318,8 @@ class Rates(object):
|
|
|
300
318
|
|
|
301
319
|
def _setFixedRates(self, rates):
|
|
302
320
|
Nk = len(self._defRates)
|
|
303
|
-
|
|
321
|
+
if len(rates) != Nk:
|
|
322
|
+
raise ValueError(f"Rate list provided must have {Nk} entries.")
|
|
304
323
|
self._myRates = np.array(rates)
|
|
305
324
|
self._rateMethod = self._fixedRates
|
|
306
325
|
|
|
@@ -370,43 +389,7 @@ def showRatesDistributions(frm=FROM, to=TO):
|
|
|
370
389
|
"""
|
|
371
390
|
Plot histograms of the rates distributions.
|
|
372
391
|
"""
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
#
|
|
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
|
|
392
|
+
fig = plots.show_rates_distributions(frm, to, SP500, BondsBaa, TNotes, Inflation, FROM)
|
|
393
|
+
return fig
|
|
394
|
+
# plt.show()
|
|
395
|
+
# 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)
|
owlplanner/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2025.05.
|
|
1
|
+
__version__ = "2025.05.05"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
owlplanner/__init__.py,sha256=QJgqS0MrRlBUFuI3PY2LbtD-Xxk1keNnbbPmSsdyZL0,552
|
|
2
|
+
owlplanner/abcapi.py,sha256=m0vtoEzz9HJV7fOK_d7OnK7ha2Qbf7wLLPCJ9YZzR1k,6851
|
|
3
|
+
owlplanner/config.py,sha256=DP-E8EPhkWvMmgPKp26rIrcppkITQyedgLfEyFrySpg,12554
|
|
4
|
+
owlplanner/logging.py,sha256=tYMw04O-XYSzjTj36fmKJGLcE1VkK6k6oJNeqtKXzuc,2530
|
|
5
|
+
owlplanner/plan.py,sha256=9mWj7AXXSTTfaa9bqqUqjPFuiPYf3HtCH31c0Y7Sryg,110615
|
|
6
|
+
owlplanner/plots.py,sha256=0bvKzvPFfUSymWvMV2gGXnQy3-LpjcywYrfJ2-W_8M0,10476
|
|
7
|
+
owlplanner/progress.py,sha256=8jlCvvtgDI89zXVNMBg1-lnEyhpPvKQS2X5oAIpoOVQ,384
|
|
8
|
+
owlplanner/rates.py,sha256=ct-CmRiuxUxslkoBy14j5p19_FesldQiPvMgw470FKE,15108
|
|
9
|
+
owlplanner/tax2025.py,sha256=wmlZpYeeGNrbyn5g7wOFqhWbggppodtHqc-ex5XRooI,7850
|
|
10
|
+
owlplanner/timelists.py,sha256=6eqdnpwmNje9sNw1Hy9Gd-2Wcpgjor1TH4sVnFrpLo4,4033
|
|
11
|
+
owlplanner/utils.py,sha256=WpJgn79YZfH8UCkcmhd-AZlxlGuz1i1-UDBRXImsY6I,2485
|
|
12
|
+
owlplanner/version.py,sha256=suJjWyGl2pv1HKBSdqeb6B361ikXPlXHjEEfihKlKrk,28
|
|
13
|
+
owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
|
|
15
|
+
owlplanner-2025.5.5.dist-info/METADATA,sha256=0QKQkzgOLIwhD32o7WFrIACVzPf15TXHke0uEU--dn0,53926
|
|
16
|
+
owlplanner-2025.5.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
17
|
+
owlplanner-2025.5.5.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
18
|
+
owlplanner-2025.5.5.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=MiKPvyLE6GQCIz4OgmsMCXsAJEEBfGOyjahGtHVvz9U,119073
|
|
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=UedHid5K_TIr2csXNt34h9ScX8Qk40-BCQcUtqPIwG0,28
|
|
12
|
-
owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
|
|
14
|
-
owlplanner-2025.5.2.dist-info/METADATA,sha256=WBjjlcC7pqqncmR2fN4oE6urUT6nLH042AwKwMix2oA,53926
|
|
15
|
-
owlplanner-2025.5.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
16
|
-
owlplanner-2025.5.2.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
17
|
-
owlplanner-2025.5.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|