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.
Files changed (111) hide show
  1. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/PKG-INFO +1 -1
  2. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/owl.tex +30 -2
  3. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/pyproject.toml +1 -1
  4. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/plan.py +23 -4
  5. owlplanner-2025.3.27/src/owlplanner/version.py +1 -0
  6. owlplanner-2025.3.27/ttt2.py +24 -0
  7. owlplanner-2025.3.27/ttt3.py +6 -0
  8. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/About_Owl.py +2 -2
  9. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Documentation.py +2 -8
  10. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Optimization_Parameters.py +16 -17
  11. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Output_Files.py +9 -3
  12. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Quick_Start.py +1 -1
  13. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Rates_Selection.py +1 -0
  14. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Settings.py +1 -1
  15. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Wages_And_Contributions.py +2 -10
  16. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/main.py +3 -2
  17. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/owlbridge.py +14 -2
  18. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/plots.py +5 -3
  19. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/requirements.txt +1 -1
  20. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/sskeys.py +54 -20
  21. owlplanner-2025.3.27/ui/sskeys.py.color +579 -0
  22. owlplanner-2025.3.16/src/owlplanner/version.py +0 -1
  23. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/.devcontainer/devcontainer.json +0 -0
  24. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/.flake8 +0 -0
  25. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/.gitattributes +0 -0
  26. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/.github/workflows/github-actions-runtests.yml +0 -0
  27. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/.gitignore +0 -0
  28. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/INSTALL.md +0 -0
  29. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/LICENSE +0 -0
  30. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/Papers/FE00006821-Class-VI-Injection-Permit--Salient-Features-and-Regulatory-Challenges_Final.pdf +0 -0
  31. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/Papers/Kou-OptionPricingDouble-2004.pdf +0 -0
  32. {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
  33. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/Papers/Optimal Asset Allocation for Retirement Saving Deterministic Vs. Time Consistent Adaptive Strategies.pdf +0 -0
  34. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/Papers/Rule-based_strategies_for_dynamic_life_cycle_inves.pdf +0 -0
  35. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/Papers/s10436-006-0062-y.pdf +0 -0
  36. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/README.md +0 -0
  37. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/USER_GUIDE.md +0 -0
  38. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docker/Dockerfile +0 -0
  39. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docker/README.md +0 -0
  40. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docker/docker-compose.yml +0 -0
  41. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docker/fastentrypoint.sh +0 -0
  42. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/AD-taxDef.png +0 -0
  43. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/AD-taxFree.png +0 -0
  44. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/AD-taxable.png +0 -0
  45. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/Hist_Bequest.png +0 -0
  46. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/Hist_Spending.png +0 -0
  47. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/MC-tutorial2a.png +0 -0
  48. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/MC-tutorial2b.png +0 -0
  49. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/OwlUI.png +0 -0
  50. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/allocations.png +0 -0
  51. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/owl.png +0 -0
  52. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/profile.png +0 -0
  53. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/ratesCorrelations.png +0 -0
  54. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/ratesPlot.png +0 -0
  55. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/savingsPlot.png +0 -0
  56. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/sourcesPlot.png +0 -0
  57. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/spendingPlot.png +0 -0
  58. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/taxIncomePlot.png +0 -0
  59. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/images/taxesPlot.png +0 -0
  60. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/docs/owl.pdf +0 -0
  61. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/case_jack+jill.toml +0 -0
  62. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/case_joe.toml +0 -0
  63. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/case_john+sally.toml +0 -0
  64. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/case_jon+jane.toml +0 -0
  65. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/case_kim+sam-bequest.toml +0 -0
  66. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/case_kim+sam-spending.toml +0 -0
  67. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/jack+jill.xlsx +0 -0
  68. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/joe.xlsx +0 -0
  69. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/john+sally.xlsx +0 -0
  70. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/jon+jane.xlsx +0 -0
  71. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/examples/template.xlsx +0 -0
  72. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/notebooks/john+sally.ipynb +0 -0
  73. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/notebooks/kim+sam.ipynb +0 -0
  74. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/notebooks/template.ipynb +0 -0
  75. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/notebooks/tutorial_1.ipynb +0 -0
  76. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/notebooks/tutorial_2.ipynb +0 -0
  77. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/notebooks/tutorial_3.ipynb +0 -0
  78. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/owlplanner.cmd +0 -0
  79. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/owlplanner.sh +0 -0
  80. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/requirements.txt +0 -0
  81. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/__init__.py +0 -0
  82. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/abcapi.py +0 -0
  83. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/config.py +0 -0
  84. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/data/__init__.py +0 -0
  85. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/data/rates.csv +0 -0
  86. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/logging.py +0 -0
  87. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/progress.py +0 -0
  88. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/rates.py +0 -0
  89. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/tax2025.py +0 -0
  90. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/timelists.py +0 -0
  91. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/src/owlplanner/utils.py +0 -0
  92. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/tests/test_logger.py +0 -0
  93. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/tests/test_regressions.py +0 -0
  94. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/tests/test_repro.py +0 -0
  95. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/tests/test_toml_cases.py +0 -0
  96. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/tests/test_units.py +0 -0
  97. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ttt.py +0 -0
  98. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Asset_Allocation.py +0 -0
  99. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Create_Case.py +0 -0
  100. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Current_Assets.py +0 -0
  101. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Fixed_Income.py +0 -0
  102. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Graphs.py +0 -0
  103. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Historical_Range.py +0 -0
  104. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Logs.py +0 -0
  105. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Monte_Carlo.py +0 -0
  106. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/README.md +0 -0
  107. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/Worksheets.py +0 -0
  108. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/main+fonts.py +0 -0
  109. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/progress.py +0 -0
  110. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/ui/style.css +0 -0
  111. {owlplanner-2025.3.16 → owlplanner-2025.3.27}/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.27
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.27"
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,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, 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)
@@ -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()
@@ -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")
@@ -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(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.27
@@ -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):
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
- 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: 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