owlplanner 2025.5.5__py3-none-any.whl → 2025.5.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/__init__.py +3 -1
- owlplanner/config.py +2 -2
- owlplanner/plan.py +139 -274
- 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 +3 -14
- owlplanner/timelists.py +2 -7
- owlplanner/version.py +1 -1
- {owlplanner-2025.5.5.dist-info → owlplanner-2025.5.15.dist-info}/METADATA +10 -5
- owlplanner-2025.5.15.dist-info/RECORD +22 -0
- owlplanner/plots.py +0 -296
- owlplanner-2025.5.5.dist-info/RECORD +0 -18
- /owlplanner/{logging.py → mylogging.py} +0 -0
- {owlplanner-2025.5.5.dist-info → owlplanner-2025.5.15.dist-info}/WHEEL +0 -0
- {owlplanner-2025.5.5.dist-info → owlplanner-2025.5.15.dist-info}/licenses/LICENSE +0 -0
owlplanner/rates.py
CHANGED
|
@@ -35,9 +35,8 @@ 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
|
-
from owlplanner import plots
|
|
41
40
|
|
|
42
41
|
# All data goes from 1928 to 2024. Update the TO value when data
|
|
43
42
|
# becomes available for subsequent years.
|
|
@@ -78,7 +77,7 @@ def getRatesDistributions(frm, to, mylog=None):
|
|
|
78
77
|
the different rates. Function returns means and covariance matrix.
|
|
79
78
|
"""
|
|
80
79
|
if mylog is None:
|
|
81
|
-
mylog =
|
|
80
|
+
mylog = log.Logger()
|
|
82
81
|
|
|
83
82
|
# Convert years to index and check range.
|
|
84
83
|
frm -= FROM
|
|
@@ -160,7 +159,7 @@ class Rates(object):
|
|
|
160
159
|
Default constructor.
|
|
161
160
|
"""
|
|
162
161
|
if mylog is None:
|
|
163
|
-
self.mylog =
|
|
162
|
+
self.mylog = log.Logger()
|
|
164
163
|
else:
|
|
165
164
|
self.mylog = mylog
|
|
166
165
|
|
|
@@ -383,13 +382,3 @@ class Rates(object):
|
|
|
383
382
|
srates = np.random.multivariate_normal(self.means, self.covar)
|
|
384
383
|
|
|
385
384
|
return srates
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
def showRatesDistributions(frm=FROM, to=TO):
|
|
389
|
-
"""
|
|
390
|
-
Plot histograms of the rates distributions.
|
|
391
|
-
"""
|
|
392
|
-
fig = plots.show_rates_distributions(frm, to, SP500, BondsBaa, TNotes, Inflation, FROM)
|
|
393
|
-
return fig
|
|
394
|
-
# plt.show()
|
|
395
|
-
# return None
|
owlplanner/timelists.py
CHANGED
|
@@ -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.15"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: owlplanner
|
|
3
|
-
Version: 2025.5.
|
|
3
|
+
Version: 2025.5.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
|
|
@@ -693,8 +693,11 @@ Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
|
693
693
|
Requires-Python: >=3.8
|
|
694
694
|
Requires-Dist: matplotlib
|
|
695
695
|
Requires-Dist: numpy
|
|
696
|
+
Requires-Dist: odfpy
|
|
696
697
|
Requires-Dist: openpyxl
|
|
697
698
|
Requires-Dist: pandas
|
|
699
|
+
Requires-Dist: plotly
|
|
700
|
+
Requires-Dist: pulp
|
|
698
701
|
Requires-Dist: scipy
|
|
699
702
|
Requires-Dist: seaborn
|
|
700
703
|
Requires-Dist: streamlit
|
|
@@ -711,15 +714,17 @@ Description-Content-Type: text/markdown
|
|
|
711
714
|
-------------------------------------------------------------------------------------
|
|
712
715
|
|
|
713
716
|
### TL;DR
|
|
714
|
-
Owl is a retirement planning tool that uses a linear programming
|
|
715
|
-
to provide guidance on retirement decisions
|
|
717
|
+
Owl is a financial retirement planning tool that uses a linear programming
|
|
718
|
+
optimization algorithm to provide guidance on retirement decisions
|
|
719
|
+
such as contributions, withdrawals, Roth conversions, and more.
|
|
716
720
|
Users can select varying return rates to perform historical back testing,
|
|
717
721
|
stochastic rates for performing Monte Carlo analyses,
|
|
718
722
|
or fixed rates either derived from historical averages, or set by the user.
|
|
719
723
|
|
|
720
724
|
There are a few ways to run Owl:
|
|
721
725
|
|
|
722
|
-
- Run Owl directly on the Streamlit Community Server at
|
|
726
|
+
- Run Owl directly on the Streamlit Community Server at
|
|
727
|
+
[owlplanner.streamlit.app](https://owlplanner.streamlit.app).
|
|
723
728
|
|
|
724
729
|
- Run locally on your computer using a Docker image.
|
|
725
730
|
Follow these [instructions](docker/README.md) for this option.
|
|
@@ -729,7 +734,7 @@ Follow these [instructions](INSTALL.md) to install Owl from the source code and
|
|
|
729
734
|
|
|
730
735
|
-------------------------------------------------------------------------------------
|
|
731
736
|
## Overview
|
|
732
|
-
This package is a
|
|
737
|
+
This package is a modeling framework for exploring the sensitivity of retirement financial decisions.
|
|
733
738
|
Strictly speaking, it is not a planning tool, but more an environment for exploring *what if* scenarios.
|
|
734
739
|
It provides different realizations of a financial strategy through the rigorous
|
|
735
740
|
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=qgj6Tm2TUGY2hnWl84UnqBg_PfKL41UcaRHV7ghcCOI,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=mu6V1pH-jOkKVs2QfQVQ_nlYgrniiHk4nFCx_ygJhiE,33036
|
|
19
|
+
owlplanner-2025.5.15.dist-info/METADATA,sha256=kw_BIk15vSKoVPiBCUMl40dVrVPrtoweNhqvIZZP52o,54024
|
|
20
|
+
owlplanner-2025.5.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
+
owlplanner-2025.5.15.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
22
|
+
owlplanner-2025.5.15.dist-info/RECORD,,
|
owlplanner/plots.py
DELETED
|
@@ -1,296 +0,0 @@
|
|
|
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
|
|
@@ -1,18 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|