owlplanner 2025.3.15__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.15 → owlplanner-2025.3.27}/PKG-INFO +1 -1
  2. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/owl.pdf +0 -0
  3. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/owl.tex +58 -25
  4. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/pyproject.toml +1 -1
  5. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/plan.py +23 -4
  6. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/rates.py +3 -0
  7. owlplanner-2025.3.27/src/owlplanner/version.py +1 -0
  8. owlplanner-2025.3.27/ttt2.py +24 -0
  9. owlplanner-2025.3.27/ttt3.py +6 -0
  10. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/About_Owl.py +2 -2
  11. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Documentation.py +2 -8
  12. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Optimization_Parameters.py +16 -17
  13. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Output_Files.py +9 -3
  14. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Quick_Start.py +1 -1
  15. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Rates_Selection.py +1 -0
  16. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Settings.py +1 -1
  17. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Wages_And_Contributions.py +2 -10
  18. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/main.py +3 -2
  19. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/owlbridge.py +14 -2
  20. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/plots.py +5 -3
  21. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/requirements.txt +1 -1
  22. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/sskeys.py +54 -20
  23. owlplanner-2025.3.27/ui/sskeys.py.color +579 -0
  24. owlplanner-2025.3.15/src/owlplanner/version.py +0 -1
  25. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/.devcontainer/devcontainer.json +0 -0
  26. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/.flake8 +0 -0
  27. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/.gitattributes +0 -0
  28. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/.github/workflows/github-actions-runtests.yml +0 -0
  29. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/.gitignore +0 -0
  30. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/INSTALL.md +0 -0
  31. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/LICENSE +0 -0
  32. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/Papers/FE00006821-Class-VI-Injection-Permit--Salient-Features-and-Regulatory-Challenges_Final.pdf +0 -0
  33. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/Papers/Kou-OptionPricingDouble-2004.pdf +0 -0
  34. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/Papers/Multi-Period Mean Expected-Shortfall Strategies Cut Your Losses and Ride Your Gains .pdf +0 -0
  35. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/Papers/Optimal Asset Allocation for Retirement Saving Deterministic Vs. Time Consistent Adaptive Strategies.pdf +0 -0
  36. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/Papers/Rule-based_strategies_for_dynamic_life_cycle_inves.pdf +0 -0
  37. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/Papers/s10436-006-0062-y.pdf +0 -0
  38. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/README.md +0 -0
  39. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/USER_GUIDE.md +0 -0
  40. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docker/Dockerfile +0 -0
  41. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docker/README.md +0 -0
  42. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docker/docker-compose.yml +0 -0
  43. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docker/fastentrypoint.sh +0 -0
  44. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/AD-taxDef.png +0 -0
  45. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/AD-taxFree.png +0 -0
  46. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/AD-taxable.png +0 -0
  47. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/Hist_Bequest.png +0 -0
  48. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/Hist_Spending.png +0 -0
  49. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/MC-tutorial2a.png +0 -0
  50. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/MC-tutorial2b.png +0 -0
  51. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/OwlUI.png +0 -0
  52. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/allocations.png +0 -0
  53. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/owl.png +0 -0
  54. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/profile.png +0 -0
  55. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/ratesCorrelations.png +0 -0
  56. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/ratesPlot.png +0 -0
  57. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/savingsPlot.png +0 -0
  58. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/sourcesPlot.png +0 -0
  59. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/spendingPlot.png +0 -0
  60. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/taxIncomePlot.png +0 -0
  61. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/taxesPlot.png +0 -0
  62. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/case_jack+jill.toml +0 -0
  63. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/case_joe.toml +0 -0
  64. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/case_john+sally.toml +0 -0
  65. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/case_jon+jane.toml +0 -0
  66. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/case_kim+sam-bequest.toml +0 -0
  67. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/case_kim+sam-spending.toml +0 -0
  68. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/jack+jill.xlsx +0 -0
  69. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/joe.xlsx +0 -0
  70. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/john+sally.xlsx +0 -0
  71. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/jon+jane.xlsx +0 -0
  72. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/template.xlsx +0 -0
  73. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/notebooks/john+sally.ipynb +0 -0
  74. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/notebooks/kim+sam.ipynb +0 -0
  75. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/notebooks/template.ipynb +0 -0
  76. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/notebooks/tutorial_1.ipynb +0 -0
  77. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/notebooks/tutorial_2.ipynb +0 -0
  78. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/notebooks/tutorial_3.ipynb +0 -0
  79. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/owlplanner.cmd +0 -0
  80. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/owlplanner.sh +0 -0
  81. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/requirements.txt +0 -0
  82. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/__init__.py +0 -0
  83. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/abcapi.py +0 -0
  84. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/config.py +0 -0
  85. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/data/__init__.py +0 -0
  86. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/data/rates.csv +0 -0
  87. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/logging.py +0 -0
  88. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/progress.py +0 -0
  89. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/tax2025.py +0 -0
  90. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/timelists.py +0 -0
  91. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/utils.py +0 -0
  92. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/tests/test_logger.py +0 -0
  93. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/tests/test_regressions.py +0 -0
  94. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/tests/test_repro.py +0 -0
  95. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/tests/test_toml_cases.py +0 -0
  96. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/tests/test_units.py +0 -0
  97. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ttt.py +0 -0
  98. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Asset_Allocation.py +0 -0
  99. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Create_Case.py +0 -0
  100. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Current_Assets.py +0 -0
  101. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Fixed_Income.py +0 -0
  102. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Graphs.py +0 -0
  103. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Historical_Range.py +0 -0
  104. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Logs.py +0 -0
  105. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Monte_Carlo.py +0 -0
  106. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/README.md +0 -0
  107. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Worksheets.py +0 -0
  108. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/main+fonts.py +0 -0
  109. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/progress.py +0 -0
  110. {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/style.css +0 -0
  111. {owlplanner-2025.3.15 → 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.15
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
@@ -32,6 +32,7 @@
32
32
  \begin{document}
33
33
  \title{Formulation of the optimization model in Owl}
34
34
  \author{Martin-D. Lacasse}
35
+ \date{March 14, 2025}
35
36
  \maketitle
36
37
  \thispagestyle{fancy}
37
38
  \fancyfoot[R]{\copyright\ 2024 - Martin-D. Lacasse}
@@ -112,8 +113,8 @@ to take only non-negative values ($\ge 0$ inequality).
112
113
  \item [$e_{n}$]
113
114
  Standard exemption for year $n$. This is a variable as the taxable income can
114
115
  sometimes be less that the standard exemption $\bar{\sigma}_n$, leading to a
115
- negative taxable income if the inflation adjusted standard deviation is simply subtracted
116
- from gross taxable income $G_n$.
116
+ negative taxable income if the inflation-adjusted standard exemption is simply subtracted
117
+ from the gross taxable income $G_n$.
117
118
  \item [$f_{t n}$]
118
119
  Fraction of tax bracket $t$ filled, so that taxable ordinary income $G_n$ can be expressed as
119
120
  \begin{eqnarray}
@@ -141,19 +142,13 @@ For more easily distinguishing parameters from variables, all parameters will be
141
142
  or using caligraphic fonts.
142
143
  Parameter values are either set by the user, historical data, or by the tax code.
143
144
  \begin{description}[leftmargin=4em,style=multiline]
144
- \item [$\beta{ij}$]
145
+ \item [$\beta_{ij}$]
145
146
  Initial balances in savings accounts. These amounts are used to initialize $b_{ij0}$.
146
147
  \item [$\tau_{kn}$]
147
148
  Annual rate of return for asset class $k$ in year $n$.
148
149
  A time series of annual return rates for each class of asset.
149
150
  Here, inflation and the rate of return of $(k=3)$ cash are assumed to be the same.
150
- In other words, investing in cash yields constant dollars (just inflation).
151
- \item[$\mathcal{T}_{ijn}$]
152
- When the allocation ratios $\alpha_{ijkn}$ are prescribed,
153
- it is more convenient to express the return rates as
154
- \begin{equation}
155
- \mathcal{T}_{ijn} = \sum_k \alpha_{ijkn} \tau_{kn}.
156
- \end{equation}
151
+ In other words, investing in cash yields constant dollars (return just matches inflation).
157
152
  \item [$\gamma_n$]
158
153
  Cumulative inflation at the beginning of year $n$ computed as the product
159
154
  \begin{equation}
@@ -172,12 +167,12 @@ Parameter values are either set by the user, historical data, or by the tax code
172
167
  and can be modified for additional exemptions after 65 of age, for example.
173
168
  It is a simple time series
174
169
  which can include any foreseeable changes in the tax code, or change in filing status due to the
175
- passing of one spouse for $n\ge n_d$. The value of $\bar{\sigma}_n$ is an upper bound for $e_n$.
170
+ passing of one spouse for $n\ge n_d$. The value of $\bar{\sigma}_n$ is an upper bound for variable $e_n$.
176
171
  \item [$\xi_{n}$]
177
- Spending profile. This is a time series that multiplies the desired net spending amount.
172
+ Spending profile. This is a time series that multiplies a basis for the desired net spending amount.
178
173
  It is $\xi_n =1$ for
179
174
  a flat profile, or can be a {\em smile} profile allowing for more money at the start
180
- of retirement. Parameter
175
+ of retirement and modulating it over retirement. Parameter
181
176
  $\xi_n$ can also contain spending adjustments typically made at the passing of one spouse.
182
177
  The {\em smile} can be implemented using a cosine superimposed over a gentle linear increase
183
178
  such as in
@@ -186,14 +181,14 @@ Parameter values are either set by the user, historical data, or by the tax code
186
181
  \end{equation}
187
182
  and then normalized by factor $N_n/(\sum_n \xi_n )$ to be sum-neutral with respect to a flat profile.
188
183
  Values of $a_1 = 15\%$ and $a_2=12\%$ provide curves that are similar to realistic
189
- spending profiles reported in the literature. See Fig.~\ref{Fig:profile} for an example.
184
+ spending profiles reported in the literature. See Fig.~\ref{Fig:profile} for an example.
190
185
  At the passing of one spouse, both profiles are reduced by a factor $\chi$ for $n \ge n_d$,
191
186
  and the normalizing factor needs to be adjusted accordingly.
192
- \begin{figure}[t]
193
- \includegraphics{profile.png}
194
- \caption{\small Example of a spending profile with 15\% cosine factor and a 12\% linear
195
- profile. \label{Fig:profile}}
196
- \end{figure}
187
+ \begin{figure}[t]
188
+ \includegraphics{profile.png}
189
+ \caption{\small Example of a spending profile with 15\% cosine factor and a 12\% linear
190
+ profile. \label{Fig:profile}}
191
+ \end{figure}
197
192
  \item [$\chi$]
198
193
  Factor to reduce spending profile after the passing of one spouse. It is typically
199
194
  assumed to be 0.6.
@@ -230,7 +225,8 @@ Parameter values are either set by the user, historical data, or by the tax code
230
225
  individuals and accounts as $\alpha_{kn}$, for example.
231
226
  When specified by the user, allocation ratios typically involve two values, one at the
232
227
  beginning of the plan $\alpha_{ijk0}$ and the other at the end
233
- $\alpha_{ijkN_{n-1}}$. Then, intermediate values are interpolated either using
228
+ $\alpha_{ijkN_{n-1}}$, or $\alpha_{ijkn_d}$ for a spouse passing before the other.
229
+ Then, intermediate values are interpolated either using
234
230
  a linear relation,
235
231
  \begin{equation}
236
232
  \alpha_{ijkn} = a + \frac{n}{N_n - 1} (b - a),
@@ -280,15 +276,26 @@ or an s-curve as in
280
276
  \end{eqnarray}
281
277
  depending on the scheme selected.
282
278
 
279
+ \item[$\mathcal{T}_{ijn}$]
280
+ When the allocation ratios $\alpha_{ijkn}$ are prescribed,
281
+ it is more convenient to express the return rates as
282
+ \begin{equation}
283
+ \mathcal{T}_{ijn} = \sum_k \alpha_{ijkn} \tau_{kn}.
284
+ \end{equation}
285
+
283
286
  \item [$\Lambda^\pm_{in}$]
284
287
  Big-ticket item requested by individual $i$ in year $n$.
285
288
  These are large expenses or influx of money
286
289
  that can be planned. Therefore, $\Lambda^\pm$ can be positive
287
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
+
288
295
  \item [$\pi_{in}$]
289
296
  Sum of pension benefits for individual $i$ in year $n$. These amounts are typically
290
- specified along with the ages at which these benefits begin. Pensions
291
- can optionally be indexed for inflation.
297
+ specified along with the ages at which these benefits begin.
298
+ Pensions can optionally be indexed for inflation.
292
299
  \item [$\zeta_{in}$]
293
300
  Social security benefits for individual $i$ in year $n$. Starting age and the passing
294
301
  of one individual for spouses will determine the time series. $\bar{\zeta}_{in}$ is
@@ -344,7 +351,8 @@ or an s-curve as in
344
351
  depends on the modified adjusted gross income (MAGI) from 2 years earlier. For the
345
352
  MAGI, we simply use $G_{n-2} + e_{n-2}$ (i.e., gross taxable income
346
353
  plus standard deduction (exemption) from 2 years ago) and ignore the additional IRS
347
- rules around tax-free interests which are insignificant in most cases.
354
+ rules around tax-free interests which are insignificant in most cases. If the plan
355
+ has individuals above 63 years old, values of MAGI for previous years are requested from the user.
348
356
 
349
357
  There are $q=5$ levels
350
358
  of step adjustments adjusted for inflation,
@@ -404,6 +412,7 @@ All intermediate variables are in uppercase letters.
404
412
  [(1-\delta(k, 0))(b_{i0n} - w_{i0n} + d_{in} + .5\kappa_{i0n})\alpha_{i0kn}\tau_{kn}]
405
413
  \end{eqnarray}
406
414
  Social security is indexed for inflation and is assumed to be taxed at 85\%.
415
+ Pensions can optionally be indexed for inflation.
407
416
  We use a discrete Kronecker $\delta$ function for selecting gains from non-equity assets in
408
417
  taxable accounts. These gains are all taxed as ordinary income. Here, we assumed that
409
418
  withdrawals and deposits in the taxable account are taking place at the beginning of the year, while
@@ -1232,14 +1241,38 @@ minimize the inner product $c\cdot y$, where $c$ is
1232
1241
  \end{eqnarray}
1233
1242
  and 0 otherwise. See Eq.~\ref{Eq:C5}.
1234
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
+
1235
1266
  \paragraph*{Maximum bequest}
1236
- 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,
1237
1269
  one would add the following row to $A_ey = v$
1238
1270
  \begin{eqnarray}
1239
1271
  \label{Eq:FixedIncome}
1240
1272
  A_e[I(0), q_g(0)] &=& 1, \nonumber \\
1241
- v[I(0)] &=& g_o.
1273
+ v[I(0)] &=& g_o,
1242
1274
  \end{eqnarray}
1275
+ subject to the net spending $g_n$ obeying Eq.~\ref{Eq:C5} over time.
1243
1276
 
1244
1277
  The objective function would then be derived from Eq.~(\ref{Eq:Bequest}) as
1245
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.15"
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:
@@ -110,6 +110,9 @@ def getRatesDistributions(frm, to, mylog=None):
110
110
  # Build correlation matrix by dividing by the stdev for each column and row.
111
111
  corr = covar / stdev[:, None]
112
112
  corr = corr.T / stdev[:, None]
113
+ # Fold round-off errors in proper bounds.
114
+ corr[corr > 1] = 1
115
+ corr[corr < -1] = -1
113
116
  mylog.print("correlation matrix: \n\t\t%s" % str(corr).replace("\n", "\n\t\t"))
114
117
 
115
118
  return means, stdev, corr, covar
@@ -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.15
10
+ owlplanner >= 2025.03.27