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.
Files changed (111) hide show
  1. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/PKG-INFO +1 -1
  2. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/owl.pdf +0 -0
  3. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/owl.tex +30 -2
  4. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/pyproject.toml +1 -1
  5. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/plan.py +24 -4
  6. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/tax2025.py +1 -1
  7. owlplanner-2025.3.30/src/owlplanner/version.py +1 -0
  8. owlplanner-2025.3.30/ttt2.py +24 -0
  9. owlplanner-2025.3.30/ttt3.py +6 -0
  10. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/About_Owl.py +6 -8
  11. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Documentation.py +10 -8
  12. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Optimization_Parameters.py +16 -17
  13. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Output_Files.py +9 -3
  14. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Quick_Start.py +1 -1
  15. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Rates_Selection.py +1 -0
  16. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Settings.py +1 -1
  17. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Wages_And_Contributions.py +2 -10
  18. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/main.py +3 -2
  19. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/owlbridge.py +14 -2
  20. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/plots.py +5 -3
  21. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/requirements.txt +1 -1
  22. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/sskeys.py +55 -20
  23. owlplanner-2025.3.30/ui/sskeys.py.color +579 -0
  24. owlplanner-2025.3.16/src/owlplanner/version.py +0 -1
  25. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/.devcontainer/devcontainer.json +0 -0
  26. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/.flake8 +0 -0
  27. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/.gitattributes +0 -0
  28. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/.github/workflows/github-actions-runtests.yml +0 -0
  29. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/.gitignore +0 -0
  30. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/INSTALL.md +0 -0
  31. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/LICENSE +0 -0
  32. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/Papers/FE00006821-Class-VI-Injection-Permit--Salient-Features-and-Regulatory-Challenges_Final.pdf +0 -0
  33. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/Papers/Kou-OptionPricingDouble-2004.pdf +0 -0
  34. {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
  35. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/Papers/Optimal Asset Allocation for Retirement Saving Deterministic Vs. Time Consistent Adaptive Strategies.pdf +0 -0
  36. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/Papers/Rule-based_strategies_for_dynamic_life_cycle_inves.pdf +0 -0
  37. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/Papers/s10436-006-0062-y.pdf +0 -0
  38. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/README.md +0 -0
  39. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/USER_GUIDE.md +0 -0
  40. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docker/Dockerfile +0 -0
  41. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docker/README.md +0 -0
  42. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docker/docker-compose.yml +0 -0
  43. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docker/fastentrypoint.sh +0 -0
  44. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/AD-taxDef.png +0 -0
  45. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/AD-taxFree.png +0 -0
  46. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/AD-taxable.png +0 -0
  47. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/Hist_Bequest.png +0 -0
  48. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/Hist_Spending.png +0 -0
  49. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/MC-tutorial2a.png +0 -0
  50. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/MC-tutorial2b.png +0 -0
  51. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/OwlUI.png +0 -0
  52. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/allocations.png +0 -0
  53. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/owl.png +0 -0
  54. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/profile.png +0 -0
  55. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/ratesCorrelations.png +0 -0
  56. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/ratesPlot.png +0 -0
  57. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/savingsPlot.png +0 -0
  58. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/sourcesPlot.png +0 -0
  59. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/spendingPlot.png +0 -0
  60. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/taxIncomePlot.png +0 -0
  61. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/docs/images/taxesPlot.png +0 -0
  62. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/case_jack+jill.toml +0 -0
  63. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/case_joe.toml +0 -0
  64. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/case_john+sally.toml +0 -0
  65. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/case_jon+jane.toml +0 -0
  66. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/case_kim+sam-bequest.toml +0 -0
  67. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/case_kim+sam-spending.toml +0 -0
  68. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/jack+jill.xlsx +0 -0
  69. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/joe.xlsx +0 -0
  70. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/john+sally.xlsx +0 -0
  71. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/jon+jane.xlsx +0 -0
  72. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/examples/template.xlsx +0 -0
  73. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/notebooks/john+sally.ipynb +0 -0
  74. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/notebooks/kim+sam.ipynb +0 -0
  75. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/notebooks/template.ipynb +0 -0
  76. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/notebooks/tutorial_1.ipynb +0 -0
  77. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/notebooks/tutorial_2.ipynb +0 -0
  78. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/notebooks/tutorial_3.ipynb +0 -0
  79. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/owlplanner.cmd +0 -0
  80. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/owlplanner.sh +0 -0
  81. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/requirements.txt +0 -0
  82. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/__init__.py +0 -0
  83. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/abcapi.py +0 -0
  84. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/config.py +0 -0
  85. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/data/__init__.py +0 -0
  86. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/data/rates.csv +0 -0
  87. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/logging.py +0 -0
  88. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/progress.py +0 -0
  89. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/rates.py +0 -0
  90. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/timelists.py +0 -0
  91. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/src/owlplanner/utils.py +0 -0
  92. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/tests/test_logger.py +0 -0
  93. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/tests/test_regressions.py +0 -0
  94. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/tests/test_repro.py +0 -0
  95. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/tests/test_toml_cases.py +0 -0
  96. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/tests/test_units.py +0 -0
  97. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ttt.py +0 -0
  98. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Asset_Allocation.py +0 -0
  99. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Create_Case.py +0 -0
  100. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Current_Assets.py +0 -0
  101. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Fixed_Income.py +0 -0
  102. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Graphs.py +0 -0
  103. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Historical_Range.py +0 -0
  104. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Logs.py +0 -0
  105. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Monte_Carlo.py +0 -0
  106. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/README.md +0 -0
  107. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/Worksheets.py +0 -0
  108. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/main+fonts.py +0 -0
  109. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/progress.py +0 -0
  110. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/style.css +0 -0
  111. {owlplanner-2025.3.16 → owlplanner-2025.3.30}/ui/tomlexamples.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owlplanner
3
- Version: 2025.3.16
3
+ Version: 2025.3.30
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
@@ -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 net spending $g_o$,
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "owlplanner"
7
- version = "2025.03.16"
7
+ version = "2025.03.30"
8
8
  authors = [
9
9
  { name="Martin-D. Lacasse", email="martin.d.lacasse@gmail.com" },
10
10
  ]
@@ -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
- A.addNewRow({_q1(Cg, 0): 1}, spending, spending)
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, 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, 751700, 9999999],
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)
@@ -0,0 +1,6 @@
1
+ import streamlit as st
2
+
3
+ bc = st.get_option("theme.backgroundColor")
4
+ tc = st.get_option("theme.textColor")
5
+
6
+ st.write(f"text color is {tc} with background color {bc}")
@@ -4,8 +4,8 @@ import sskeys as kz
4
4
  import owlbridge as owb
