owlplanner 2025.3.16__tar.gz → 2025.3.30__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.30}/PKG-INFO +1 -1
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/owl.pdf +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/owl.tex +30 -2
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/pyproject.toml +1 -1
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/plan.py +24 -4
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/tax2025.py +1 -1
- owlplanner-2025.3.30/src/owlplanner/version.py +1 -0
- owlplanner-2025.3.30/ttt2.py +24 -0
- owlplanner-2025.3.30/ttt3.py +6 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/About_Owl.py +6 -8
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Documentation.py +10 -8
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Optimization_Parameters.py +16 -17
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Output_Files.py +9 -3
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Quick_Start.py +1 -1
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Rates_Selection.py +1 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Settings.py +1 -1
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Wages_And_Contributions.py +2 -10
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/main.py +3 -2
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/owlbridge.py +14 -2
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/plots.py +5 -3
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/requirements.txt +1 -1
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/sskeys.py +55 -20
- owlplanner-2025.3.30/ui/sskeys.py.color +579 -0
- owlplanner-2025.3.16/src/owlplanner/version.py +0 -1
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/.devcontainer/devcontainer.json +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/.flake8 +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/.gitattributes +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/.github/workflows/github-actions-runtests.yml +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/.gitignore +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/INSTALL.md +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/LICENSE +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/Papers/FE00006821-Class-VI-Injection-Permit--Salient-Features-and-Regulatory-Challenges_Final.pdf +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/Papers/Kou-OptionPricingDouble-2004.pdf +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/Papers/Multi-Period Mean Expected-Shortfall Strategies Cut Your Losses and Ride Your Gains .pdf +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/Papers/Optimal Asset Allocation for Retirement Saving Deterministic Vs. Time Consistent Adaptive Strategies.pdf +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/Papers/Rule-based_strategies_for_dynamic_life_cycle_inves.pdf +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/Papers/s10436-006-0062-y.pdf +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/README.md +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/USER_GUIDE.md +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docker/Dockerfile +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docker/README.md +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docker/docker-compose.yml +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docker/fastentrypoint.sh +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/AD-taxDef.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/AD-taxFree.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/AD-taxable.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/Hist_Bequest.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/Hist_Spending.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/MC-tutorial2a.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/MC-tutorial2b.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/OwlUI.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/allocations.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/owl.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/profile.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/ratesCorrelations.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/ratesPlot.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/savingsPlot.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/sourcesPlot.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/spendingPlot.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/taxIncomePlot.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/taxesPlot.png +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/case_jack+jill.toml +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/case_joe.toml +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/case_john+sally.toml +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/case_jon+jane.toml +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/case_kim+sam-bequest.toml +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/case_kim+sam-spending.toml +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/jack+jill.xlsx +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/joe.xlsx +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/john+sally.xlsx +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/jon+jane.xlsx +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/template.xlsx +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/notebooks/john+sally.ipynb +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/notebooks/kim+sam.ipynb +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/notebooks/template.ipynb +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/notebooks/tutorial_1.ipynb +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/notebooks/tutorial_2.ipynb +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/notebooks/tutorial_3.ipynb +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/owlplanner.cmd +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/owlplanner.sh +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/requirements.txt +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/__init__.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/abcapi.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/config.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/data/__init__.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/data/rates.csv +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/logging.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/progress.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/rates.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/timelists.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/utils.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/tests/test_logger.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/tests/test_regressions.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/tests/test_repro.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/tests/test_toml_cases.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/tests/test_units.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ttt.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Asset_Allocation.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Create_Case.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Current_Assets.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Fixed_Income.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Graphs.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Historical_Range.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Logs.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Monte_Carlo.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/README.md +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Worksheets.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/main+fonts.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/progress.py +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/style.css +0 -0
- {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/tomlexamples.py +0 -0
|
Binary file
|
|
@@ -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,8 @@ 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
|
-
|
|
1200
|
+
# To allow slack in first year, Cg can be made Nn+1 and store basis in g[Nn].
|
|
1201
|
+
A.addNewRow({_q1(Cg, 0, Nn): 1}, spending, spending)
|
|
1195
1202
|
|
|
1196
1203
|
# Set initial balances through constraints.
|
|
1197
1204
|
for i in range(Ni):
|
|
@@ -1297,8 +1304,10 @@ class Plan(object):
|
|
|
1297
1304
|
|
|
1298
1305
|
# Impose income profile.
|
|
1299
1306
|
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,
|
|
1307
|
+
rowDic = {_q1(Cg, 0, Nn): -spLo * self.xiBar_n[n], _q1(Cg, n, Nn): self.xiBar_n[0]}
|
|
1308
|
+
A.addNewRow(rowDic, zero, inf)
|
|
1309
|
+
rowDic = {_q1(Cg, 0, Nn): spHi * self.xiBar_n[n], _q1(Cg, n, Nn): -self.xiBar_n[0]}
|
|
1310
|
+
A.addNewRow(rowDic, zero, inf)
|
|
1302
1311
|
|
|
1303
1312
|
# Taxable ordinary income.
|
|
1304
1313
|
for n in range(Nn):
|
|
@@ -1362,7 +1371,9 @@ class Plan(object):
|
|
|
1362
1371
|
# Now build a solver-neutral objective vector.
|
|
1363
1372
|
c = abc.Objective(self.nvars)
|
|
1364
1373
|
if objective == "maxSpending":
|
|
1365
|
-
c.setElem(_q1(Cg, 0, Nn), -1)
|
|
1374
|
+
# c.setElem(_q1(Cg, 0, Nn), -1) # Only OK in implemention without slack.
|
|
1375
|
+
for n in range(Nn):
|
|
1376
|
+
c.setElem(_q1(Cg, n, Nn), -1/self.gamma_n[n])
|
|
1366
1377
|
elif objective == "maxBequest":
|
|
1367
1378
|
for i in range(Ni):
|
|
1368
1379
|
c.setElem(_q3(Cb, i, 0, Nn, Ni, Nj, Nn + 1), -1)
|
|
@@ -1608,6 +1619,7 @@ class Plan(object):
|
|
|
1608
1619
|
"units",
|
|
1609
1620
|
"maxRothConversion",
|
|
1610
1621
|
"netSpending",
|
|
1622
|
+
"spendingSlack",
|
|
1611
1623
|
"bequest",
|
|
1612
1624
|
"bigM",
|
|
1613
1625
|
"noRothConversions",
|
|
@@ -1642,6 +1654,7 @@ class Plan(object):
|
|
|
1642
1654
|
if objective == "maxSpending" and "bequest" not in myoptions:
|
|
1643
1655
|
self.mylog.vprint("Using bequest of $1.")
|
|
1644
1656
|
|
|
1657
|
+
self.prevMAGI = np.zeros(2)
|
|
1645
1658
|
if "previousMAGIs" in myoptions:
|
|
1646
1659
|
magi = myoptions["previousMAGIs"]
|
|
1647
1660
|
if len(magi) != 2:
|
|
@@ -1653,6 +1666,13 @@ class Plan(object):
|
|
|
1653
1666
|
units = 1000
|
|
1654
1667
|
self.prevMAGI = units * np.array(magi)
|
|
1655
1668
|
|
|
1669
|
+
self.lambdha = 0
|
|
1670
|
+
if "spendingSlack" in myoptions:
|
|
1671
|
+
lambdha = myoptions["spendingSlack"]
|
|
1672
|
+
if lambdha < 0 or lambdha > 50:
|
|
1673
|
+
raise ValueError(f"Slack value out of range {lambdha}.")
|
|
1674
|
+
self.lambdha = lambdha / 100
|
|
1675
|
+
|
|
1656
1676
|
self._adjustParameters()
|
|
1657
1677
|
|
|
1658
1678
|
if "solver" in options:
|
|
@@ -36,7 +36,7 @@ rates_nonTCJA = np.array([0.10, 0.15, 0.25, 0.28, 0.33, 0.35, 0.396])
|
|
|
36
36
|
taxBrackets_TCJA = np.array(
|
|
37
37
|
[
|
|
38
38
|
[11925, 48475, 103350, 197300, 250525, 626350, 9999999],
|
|
39
|
-
[23850, 96950, 206700, 394600, 501050,
|
|
39
|
+
[23850, 96950, 206700, 394600, 501050, 751600, 9999999],
|
|
40
40
|
]
|
|
41
41
|
)
|
|
42
42
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2025.03.30"
|
|
@@ -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()
|
|
@@ -41,12 +41,10 @@ or directly by [email](mailto://martin.d.lacasse@gmail.com).
|
|
|
41
41
|
|
|
42
42
|
#### :orange[Privacy]
|
|
43
43
|
- This app does not store or forward any information. All data entered is lost
|
|
44
|
-
after a session is closed.
|
|
45
|
-
computer before closing the session.
|
|
46
|
-
Source code is publicly available and can be inspected in the repository.
|
|
44
|
+
after a session is closed. You can choose to download selected part of your
|
|
45
|
+
own data to your computer before closing the session.
|
|
47
46
|
|
|
48
|
-
#### :orange[
|
|
49
|
-
-
|
|
50
|
-
- This program comes with no guarantee. Use at your own risk.
|
|
47
|
+
#### :orange[Disclaimer]
|
|
48
|
+
- This program is provided for educational purposes and comes with no guarantee. Use at your own risk.
|
|
51
49
|
"""
|
|
52
50
|
)
|
|
@@ -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")
|
|
@@ -291,6 +291,14 @@ and a time delay, in years from today, before the non-flat behavior starts to ac
|
|
|
291
291
|
Values default to 15%, 12%, and 0 year respectively, but they are fully configurable
|
|
292
292
|
for experimentation and to fit your anticipated lifestyle.
|
|
293
293
|
|
|
294
|
+
A slack variable can also be adjusted. This variable allows the net spending to deviate from
|
|
295
|
+
the desired profile in order to maximize the objective. This is provided mostly for educational purpose
|
|
296
|
+
as maximizing the total net spending will involve leaving the savings invested for as long as possible,
|
|
297
|
+
and therefore this will favor smaller spending early in the plan and larger towards the end.
|
|
298
|
+
This tension between maximizing a dollar amount and the utility of money then becomes evident.
|
|
299
|
+
While the health of the individuals and therefore the utility of money is higher at the beginning
|
|
300
|
+
of retirement, maximizing the total spending or bequest will pull in an opposite direction.
|
|
301
|
+
|
|
294
302
|
For married couples, the survivor's
|
|
295
303
|
net spending percentage is also configurable. A value of 60% is typically used.
|
|
296
304
|
The selected profile multiplies
|
|
@@ -390,12 +398,6 @@ when considering Monte Carlo simulations, consider:
|
|
|
390
398
|
#### Logs
|
|
391
399
|
Messages coming from the underlying Owl calculation engine are displayed on this page.
|
|
392
400
|
|
|
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
401
|
#### Documentation
|
|
400
402
|
These very pages.
|
|
401
403
|
|
|
@@ -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, width="auto"):
|
|
514
|
+
st.html("<style> hr {border-color: %s;width: %s}</style><hr>" % (color, width))
|
|
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,42 @@ 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: 100;
|
|
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", "99%")
|
|
569
|
+
|
|
535
570
|
return ret
|