owlplanner 2025.3.16__tar.gz → 2025.3.27__tar.gz
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-2025.3.16 → owlplanner-2025.3.27}/PKG-INFO +1 -1
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/owl.tex +30 -2
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/pyproject.toml +1 -1
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/plan.py +23 -4
- owlplanner-2025.3.27/src/owlplanner/version.py +1 -0
- owlplanner-2025.3.27/ttt2.py +24 -0
- owlplanner-2025.3.27/ttt3.py +6 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/About_Owl.py +2 -2
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Documentation.py +2 -8
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Optimization_Parameters.py +16 -17
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Output_Files.py +9 -3
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Quick_Start.py +1 -1
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Rates_Selection.py +1 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Settings.py +1 -1
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Wages_And_Contributions.py +2 -10
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/main.py +3 -2
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/owlbridge.py +14 -2
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/plots.py +5 -3
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/requirements.txt +1 -1
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/sskeys.py +54 -20
- owlplanner-2025.3.27/ui/sskeys.py.color +579 -0
- owlplanner-2025.3.16/src/owlplanner/version.py +0 -1
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/.devcontainer/devcontainer.json +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/.flake8 +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/.gitattributes +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/.github/workflows/github-actions-runtests.yml +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/.gitignore +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/INSTALL.md +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/LICENSE +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/Papers/FE00006821-Class-VI-Injection-Permit--Salient-Features-and-Regulatory-Challenges_Final.pdf +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/Papers/Kou-OptionPricingDouble-2004.pdf +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/Papers/Multi-Period Mean Expected-Shortfall Strategies Cut Your Losses and Ride Your Gains .pdf +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/Papers/Optimal Asset Allocation for Retirement Saving Deterministic Vs. Time Consistent Adaptive Strategies.pdf +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/Papers/Rule-based_strategies_for_dynamic_life_cycle_inves.pdf +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/Papers/s10436-006-0062-y.pdf +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/README.md +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/USER_GUIDE.md +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docker/Dockerfile +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docker/README.md +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docker/docker-compose.yml +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docker/fastentrypoint.sh +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/AD-taxDef.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/AD-taxFree.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/AD-taxable.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/Hist_Bequest.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/Hist_Spending.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/MC-tutorial2a.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/MC-tutorial2b.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/OwlUI.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/allocations.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/owl.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/profile.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/ratesCorrelations.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/ratesPlot.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/savingsPlot.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/sourcesPlot.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/spendingPlot.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/taxIncomePlot.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/taxesPlot.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/owl.pdf +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/case_jack+jill.toml +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/case_joe.toml +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/case_john+sally.toml +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/case_jon+jane.toml +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/case_kim+sam-bequest.toml +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/case_kim+sam-spending.toml +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/jack+jill.xlsx +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/joe.xlsx +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/john+sally.xlsx +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/jon+jane.xlsx +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/template.xlsx +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/notebooks/john+sally.ipynb +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/notebooks/kim+sam.ipynb +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/notebooks/template.ipynb +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/notebooks/tutorial_1.ipynb +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/notebooks/tutorial_2.ipynb +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/notebooks/tutorial_3.ipynb +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/owlplanner.cmd +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/owlplanner.sh +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/requirements.txt +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/__init__.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/abcapi.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/config.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/data/__init__.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/data/rates.csv +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/logging.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/progress.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/rates.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/tax2025.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/timelists.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/utils.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/tests/test_logger.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/tests/test_regressions.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/tests/test_repro.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/tests/test_toml_cases.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/tests/test_units.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ttt.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Asset_Allocation.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Create_Case.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Current_Assets.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Fixed_Income.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Graphs.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Historical_Range.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Logs.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Monte_Carlo.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/README.md +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Worksheets.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/main+fonts.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/progress.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/style.css +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/tomlexamples.py +0 -0
|
@@ -288,6 +288,10 @@ or an s-curve as in
|
|
|
288
288
|
These are large expenses or influx of money
|
|
289
289
|
that can be planned. Therefore, $\Lambda^\pm$ can be positive
|
|
290
290
|
(e.g., sell a house, inheritance) or negative (e.g., buy a house, large gifts).
|
|
291
|
+
\item [$\lambda$]
|
|
292
|
+
Allowed deviation from the desired net spending profile during one year. Parameter
|
|
293
|
+
$\lambda$ can be better understood as a percentage.
|
|
294
|
+
|
|
291
295
|
\item [$\pi_{in}$]
|
|
292
296
|
Sum of pension benefits for individual $i$ in year $n$. These amounts are typically
|
|
293
297
|
specified along with the ages at which these benefits begin.
|
|
@@ -1237,14 +1241,38 @@ minimize the inner product $c\cdot y$, where $c$ is
|
|
|
1237
1241
|
\end{eqnarray}
|
|
1238
1242
|
and 0 otherwise. See Eq.~\ref{Eq:C5}.
|
|
1239
1243
|
|
|
1244
|
+
\paragraph*{Maximum variable net spending}
|
|
1245
|
+
If instead of maximizing a basis for net spending that is multiplied by a profile $\bar{\xi}_n$,
|
|
1246
|
+
one maximizes the sum of net spending over the full duration
|
|
1247
|
+
of the plan, in today's dollars. In that case, the quantity to optimize is
|
|
1248
|
+
\begin{eqnarray}
|
|
1249
|
+
c[q_g(n)] &=& -1/\gamma_n,
|
|
1250
|
+
\end{eqnarray}
|
|
1251
|
+
and 0 otherwise.
|
|
1252
|
+
In that case, constraint equality Eq.~\ref{Eq:C5} will need to be changed to an inequality.
|
|
1253
|
+
Instead of obeying
|
|
1254
|
+
\begin{equation}
|
|
1255
|
+
g_n \xi_0 - g_0 \bar{\xi}_n = 0,
|
|
1256
|
+
\end{equation}
|
|
1257
|
+
we now impose the following inequality constraint
|
|
1258
|
+
\begin{equation}
|
|
1259
|
+
\label{Eq:C15}
|
|
1260
|
+
(1 - \lambda/100) g_0 \bar{\xi}_n/\xi_0 <= g_n <= (1 + \lambda/100) g_0 \bar{\xi}_n/\xi_0 ,
|
|
1261
|
+
\end{equation}
|
|
1262
|
+
where $\lambda$ is the percentage that the annual net spending is allowed to deviate
|
|
1263
|
+
from the desired profile. It should be noticed that when $\lambda = 0$ the two
|
|
1264
|
+
last equations are equivalent.
|
|
1265
|
+
|
|
1240
1266
|
\paragraph*{Maximum bequest}
|
|
1241
|
-
If, on the other hand, one would like to maximize the bequest under the constraint of a desired
|
|
1267
|
+
If, on the other hand, one would like to maximize the bequest under the constraint of a desired
|
|
1268
|
+
net spending $g_o$, specified for the first year,
|
|
1242
1269
|
one would add the following row to $A_ey = v$
|
|
1243
1270
|
\begin{eqnarray}
|
|
1244
1271
|
\label{Eq:FixedIncome}
|
|
1245
1272
|
A_e[I(0), q_g(0)] &=& 1, \nonumber \\
|
|
1246
|
-
v[I(0)] &=& g_o
|
|
1273
|
+
v[I(0)] &=& g_o,
|
|
1247
1274
|
\end{eqnarray}
|
|
1275
|
+
subject to the net spending $g_n$ obeying Eq.~\ref{Eq:C5} over time.
|
|
1248
1276
|
|
|
1249
1277
|
The objective function would then be derived from Eq.~(\ref{Eq:Bequest}) as
|
|
1250
1278
|
minimizing the inner product $c\cdot y$, where $c$ is
|
|
@@ -307,6 +307,9 @@ class Plan(object):
|
|
|
307
307
|
# Previous 2 years for Medicare.
|
|
308
308
|
self.prevMAGI = np.zeros((2))
|
|
309
309
|
|
|
310
|
+
# Default slack on profile.
|
|
311
|
+
self.lambdha = 0
|
|
312
|
+
|
|
310
313
|
# Scenario starts at the beginning of this year and ends at the end of the last year.
|
|
311
314
|
s = ["", "s"][self.N_i - 1]
|
|
312
315
|
self.mylog.vprint(f"Preparing scenario of {self.N_n} years for {self.N_i} individual{s}.")
|
|
@@ -1071,6 +1074,9 @@ class Plan(object):
|
|
|
1071
1074
|
Cx = self.C["x"]
|
|
1072
1075
|
Cz = self.C["z"]
|
|
1073
1076
|
|
|
1077
|
+
spLo = 1 - self.lambdha
|
|
1078
|
+
spHi = 1 + self.lambdha
|
|
1079
|
+
|
|
1074
1080
|
tau_ijn = np.zeros((Ni, Nj, Nn))
|
|
1075
1081
|
for i in range(Ni):
|
|
1076
1082
|
for j in range(Nj):
|
|
@@ -1191,7 +1197,7 @@ class Plan(object):
|
|
|
1191
1197
|
# Account for time elapsed in the current year.
|
|
1192
1198
|
spending *= units * self.yearFracLeft
|
|
1193
1199
|
# self.mylog.vprint('Maximizing bequest with desired net spending of:', u.d(spending))
|
|
1194
|
-
A.addNewRow({_q1(Cg, 0): 1}, spending, spending)
|
|
1200
|
+
A.addNewRow({_q1(Cg, 0): 1}, spLo * spending, spHi * spending)
|
|
1195
1201
|
|
|
1196
1202
|
# Set initial balances through constraints.
|
|
1197
1203
|
for i in range(Ni):
|
|
@@ -1297,8 +1303,10 @@ class Plan(object):
|
|
|
1297
1303
|
|
|
1298
1304
|
# Impose income profile.
|
|
1299
1305
|
for n in range(1, Nn):
|
|
1300
|
-
rowDic = {_q1(Cg, 0, Nn): -self.xiBar_n[n], _q1(Cg, n, Nn): self.xiBar_n[0]}
|
|
1301
|
-
A.addNewRow(rowDic, zero,
|
|
1306
|
+
rowDic = {_q1(Cg, 0, Nn): -spLo * self.xiBar_n[n], _q1(Cg, n, Nn): self.xiBar_n[0]}
|
|
1307
|
+
A.addNewRow(rowDic, zero, inf)
|
|
1308
|
+
rowDic = {_q1(Cg, 0, Nn): spHi * self.xiBar_n[n], _q1(Cg, n, Nn): -self.xiBar_n[0]}
|
|
1309
|
+
A.addNewRow(rowDic, zero, inf)
|
|
1302
1310
|
|
|
1303
1311
|
# Taxable ordinary income.
|
|
1304
1312
|
for n in range(Nn):
|
|
@@ -1362,7 +1370,9 @@ class Plan(object):
|
|
|
1362
1370
|
# Now build a solver-neutral objective vector.
|
|
1363
1371
|
c = abc.Objective(self.nvars)
|
|
1364
1372
|
if objective == "maxSpending":
|
|
1365
|
-
c.setElem(_q1(Cg, 0, Nn), -1)
|
|
1373
|
+
# c.setElem(_q1(Cg, 0, Nn), -1)
|
|
1374
|
+
for n in range(Nn):
|
|
1375
|
+
c.setElem(_q1(Cg, n, Nn), -1/self.gamma_n[n])
|
|
1366
1376
|
elif objective == "maxBequest":
|
|
1367
1377
|
for i in range(Ni):
|
|
1368
1378
|
c.setElem(_q3(Cb, i, 0, Nn, Ni, Nj, Nn + 1), -1)
|
|
@@ -1608,6 +1618,7 @@ class Plan(object):
|
|
|
1608
1618
|
"units",
|
|
1609
1619
|
"maxRothConversion",
|
|
1610
1620
|
"netSpending",
|
|
1621
|
+
"spendingSlack",
|
|
1611
1622
|
"bequest",
|
|
1612
1623
|
"bigM",
|
|
1613
1624
|
"noRothConversions",
|
|
@@ -1642,6 +1653,7 @@ class Plan(object):
|
|
|
1642
1653
|
if objective == "maxSpending" and "bequest" not in myoptions:
|
|
1643
1654
|
self.mylog.vprint("Using bequest of $1.")
|
|
1644
1655
|
|
|
1656
|
+
self.prevMAGI = np.zeros(2)
|
|
1645
1657
|
if "previousMAGIs" in myoptions:
|
|
1646
1658
|
magi = myoptions["previousMAGIs"]
|
|
1647
1659
|
if len(magi) != 2:
|
|
@@ -1653,6 +1665,13 @@ class Plan(object):
|
|
|
1653
1665
|
units = 1000
|
|
1654
1666
|
self.prevMAGI = units * np.array(magi)
|
|
1655
1667
|
|
|
1668
|
+
self.lambdha = 0
|
|
1669
|
+
if "spendingSlack" in myoptions:
|
|
1670
|
+
lambdha = myoptions["spendingSlack"]
|
|
1671
|
+
if lambdha < 0 or lambdha > 50:
|
|
1672
|
+
raise ValueError(f"Slack value out of range {lambdha}.")
|
|
1673
|
+
self.lambdha = lambdha / 100
|
|
1674
|
+
|
|
1656
1675
|
self._adjustParameters()
|
|
1657
1676
|
|
|
1658
1677
|
if "solver" in options:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2025.03.27"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import streamlit as st
|
|
2
|
+
from streamlit_theme import st_theme
|
|
3
|
+
|
|
4
|
+
header = st.container()
|
|
5
|
+
with header:
|
|
6
|
+
# Print the entire theme dictionary
|
|
7
|
+
st.write("Getting theme settings...")
|
|
8
|
+
|
|
9
|
+
# Get the current theme
|
|
10
|
+
current_theme = st_theme()
|
|
11
|
+
|
|
12
|
+
if current_theme:
|
|
13
|
+
print(current_theme)
|
|
14
|
+
else:
|
|
15
|
+
print('Got none', current_theme)
|
|
16
|
+
|
|
17
|
+
# Check if the theme is dark or light
|
|
18
|
+
# if current_theme["isDark"]:
|
|
19
|
+
# st.write("Current theme is dark")
|
|
20
|
+
# else:
|
|
21
|
+
# st.write("Current theme is light")
|
|
22
|
+
|
|
23
|
+
# Print the entire theme dictionary
|
|
24
|
+
st.write("Current theme settings:", current_theme)
|
|
@@ -4,8 +4,8 @@ import sskeys as kz
|
|
|
4
4
|
import owlbridge as owb
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
st.write("
|
|
8
|
-
kz.
|
|
7
|
+
st.write("# About Owl 🦉")
|
|
8
|
+
kz.divider("orange")
|
|
9
9
|
|
|
10
10
|
st.write(f"This is Owl version {owb.version()} running on Streamlit {st.__version__}.")
|
|
11
11
|
# st.balloons()
|
|
@@ -4,8 +4,8 @@ import sskeys as kz
|
|
|
4
4
|
|
|
5
5
|
col1, col2, col3 = st.columns([0.69, 0.02, 0.29], gap="large")
|
|
6
6
|
with col1:
|
|
7
|
-
st.write("
|
|
8
|
-
kz.
|
|
7
|
+
st.write("# Documentation")
|
|
8
|
+
kz.divider("orange")
|
|
9
9
|
st.write("## Owl Retirement Planner\n-------")
|
|
10
10
|
with col3:
|
|
11
11
|
st.image("http://raw.github.com/mdlacasse/Owl/main/docs/images/owl.png")
|
|
@@ -390,12 +390,6 @@ when considering Monte Carlo simulations, consider:
|
|
|
390
390
|
#### Logs
|
|
391
391
|
Messages coming from the underlying Owl calculation engine are displayed on this page.
|
|
392
392
|
|
|
393
|
-
#### Settings
|
|
394
|
-
This page contains global settings. At the current time, there is only a single
|
|
395
|
-
option for choosing the style used for the graphs. Some color
|
|
396
|
-
schemes are best suited for colorblind individuals. The *classic* offers good contrast, while
|
|
397
|
-
*petroff10* presents other distinguishing colors.
|
|
398
|
-
|
|
399
393
|
#### Documentation
|
|
400
394
|
These very pages.
|
|
401
395
|
|
|
@@ -13,7 +13,7 @@ kz.initKey("smileDelay", 0)
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def initProfile():
|
|
16
|
-
owb.setProfile(
|
|
16
|
+
owb.setProfile(None)
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
ret = kz.titleBar("Optimization Parameters")
|
|
@@ -88,29 +88,28 @@ else:
|
|
|
88
88
|
col1, col2, col3 = st.columns(3, gap="medium", vertical_alignment="top")
|
|
89
89
|
with col1:
|
|
90
90
|
ret = kz.getRadio("Type of profile", profileChoices, "spendingProfile", callback=owb.setProfile)
|
|
91
|
+
if kz.getKey("spendingProfile") == "smile":
|
|
92
|
+
helpmsg = "Time in year before spending starts decreasing."
|
|
93
|
+
ret = kz.getIntNum(
|
|
94
|
+
"Smile delay (in years from now)", "smileDelay", max_value=30, help=helpmsg, callback=owb.setProfile
|
|
95
|
+
)
|
|
91
96
|
with col2:
|
|
97
|
+
kz.initKey("spendingSlack", 0)
|
|
98
|
+
helpmsg = "Percentage allowed to deviate from spending profile."
|
|
99
|
+
ret = kz.getIntNum("Profile slack (%)", "spendingSlack", max_value=50, help=helpmsg)
|
|
100
|
+
if kz.getKey("spendingProfile") == "smile":
|
|
101
|
+
helpmsg = "Percentage to decrease for the slow-go years."
|
|
102
|
+
ret = kz.getIntNum("Smile dip (%)", "smileDip", max_value=100, help=helpmsg, callback=owb.setProfile)
|
|
103
|
+
with col3:
|
|
92
104
|
if kz.getKey("status") == "married":
|
|
93
105
|
helpmsg = "Percentage of spending required for the surviving spouse."
|
|
94
106
|
ret = kz.getIntNum(
|
|
95
107
|
"Survivor's spending (%)", "survivor", max_value=100, help=helpmsg, callback=owb.setProfile
|
|
96
108
|
)
|
|
97
109
|
if kz.getKey("spendingProfile") == "smile":
|
|
98
|
-
helpmsg = "
|
|
99
|
-
ret = kz.getIntNum(
|
|
100
|
-
|
|
101
|
-
)
|
|
102
|
-
with col3:
|
|
103
|
-
helpmsg = "Percentage to decrease for the slow-go years."
|
|
104
|
-
ret = kz.getIntNum("Smile dip (%)", "smileDip", max_value=100, help=helpmsg, callback=owb.setProfile)
|
|
105
|
-
helpmsg = "Percentage to increase (or decrease) over time period."
|
|
106
|
-
ret = kz.getIntNum(
|
|
107
|
-
"Smile increase (%)",
|
|
108
|
-
"smileIncrease",
|
|
109
|
-
min_value=-100,
|
|
110
|
-
max_value=100,
|
|
111
|
-
help=helpmsg,
|
|
112
|
-
callback=owb.setProfile,
|
|
113
|
-
)
|
|
110
|
+
helpmsg = "Percentage to increase (or decrease) over time period."
|
|
111
|
+
ret = kz.getIntNum("Smile increase (%)", "smileIncrease",
|
|
112
|
+
min_value=-100, max_value=100, help=helpmsg, callback=owb.setProfile)
|
|
114
113
|
|
|
115
114
|
st.divider()
|
|
116
115
|
col1, col2 = st.columns(2, gap="small")
|
|
@@ -17,7 +17,9 @@ else:
|
|
|
17
17
|
caseName = kz.getKey("name")
|
|
18
18
|
df = kz.compareSummaries()
|
|
19
19
|
if df is not None:
|
|
20
|
-
st.write("#### Synopsis"
|
|
20
|
+
st.write("#### Synopsis\n"
|
|
21
|
+
"This table provides a summary of the current case and"
|
|
22
|
+
" compares it with other similar cases that ran successfully.")
|
|
21
23
|
styledDf = df[1:].style.map(kz.colorBySign)
|
|
22
24
|
st.dataframe(styledDf, use_container_width=True)
|
|
23
25
|
st.caption("Values with [legend] are nominal, otherwise in today's \\$.")
|
|
@@ -27,7 +29,9 @@ else:
|
|
|
27
29
|
)
|
|
28
30
|
|
|
29
31
|
st.divider()
|
|
30
|
-
st.write("#### Excel workbooks"
|
|
32
|
+
st.write("#### Excel workbooks\n"
|
|
33
|
+
"These workbooks contain time tables describing the flow of money,"
|
|
34
|
+
" the first one as input to the case, and the second as its output.")
|
|
31
35
|
col1, col2 = st.columns(2, gap="large")
|
|
32
36
|
with col1:
|
|
33
37
|
download2 = st.download_button(
|
|
@@ -52,7 +56,9 @@ else:
|
|
|
52
56
|
lines = kz.getKey("casetoml")
|
|
53
57
|
if lines != "":
|
|
54
58
|
st.divider()
|
|
55
|
-
st.write("#### Case parameter file"
|
|
59
|
+
st.write("#### Case parameter file\n"
|
|
60
|
+
"This file contains the parameters characterizing the current case"
|
|
61
|
+
" and can be used, along with the *Wages and Contributions* file, to reproduce it in the future.")
|
|
56
62
|
st.code(lines, language="toml")
|
|
57
63
|
|
|
58
64
|
st.download_button(
|
|
@@ -8,7 +8,7 @@ with col3:
|
|
|
8
8
|
st.caption("Retirement planner with great wisdom")
|
|
9
9
|
with col1:
|
|
10
10
|
st.write("# Owl Retirement Planner\nA retirement exploration tool based on linear programming")
|
|
11
|
-
kz.
|
|
11
|
+
kz.divider("orange")
|
|
12
12
|
st.write("### Quick Start")
|
|
13
13
|
st.markdown(
|
|
14
14
|
"""
|
|
@@ -11,7 +11,7 @@ col1, col2, col3 = st.columns(3, gap="large")
|
|
|
11
11
|
with col1:
|
|
12
12
|
st.write("#### Graphs appearance style")
|
|
13
13
|
key = "plot_style"
|
|
14
|
-
kz.initGlobalKey(key, plots.styles[0])
|
|
14
|
+
kz.initGlobalKey("_"+key, plots.styles[0])
|
|
15
15
|
helpmsg = "Select color style for graphs."
|
|
16
16
|
st.selectbox(
|
|
17
17
|
"Select plot style",
|
|
@@ -4,20 +4,12 @@ import sskeys as kz
|
|
|
4
4
|
import owlbridge as owb
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
# Refresh Wages and Contributions tables as time window likely to change.
|
|
8
|
-
def resetTimeLists():
|
|
9
|
-
# if not kz.getKey("duplicate"):
|
|
10
|
-
tlists = owb.resetContributions()
|
|
11
|
-
for i, iname in enumerate(tlists):
|
|
12
|
-
kz.setKey("timeList" + str(i), tlists[iname])
|
|
13
|
-
|
|
14
|
-
|
|
15
7
|
ret = kz.titleBar("Wages and Contributions")
|
|
16
8
|
|
|
17
9
|
if ret is None or kz.caseHasNoPlan():
|
|
18
10
|
st.info("Case(s) must be first created before running this page.")
|
|
19
11
|
else:
|
|
20
|
-
kz.runOncePerCase(resetTimeLists)
|
|
12
|
+
kz.runOncePerCase(owb.resetTimeLists)
|
|
21
13
|
kz.initKey("stTimeLists", None)
|
|
22
14
|
n = 2 if kz.getKey("status") == "married" else 1
|
|
23
15
|
|
|
@@ -66,4 +58,4 @@ else:
|
|
|
66
58
|
newdf.fillna(0, inplace=True)
|
|
67
59
|
kz.storeKey("_timeList" + str(i), newdf)
|
|
68
60
|
|
|
69
|
-
st.button("Reset to zero", help="Reset all values to zero.", on_click=resetTimeLists)
|
|
61
|
+
st.button("Reset to zero", help="Reset all values to zero.", on_click=owb.resetTimeLists)
|
|
@@ -2,7 +2,7 @@ import streamlit as st
|
|
|
2
2
|
|
|
3
3
|
import sskeys as kz
|
|
4
4
|
|
|
5
|
-
# Pick one for narrow or wide graphs. That can be changed in upper-right settings menu.
|
|
5
|
+
# Pick one for narrow or wide graphs. That can also be changed in upper-right settings menu.
|
|
6
6
|
st.set_page_config(layout="wide", page_title="Owl Retirement Planner")
|
|
7
7
|
# st.set_page_config(layout="centered", page_title="Owl Retirement Planner")
|
|
8
8
|
|
|
@@ -31,7 +31,8 @@ pages = {
|
|
|
31
31
|
],
|
|
32
32
|
"Resources": [
|
|
33
33
|
st.Page("Logs.py", icon=":material/error:"),
|
|
34
|
-
|
|
34
|
+
# Graph style needs a rewrite of plot() to avoid cross-talk between sessions.
|
|
35
|
+
# st.Page("Settings.py", icon=":material/settings:"),
|
|
35
36
|
st.Page("Quick_Start.py", icon=":material/rocket_launch:", default=True),
|
|
36
37
|
st.Page("Documentation.py", icon=":material/help:"),
|
|
37
38
|
st.Page("About_Owl.py", icon=":material/info:"),
|
|
@@ -42,6 +42,10 @@ def createPlan():
|
|
|
42
42
|
val = kz.getKey("plots")
|
|
43
43
|
if val is not None:
|
|
44
44
|
plan.setDefaultPlots(val)
|
|
45
|
+
if kz.getKey("spendingProfile"):
|
|
46
|
+
setProfile(None)
|
|
47
|
+
resetTimeLists()
|
|
48
|
+
|
|
45
49
|
st.toast(f"Created new case *'{name}'*. You can now move to the next page.")
|
|
46
50
|
|
|
47
51
|
|
|
@@ -353,6 +357,12 @@ def resetContributions(plan):
|
|
|
353
357
|
return plan.zeroContributions()
|
|
354
358
|
|
|
355
359
|
|
|
360
|
+
def resetTimeLists():
|
|
361
|
+
tlists = resetContributions()
|
|
362
|
+
for i, iname in enumerate(tlists):
|
|
363
|
+
kz.setKey("timeList" + str(i), tlists[iname])
|
|
364
|
+
|
|
365
|
+
|
|
356
366
|
@_checkPlan
|
|
357
367
|
def setAllocationRatios(plan):
|
|
358
368
|
_setAllocationRatios(plan)
|
|
@@ -434,9 +444,11 @@ def plotSingleResults(plan):
|
|
|
434
444
|
|
|
435
445
|
|
|
436
446
|
@_checkPlan
|
|
437
|
-
def setProfile(plan, key
|
|
438
|
-
if
|
|
447
|
+
def setProfile(plan, key):
|
|
448
|
+
if key is not None:
|
|
439
449
|
kz.setpull(key)
|
|
450
|
+
else:
|
|
451
|
+
kz.flagModified()
|
|
440
452
|
profile = kz.getKey("spendingProfile")
|
|
441
453
|
survivor = kz.getKey("survivor")
|
|
442
454
|
dip = kz.getKey("smileDip")
|
|
@@ -6,10 +6,12 @@ import sskeys as kz
|
|
|
6
6
|
|
|
7
7
|
def changeStyle(key):
|
|
8
8
|
val = kz.getGlobalKey("_" + key)
|
|
9
|
-
kz.
|
|
9
|
+
kz.getGlobalKey(key)
|
|
10
10
|
plt.style.use(val)
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# This makes all graphs appear have the same height.
|
|
14
|
+
plt.rcParams.update({"figure.autolayout": True})
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
styles = ["default"]
|
|
@@ -259,9 +259,13 @@ def storepull(key):
|
|
|
259
259
|
|
|
260
260
|
def setKey(key, val):
|
|
261
261
|
ss.cases[ss.currentCase][key] = val
|
|
262
|
+
flagModified()
|
|
263
|
+
return val
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def flagModified():
|
|
262
267
|
ss.cases[ss.currentCase]["caseStatus"] = "modified"
|
|
263
268
|
ss.cases[ss.currentCase]["summaryDf"] = None
|
|
264
|
-
return val
|
|
265
269
|
|
|
266
270
|
|
|
267
271
|
def storeKey(key, val):
|
|
@@ -356,7 +360,8 @@ def getSolveParameters():
|
|
|
356
360
|
objective = "maxBequest"
|
|
357
361
|
|
|
358
362
|
options = {}
|
|
359
|
-
optList = ["netSpending", "maxRothConversion", "noRothConversions",
|
|
363
|
+
optList = ["netSpending", "maxRothConversion", "noRothConversions",
|
|
364
|
+
"withMedicare", "bequest", "solver", "spendingSlack"]
|
|
360
365
|
for opt in optList:
|
|
361
366
|
val = getKey(opt)
|
|
362
367
|
if val is not None:
|
|
@@ -505,8 +510,16 @@ def getToggle(text, nkey, callback=setpull, disabled=False, help=None):
|
|
|
505
510
|
)
|
|
506
511
|
|
|
507
512
|
|
|
508
|
-
def
|
|
509
|
-
st.html("<style> hr {border-color:
|
|
513
|
+
def divider(color):
|
|
514
|
+
st.html("<style> hr {border-color: %s;}</style><hr>" % color)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def getColors():
|
|
518
|
+
def_theme = "dark"
|
|
519
|
+
bc = "#0E1117" if def_theme == "dark" else "#FFFFFF"
|
|
520
|
+
fc = "#FAFAFA" if def_theme == "dark" else "#31333F"
|
|
521
|
+
|
|
522
|
+
return bc, fc
|
|
510
523
|
|
|
511
524
|
|
|
512
525
|
def titleBar(txt, choices=None):
|
|
@@ -516,20 +529,41 @@ def titleBar(txt, choices=None):
|
|
|
516
529
|
else:
|
|
517
530
|
helpmsg = "Select an existing case, or create a new one from scratch or from a *case* parameter file."
|
|
518
531
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
532
|
+
header = st.container()
|
|
533
|
+
# header.title("Here is a sticky header")
|
|
534
|
+
header.write("""<div class='fixed-header'/>""", unsafe_allow_html=True)
|
|
535
|
+
|
|
536
|
+
bc, fc = getColors()
|
|
537
|
+
|
|
538
|
+
# Custom CSS for the sticky header
|
|
539
|
+
st.markdown(
|
|
540
|
+
"""<style>
|
|
541
|
+
div[data-testid="stVerticalBlock"] div:has(div.fixed-header) {
|
|
542
|
+
border-radius: 10px;
|
|
543
|
+
position: sticky;
|
|
544
|
+
background: linear-gradient(to right, #551b1b, #909090);
|
|
545
|
+
/* background-color: %s; */
|
|
546
|
+
color: %s;
|
|
547
|
+
top: 2.875rem;
|
|
548
|
+
z-index: 999;
|
|
549
|
+
}
|
|
550
|
+
</style>""" % (bc, fc), unsafe_allow_html=True
|
|
551
|
+
)
|
|
552
|
+
with header:
|
|
553
|
+
col1, col2, col3, col4 = st.columns([0.005, 0.6, 0.4, 0.01], gap="small")
|
|
554
|
+
with col2:
|
|
555
|
+
st.write("## " + txt)
|
|
556
|
+
with col3:
|
|
557
|
+
nkey = txt
|
|
558
|
+
ret = st.selectbox(
|
|
559
|
+
"Case selector",
|
|
560
|
+
choices,
|
|
561
|
+
help=helpmsg,
|
|
562
|
+
index=getIndex(currentCaseName(), choices),
|
|
563
|
+
key="_" + nkey,
|
|
564
|
+
on_change=switchToCase,
|
|
565
|
+
args=[nkey],
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
divider("white")
|
|
535
569
|
return ret
|