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.
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/PKG-INFO +1 -1
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/owl.pdf +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/owl.tex +58 -25
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/pyproject.toml +1 -1
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/plan.py +23 -4
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/rates.py +3 -0
- owlplanner-2025.3.27/src/owlplanner/version.py +1 -0
- owlplanner-2025.3.27/ttt2.py +24 -0
- owlplanner-2025.3.27/ttt3.py +6 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/About_Owl.py +2 -2
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Documentation.py +2 -8
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Optimization_Parameters.py +16 -17
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Output_Files.py +9 -3
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Quick_Start.py +1 -1
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Rates_Selection.py +1 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Settings.py +1 -1
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Wages_And_Contributions.py +2 -10
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/main.py +3 -2
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/owlbridge.py +14 -2
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/plots.py +5 -3
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/requirements.txt +1 -1
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/sskeys.py +54 -20
- owlplanner-2025.3.27/ui/sskeys.py.color +579 -0
- owlplanner-2025.3.15/src/owlplanner/version.py +0 -1
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/.devcontainer/devcontainer.json +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/.flake8 +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/.gitattributes +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/.github/workflows/github-actions-runtests.yml +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/.gitignore +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/INSTALL.md +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/LICENSE +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/Papers/FE00006821-Class-VI-Injection-Permit--Salient-Features-and-Regulatory-Challenges_Final.pdf +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/Papers/Kou-OptionPricingDouble-2004.pdf +0 -0
- {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
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/Papers/Optimal Asset Allocation for Retirement Saving Deterministic Vs. Time Consistent Adaptive Strategies.pdf +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/Papers/Rule-based_strategies_for_dynamic_life_cycle_inves.pdf +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/Papers/s10436-006-0062-y.pdf +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/README.md +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/USER_GUIDE.md +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docker/Dockerfile +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docker/README.md +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docker/docker-compose.yml +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docker/fastentrypoint.sh +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/AD-taxDef.png +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/AD-taxFree.png +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/AD-taxable.png +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/Hist_Bequest.png +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/Hist_Spending.png +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/MC-tutorial2a.png +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/MC-tutorial2b.png +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/OwlUI.png +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/allocations.png +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/owl.png +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/profile.png +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/ratesCorrelations.png +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/ratesPlot.png +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/savingsPlot.png +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/sourcesPlot.png +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/spendingPlot.png +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/taxIncomePlot.png +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/docs/images/taxesPlot.png +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/case_jack+jill.toml +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/case_joe.toml +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/case_john+sally.toml +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/case_jon+jane.toml +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/case_kim+sam-bequest.toml +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/case_kim+sam-spending.toml +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/jack+jill.xlsx +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/joe.xlsx +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/john+sally.xlsx +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/jon+jane.xlsx +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/examples/template.xlsx +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/notebooks/john+sally.ipynb +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/notebooks/kim+sam.ipynb +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/notebooks/template.ipynb +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/notebooks/tutorial_1.ipynb +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/notebooks/tutorial_2.ipynb +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/notebooks/tutorial_3.ipynb +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/owlplanner.cmd +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/owlplanner.sh +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/requirements.txt +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/__init__.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/abcapi.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/config.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/data/__init__.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/data/rates.csv +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/logging.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/progress.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/tax2025.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/timelists.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/src/owlplanner/utils.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/tests/test_logger.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/tests/test_regressions.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/tests/test_repro.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/tests/test_toml_cases.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/tests/test_units.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ttt.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Asset_Allocation.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Create_Case.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Current_Assets.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Fixed_Income.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Graphs.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Historical_Range.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Logs.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Monte_Carlo.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/README.md +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/Worksheets.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/main+fonts.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/progress.py +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/style.css +0 -0
- {owlplanner-2025.3.15 → owlplanner-2025.3.27}/ui/tomlexamples.py +0 -0
|
Binary file
|
|
@@ -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
|
|
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 [$\
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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}}
|
|
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.
|
|
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
|
|
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
|
|
@@ -307,6 +307,9 @@ class Plan(object):
|
|
|
307
307
|
# Previous 2 years for Medicare.
|
|
308
308
|
self.prevMAGI = np.zeros((2))
|
|
309
309
|
|
|
310
|
+
# Default slack on profile.
|
|
311
|
+
self.lambdha = 0
|
|
312
|
+
|
|
310
313
|
# Scenario starts at the beginning of this year and ends at the end of the last year.
|
|
311
314
|
s = ["", "s"][self.N_i - 1]
|
|
312
315
|
self.mylog.vprint(f"Preparing scenario of {self.N_n} years for {self.N_i} individual{s}.")
|
|
@@ -1071,6 +1074,9 @@ class Plan(object):
|
|
|
1071
1074
|
Cx = self.C["x"]
|
|
1072
1075
|
Cz = self.C["z"]
|
|
1073
1076
|
|
|
1077
|
+
spLo = 1 - self.lambdha
|
|
1078
|
+
spHi = 1 + self.lambdha
|
|
1079
|
+
|
|
1074
1080
|
tau_ijn = np.zeros((Ni, Nj, Nn))
|
|
1075
1081
|
for i in range(Ni):
|
|
1076
1082
|
for j in range(Nj):
|
|
@@ -1191,7 +1197,7 @@ class Plan(object):
|
|
|
1191
1197
|
# Account for time elapsed in the current year.
|
|
1192
1198
|
spending *= units * self.yearFracLeft
|
|
1193
1199
|
# self.mylog.vprint('Maximizing bequest with desired net spending of:', u.d(spending))
|
|
1194
|
-
A.addNewRow({_q1(Cg, 0): 1}, spending, spending)
|
|
1200
|
+
A.addNewRow({_q1(Cg, 0): 1}, spLo * spending, spHi * spending)
|
|
1195
1201
|
|
|
1196
1202
|
# Set initial balances through constraints.
|
|
1197
1203
|
for i in range(Ni):
|
|
@@ -1297,8 +1303,10 @@ class Plan(object):
|
|
|
1297
1303
|
|
|
1298
1304
|
# Impose income profile.
|
|
1299
1305
|
for n in range(1, Nn):
|
|
1300
|
-
rowDic = {_q1(Cg, 0, Nn): -self.xiBar_n[n], _q1(Cg, n, Nn): self.xiBar_n[0]}
|
|
1301
|
-
A.addNewRow(rowDic, zero,
|
|
1306
|
+
rowDic = {_q1(Cg, 0, Nn): -spLo * self.xiBar_n[n], _q1(Cg, n, Nn): self.xiBar_n[0]}
|
|
1307
|
+
A.addNewRow(rowDic, zero, inf)
|
|
1308
|
+
rowDic = {_q1(Cg, 0, Nn): spHi * self.xiBar_n[n], _q1(Cg, n, Nn): -self.xiBar_n[0]}
|
|
1309
|
+
A.addNewRow(rowDic, zero, inf)
|
|
1302
1310
|
|
|
1303
1311
|
# Taxable ordinary income.
|
|
1304
1312
|
for n in range(Nn):
|
|
@@ -1362,7 +1370,9 @@ class Plan(object):
|
|
|
1362
1370
|
# Now build a solver-neutral objective vector.
|
|
1363
1371
|
c = abc.Objective(self.nvars)
|
|
1364
1372
|
if objective == "maxSpending":
|
|
1365
|
-
c.setElem(_q1(Cg, 0, Nn), -1)
|
|
1373
|
+
# c.setElem(_q1(Cg, 0, Nn), -1)
|
|
1374
|
+
for n in range(Nn):
|
|
1375
|
+
c.setElem(_q1(Cg, n, Nn), -1/self.gamma_n[n])
|
|
1366
1376
|
elif objective == "maxBequest":
|
|
1367
1377
|
for i in range(Ni):
|
|
1368
1378
|
c.setElem(_q3(Cb, i, 0, Nn, Ni, Nj, Nn + 1), -1)
|
|
@@ -1608,6 +1618,7 @@ class Plan(object):
|
|
|
1608
1618
|
"units",
|
|
1609
1619
|
"maxRothConversion",
|
|
1610
1620
|
"netSpending",
|
|
1621
|
+
"spendingSlack",
|
|
1611
1622
|
"bequest",
|
|
1612
1623
|
"bigM",
|
|
1613
1624
|
"noRothConversions",
|
|
@@ -1642,6 +1653,7 @@ class Plan(object):
|
|
|
1642
1653
|
if objective == "maxSpending" and "bequest" not in myoptions:
|
|
1643
1654
|
self.mylog.vprint("Using bequest of $1.")
|
|
1644
1655
|
|
|
1656
|
+
self.prevMAGI = np.zeros(2)
|
|
1645
1657
|
if "previousMAGIs" in myoptions:
|
|
1646
1658
|
magi = myoptions["previousMAGIs"]
|
|
1647
1659
|
if len(magi) != 2:
|
|
@@ -1653,6 +1665,13 @@ class Plan(object):
|
|
|
1653
1665
|
units = 1000
|
|
1654
1666
|
self.prevMAGI = units * np.array(magi)
|
|
1655
1667
|
|
|
1668
|
+
self.lambdha = 0
|
|
1669
|
+
if "spendingSlack" in myoptions:
|
|
1670
|
+
lambdha = myoptions["spendingSlack"]
|
|
1671
|
+
if lambdha < 0 or lambdha > 50:
|
|
1672
|
+
raise ValueError(f"Slack value out of range {lambdha}.")
|
|
1673
|
+
self.lambdha = lambdha / 100
|
|
1674
|
+
|
|
1656
1675
|
self._adjustParameters()
|
|
1657
1676
|
|
|
1658
1677
|
if "solver" in options:
|
|
@@ -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)
|
|
@@ -4,8 +4,8 @@ import sskeys as kz
|
|
|
4
4
|
import owlbridge as owb
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
st.write("
|
|
8
|
-
kz.
|
|
7
|
+
st.write("# About Owl 🦉")
|
|
8
|
+
kz.divider("orange")
|
|
9
9
|
|
|
10
10
|
st.write(f"This is Owl version {owb.version()} running on Streamlit {st.__version__}.")
|
|
11
11
|
# st.balloons()
|
|
@@ -4,8 +4,8 @@ import sskeys as kz
|
|
|
4
4
|
|
|
5
5
|
col1, col2, col3 = st.columns([0.69, 0.02, 0.29], gap="large")
|
|
6
6
|
with col1:
|
|
7
|
-
st.write("
|
|
8
|
-
kz.
|
|
7
|
+
st.write("# Documentation")
|
|
8
|
+
kz.divider("orange")
|
|
9
9
|
st.write("## Owl Retirement Planner\n-------")
|
|
10
10
|
with col3:
|
|
11
11
|
st.image("http://raw.github.com/mdlacasse/Owl/main/docs/images/owl.png")
|
|
@@ -390,12 +390,6 @@ when considering Monte Carlo simulations, consider:
|
|
|
390
390
|
#### Logs
|
|
391
391
|
Messages coming from the underlying Owl calculation engine are displayed on this page.
|
|
392
392
|
|
|
393
|
-
#### Settings
|
|
394
|
-
This page contains global settings. At the current time, there is only a single
|
|
395
|
-
option for choosing the style used for the graphs. Some color
|
|
396
|
-
schemes are best suited for colorblind individuals. The *classic* offers good contrast, while
|
|
397
|
-
*petroff10* presents other distinguishing colors.
|
|
398
|
-
|
|
399
393
|
#### Documentation
|
|
400
394
|
These very pages.
|
|
401
395
|
|
|
@@ -13,7 +13,7 @@ kz.initKey("smileDelay", 0)
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def initProfile():
|
|
16
|
-
owb.setProfile(
|
|
16
|
+
owb.setProfile(None)
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
ret = kz.titleBar("Optimization Parameters")
|
|
@@ -88,29 +88,28 @@ else:
|
|
|
88
88
|
col1, col2, col3 = st.columns(3, gap="medium", vertical_alignment="top")
|
|
89
89
|
with col1:
|
|
90
90
|
ret = kz.getRadio("Type of profile", profileChoices, "spendingProfile", callback=owb.setProfile)
|
|
91
|
+
if kz.getKey("spendingProfile") == "smile":
|
|
92
|
+
helpmsg = "Time in year before spending starts decreasing."
|
|
93
|
+
ret = kz.getIntNum(
|
|
94
|
+
"Smile delay (in years from now)", "smileDelay", max_value=30, help=helpmsg, callback=owb.setProfile
|
|
95
|
+
)
|
|
91
96
|
with col2:
|
|
97
|
+
kz.initKey("spendingSlack", 0)
|
|
98
|
+
helpmsg = "Percentage allowed to deviate from spending profile."
|
|
99
|
+
ret = kz.getIntNum("Profile slack (%)", "spendingSlack", max_value=50, help=helpmsg)
|
|
100
|
+
if kz.getKey("spendingProfile") == "smile":
|
|
101
|
+
helpmsg = "Percentage to decrease for the slow-go years."
|
|
102
|
+
ret = kz.getIntNum("Smile dip (%)", "smileDip", max_value=100, help=helpmsg, callback=owb.setProfile)
|
|
103
|
+
with col3:
|
|
92
104
|
if kz.getKey("status") == "married":
|
|
93
105
|
helpmsg = "Percentage of spending required for the surviving spouse."
|
|
94
106
|
ret = kz.getIntNum(
|
|
95
107
|
"Survivor's spending (%)", "survivor", max_value=100, help=helpmsg, callback=owb.setProfile
|
|
96
108
|
)
|
|
97
109
|
if kz.getKey("spendingProfile") == "smile":
|
|
98
|
-
helpmsg = "
|
|
99
|
-
ret = kz.getIntNum(
|
|
100
|
-
|
|
101
|
-
)
|
|
102
|
-
with col3:
|
|
103
|
-
helpmsg = "Percentage to decrease for the slow-go years."
|
|
104
|
-
ret = kz.getIntNum("Smile dip (%)", "smileDip", max_value=100, help=helpmsg, callback=owb.setProfile)
|
|
105
|
-
helpmsg = "Percentage to increase (or decrease) over time period."
|
|
106
|
-
ret = kz.getIntNum(
|
|
107
|
-
"Smile increase (%)",
|
|
108
|
-
"smileIncrease",
|
|
109
|
-
min_value=-100,
|
|
110
|
-
max_value=100,
|
|
111
|
-
help=helpmsg,
|
|
112
|
-
callback=owb.setProfile,
|
|
113
|
-
)
|
|
110
|
+
helpmsg = "Percentage to increase (or decrease) over time period."
|
|
111
|
+
ret = kz.getIntNum("Smile increase (%)", "smileIncrease",
|
|
112
|
+
min_value=-100, max_value=100, help=helpmsg, callback=owb.setProfile)
|
|
114
113
|
|
|
115
114
|
st.divider()
|
|
116
115
|
col1, col2 = st.columns(2, gap="small")
|
|
@@ -17,7 +17,9 @@ else:
|
|
|
17
17
|
caseName = kz.getKey("name")
|
|
18
18
|
df = kz.compareSummaries()
|
|
19
19
|
if df is not None:
|
|
20
|
-
st.write("#### Synopsis"
|
|
20
|
+
st.write("#### Synopsis\n"
|
|
21
|
+
"This table provides a summary of the current case and"
|
|
22
|
+
" compares it with other similar cases that ran successfully.")
|
|
21
23
|
styledDf = df[1:].style.map(kz.colorBySign)
|
|
22
24
|
st.dataframe(styledDf, use_container_width=True)
|
|
23
25
|
st.caption("Values with [legend] are nominal, otherwise in today's \\$.")
|
|
@@ -27,7 +29,9 @@ else:
|
|
|
27
29
|
)
|
|
28
30
|
|
|
29
31
|
st.divider()
|
|
30
|
-
st.write("#### Excel workbooks"
|
|
32
|
+
st.write("#### Excel workbooks\n"
|
|
33
|
+
"These workbooks contain time tables describing the flow of money,"
|
|
34
|
+
" the first one as input to the case, and the second as its output.")
|
|
31
35
|
col1, col2 = st.columns(2, gap="large")
|
|
32
36
|
with col1:
|
|
33
37
|
download2 = st.download_button(
|
|
@@ -52,7 +56,9 @@ else:
|
|
|
52
56
|
lines = kz.getKey("casetoml")
|
|
53
57
|
if lines != "":
|
|
54
58
|
st.divider()
|
|
55
|
-
st.write("#### Case parameter file"
|
|
59
|
+
st.write("#### Case parameter file\n"
|
|
60
|
+
"This file contains the parameters characterizing the current case"
|
|
61
|
+
" and can be used, along with the *Wages and Contributions* file, to reproduce it in the future.")
|
|
56
62
|
st.code(lines, language="toml")
|
|
57
63
|
|
|
58
64
|
st.download_button(
|
|
@@ -8,7 +8,7 @@ with col3:
|
|
|
8
8
|
st.caption("Retirement planner with great wisdom")
|
|
9
9
|
with col1:
|
|
10
10
|
st.write("# Owl Retirement Planner\nA retirement exploration tool based on linear programming")
|
|
11
|
-
kz.
|
|
11
|
+
kz.divider("orange")
|
|
12
12
|
st.write("### Quick Start")
|
|
13
13
|
st.markdown(
|
|
14
14
|
"""
|
|
@@ -11,7 +11,7 @@ col1, col2, col3 = st.columns(3, gap="large")
|
|
|
11
11
|
with col1:
|
|
12
12
|
st.write("#### Graphs appearance style")
|
|
13
13
|
key = "plot_style"
|
|
14
|
-
kz.initGlobalKey(key, plots.styles[0])
|
|
14
|
+
kz.initGlobalKey("_"+key, plots.styles[0])
|
|
15
15
|
helpmsg = "Select color style for graphs."
|
|
16
16
|
st.selectbox(
|
|
17
17
|
"Select plot style",
|
|
@@ -4,20 +4,12 @@ import sskeys as kz
|
|
|
4
4
|
import owlbridge as owb
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
# Refresh Wages and Contributions tables as time window likely to change.
|
|
8
|
-
def resetTimeLists():
|
|
9
|
-
# if not kz.getKey("duplicate"):
|
|
10
|
-
tlists = owb.resetContributions()
|
|
11
|
-
for i, iname in enumerate(tlists):
|
|
12
|
-
kz.setKey("timeList" + str(i), tlists[iname])
|
|
13
|
-
|
|
14
|
-
|
|
15
7
|
ret = kz.titleBar("Wages and Contributions")
|
|
16
8
|
|
|
17
9
|
if ret is None or kz.caseHasNoPlan():
|
|
18
10
|
st.info("Case(s) must be first created before running this page.")
|
|
19
11
|
else:
|
|
20
|
-
kz.runOncePerCase(resetTimeLists)
|
|
12
|
+
kz.runOncePerCase(owb.resetTimeLists)
|
|
21
13
|
kz.initKey("stTimeLists", None)
|
|
22
14
|
n = 2 if kz.getKey("status") == "married" else 1
|
|
23
15
|
|
|
@@ -66,4 +58,4 @@ else:
|
|
|
66
58
|
newdf.fillna(0, inplace=True)
|
|
67
59
|
kz.storeKey("_timeList" + str(i), newdf)
|
|
68
60
|
|
|
69
|
-
st.button("Reset to zero", help="Reset all values to zero.", on_click=resetTimeLists)
|
|
61
|
+
st.button("Reset to zero", help="Reset all values to zero.", on_click=owb.resetTimeLists)
|
|
@@ -2,7 +2,7 @@ import streamlit as st
|
|
|
2
2
|
|
|
3
3
|
import sskeys as kz
|
|
4
4
|
|
|
5
|
-
# Pick one for narrow or wide graphs. That can be changed in upper-right settings menu.
|
|
5
|
+
# Pick one for narrow or wide graphs. That can also be changed in upper-right settings menu.
|
|
6
6
|
st.set_page_config(layout="wide", page_title="Owl Retirement Planner")
|
|
7
7
|
# st.set_page_config(layout="centered", page_title="Owl Retirement Planner")
|
|
8
8
|
|
|
@@ -31,7 +31,8 @@ pages = {
|
|
|
31
31
|
],
|
|
32
32
|
"Resources": [
|
|
33
33
|
st.Page("Logs.py", icon=":material/error:"),
|
|
34
|
-
|
|
34
|
+
# Graph style needs a rewrite of plot() to avoid cross-talk between sessions.
|
|
35
|
+
# st.Page("Settings.py", icon=":material/settings:"),
|
|
35
36
|
st.Page("Quick_Start.py", icon=":material/rocket_launch:", default=True),
|
|
36
37
|
st.Page("Documentation.py", icon=":material/help:"),
|
|
37
38
|
st.Page("About_Owl.py", icon=":material/info:"),
|
|
@@ -42,6 +42,10 @@ def createPlan():
|
|
|
42
42
|
val = kz.getKey("plots")
|
|
43
43
|
if val is not None:
|
|
44
44
|
plan.setDefaultPlots(val)
|
|
45
|
+
if kz.getKey("spendingProfile"):
|
|
46
|
+
setProfile(None)
|
|
47
|
+
resetTimeLists()
|
|
48
|
+
|
|
45
49
|
st.toast(f"Created new case *'{name}'*. You can now move to the next page.")
|
|
46
50
|
|
|
47
51
|
|
|
@@ -353,6 +357,12 @@ def resetContributions(plan):
|
|
|
353
357
|
return plan.zeroContributions()
|
|
354
358
|
|
|
355
359
|
|
|
360
|
+
def resetTimeLists():
|
|
361
|
+
tlists = resetContributions()
|
|
362
|
+
for i, iname in enumerate(tlists):
|
|
363
|
+
kz.setKey("timeList" + str(i), tlists[iname])
|
|
364
|
+
|
|
365
|
+
|
|
356
366
|
@_checkPlan
|
|
357
367
|
def setAllocationRatios(plan):
|
|
358
368
|
_setAllocationRatios(plan)
|
|
@@ -434,9 +444,11 @@ def plotSingleResults(plan):
|
|
|
434
444
|
|
|
435
445
|
|
|
436
446
|
@_checkPlan
|
|
437
|
-
def setProfile(plan, key
|
|
438
|
-
if
|
|
447
|
+
def setProfile(plan, key):
|
|
448
|
+
if key is not None:
|
|
439
449
|
kz.setpull(key)
|
|
450
|
+
else:
|
|
451
|
+
kz.flagModified()
|
|
440
452
|
profile = kz.getKey("spendingProfile")
|
|
441
453
|
survivor = kz.getKey("survivor")
|
|
442
454
|
dip = kz.getKey("smileDip")
|
|
@@ -6,10 +6,12 @@ import sskeys as kz
|
|
|
6
6
|
|
|
7
7
|
def changeStyle(key):
|
|
8
8
|
val = kz.getGlobalKey("_" + key)
|
|
9
|
-
kz.
|
|
9
|
+
kz.getGlobalKey(key)
|
|
10
10
|
plt.style.use(val)
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# This makes all graphs appear have the same height.
|
|
14
|
+
plt.rcParams.update({"figure.autolayout": True})
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
styles = ["default"]
|