5
5
 
6
6
 
7
- st.write("## About Owl 🦉")
8
- kz.orangeDivider()
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. However, you can choose to download selected data to your own
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[Disclaimers]
49
- - I am not a financial planner. You make your own decisions.
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("## Documentation")
8
- kz.orangeDivider()
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(profileChoices[0], False)
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 = "Time in year before spending starts decreasing."
99
- ret = kz.getIntNum(
100
- "Smile delay (in years from now)", "smileDelay", max_value=30, help=helpmsg, callback=owb.setProfile
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.orangeDivider()
11
+ kz.divider("orange")
12
12
  st.write("### Quick Start")
13
13
  st.markdown(
14
14
  """
@@ -41,6 +41,7 @@ def initRates():
41
41
  updateFixedRates(fixedChoices[0], False)
42
42
  else:
43
43
  owb.setRates()
44
+ kz.flagModified()
44
45
 
45
46
 
46
47
  kz.initKey("rateType", rateChoices[0])
@@ -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
- st.Page("Settings.py", icon=":material/settings:"),
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, pull=True):
438
- if pull:
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.storeGlobalKey(key, val)
9
+ kz.getGlobalKey(key)
10
10
  plt.style.use(val)
11
- # This makes all graphs appear have the same height.
12
- plt.rcParams.update({'figure.autolayout': True})
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"]
@@ -7,4 +7,4 @@ scipy
7
7
  streamlit
8
8
  toml
9
9
  # --extra-index-url https://test.pypi.org/simple
10
- owlplanner >= 2025.03.16
10
+ owlplanner >= 2025.03.30
@@ -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", "withMedicare", "bequest", "solver"]
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 orangeDivider():
509
- st.html("<style> hr {border-color: orange;}</style><hr>")
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
- col1, col2 = st.columns(2, gap="large")
520
- with col1:
521
- st.write("## " + txt)
522
- with col2:
523
- nkey = txt
524
- ret = st.selectbox(
525
- "Case selector",
526
- choices,
527
- help=helpmsg,
528
- index=getIndex(currentCaseName(), choices),
529
- key="_" + nkey,
530
- on_change=switchToCase,
531
- args=[nkey],
532
- )
533
-
534
- orangeDivider()
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