owlplanner 2025.5.30__tar.gz → 2025.6.3__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.5.30 → owlplanner-2025.6.3}/INSTALL.md +4 -2
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/PKG-INFO +6 -11
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/README.md +5 -10
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/USER_GUIDE.md +1 -1
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/owl.pdf +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/owl.tex +6 -6
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/pyproject.toml +1 -1
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/src/owlplanner/abcapi.py +2 -2
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/src/owlplanner/config.py +3 -2
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/src/owlplanner/mylogging.py +2 -2
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/src/owlplanner/plan.py +19 -19
- owlplanner-2025.6.3/src/owlplanner/plotting/__init__.py +12 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/src/owlplanner/plotting/base.py +5 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/src/owlplanner/plotting/factory.py +5 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/src/owlplanner/plotting/matplotlib_backend.py +10 -3
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/src/owlplanner/plotting/plotly_backend.py +8 -2
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/src/owlplanner/progress.py +4 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/src/owlplanner/rates.py +2 -3
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/src/owlplanner/tax2025.py +5 -4
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/src/owlplanner/timelists.py +5 -4
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/src/owlplanner/utils.py +2 -2
- owlplanner-2025.6.3/src/owlplanner/version.py +1 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/About_Owl.py +3 -2
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/Asset_Allocation.py +22 -15
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/Create_Case.py +4 -3
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/Current_Assets.py +5 -5
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/Documentation.py +91 -87
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/Fixed_Income.py +3 -3
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/Graphs.py +1 -1
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/Historical_Range.py +1 -1
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/Logs.py +1 -1
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/Monte_Carlo.py +1 -1
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/Optimization_Parameters.py +7 -7
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/Output_Files.py +4 -4
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/Quick_Start.py +3 -3
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/Rates_Selection.py +22 -15
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/Settings.py +4 -4
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/Wages_and_Contributions.py +6 -6
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/Worksheets.py +1 -1
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/owlbridge.py +19 -20
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/sskeys.py +5 -5
- owlplanner-2025.5.30/src/owlplanner/plotting/__init__.py +0 -7
- owlplanner-2025.5.30/src/owlplanner/version.py +0 -1
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/.devcontainer/devcontainer.json +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/.flake8 +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/.gitattributes +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/.github/workflows/github-actions-runtests.yml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/.gitignore +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/.streamlit/config.toml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/.streamlit/fullconfig.toml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/LICENSE +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docker/Dockerfile.build +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docker/Dockerfile.run +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docker/README.md +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docker/buildentrypoint.sh +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docker/docker-compose.yml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docker/runentrypoint.sh +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/images/AD-taxDef.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/images/AD-taxFree.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/images/AD-taxable.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/images/Hist_Bequest.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/images/Hist_Spending.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/images/MC-tutorial2a.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/images/MC-tutorial2b.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/images/OwlUI.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/images/allocations.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/images/owl.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/images/profile.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/images/ratesCorrelations.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/images/ratesPlot.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/images/savingsPlot.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/images/sourcesPlot.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/images/spendingPlot.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/images/taxIncomePlot.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/docs/images/taxesPlot.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/examples/case_jack+jill.toml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/examples/case_joe.toml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/examples/case_john+sally.toml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/examples/case_jon+jane.toml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/examples/case_kim+sam-bequest.toml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/examples/case_kim+sam-spending.toml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/examples/jack+jill.xlsx +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/examples/joe.xlsx +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/examples/john+sally.xlsx +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/examples/jon+jane.xlsx +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/examples/template.xlsx +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/notebooks/john+sally.ipynb +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/notebooks/kim+sam.ipynb +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/notebooks/template.ipynb +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/notebooks/tutorial_1.ipynb +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/notebooks/tutorial_2.ipynb +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/notebooks/tutorial_3.ipynb +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/owlplanner.cmd +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/owlplanner.sh +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/pytest.ini +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/requirements.txt +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/src/owlplanner/__init__.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/src/owlplanner/data/__init__.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/src/owlplanner/data/rates.csv +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/tests/test_logger.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/tests/test_regressions.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/tests/test_repro.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/tests/test_toml_cases.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/tests/test_ui_asset_allocation.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/tests/test_ui_compare_summaries.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/tests/test_ui_sskeys.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/tests/test_units.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/README.md +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/__init__.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/main.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/progress.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.3}/ui/tomlexamples.py +0 -0
|
@@ -24,13 +24,15 @@ git clone https://github.com/mdlacasse/Owl.git
|
|
|
24
24
|
|
|
25
25
|
```
|
|
26
26
|
Then go (`cd`) to the directory where you installed Owl.
|
|
27
|
-
You can install the Owl package directly from the [Python Package Index](http://pypi.org).
|
|
28
27
|
From the top directory of the source code run:
|
|
29
28
|
The following command will install the current version of owlplanner and all its dependencies:
|
|
30
29
|
```shell
|
|
31
30
|
pip install -r ui/requirements.txt
|
|
32
31
|
```
|
|
33
32
|
|
|
33
|
+
You can also install the Owl package directly from the [Python Package Index](http://pypi.org).
|
|
34
|
+
|
|
35
|
+
|
|
34
36
|
### Running the streamlit frontend locally
|
|
35
37
|
Running the Owl user interface locally from Windows:
|
|
36
38
|
```shell
|
|
@@ -64,7 +66,7 @@ Run checks before all commits:
|
|
|
64
66
|
flake8 ui src tests
|
|
65
67
|
pytest
|
|
66
68
|
```
|
|
67
|
-
Edit version number in `src/owlplanner/version.py
|
|
69
|
+
Edit version number in `src/owlplanner/version.py` and in `pyproject.toml`. Then,
|
|
68
70
|
```shell
|
|
69
71
|
rm dist/*
|
|
70
72
|
python -m build
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: owlplanner
|
|
3
|
-
Version: 2025.
|
|
3
|
+
Version: 2025.6.3
|
|
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
|
|
@@ -748,13 +748,6 @@ This is exactly where this tool fits it. Given your savings capabilities and spe
|
|
|
748
748
|
it can generate different future realizations of
|
|
749
749
|
your strategy under different market assumptions, helping to better understand your financial situation.
|
|
750
750
|
|
|
751
|
-
Disclaimers: I am not a financial planner. You make your own decisions.
|
|
752
|
-
This program comes with no guarantee. Use at your own risk.
|
|
753
|
-
|
|
754
|
-
More disclaimers: While some output of the code has been verified with other approaches,
|
|
755
|
-
this code is still under development and I cannot guarantee the accuracy of the results.
|
|
756
|
-
Use at your own risk.
|
|
757
|
-
|
|
758
751
|
-------------------------------------------------------------------------------------
|
|
759
752
|
## Purpose and vision
|
|
760
753
|
The goal of Owl is to create a free and open-source ecosystem that has cutting-edge optimization capabilities,
|
|
@@ -839,7 +832,7 @@ an Excel spreadsheet that contains future wages, contributions
|
|
|
839
832
|
to savings accounts, and planned *big-ticket items* such as the purchase of a lake house,
|
|
840
833
|
the sale of a boat, large gifts, or inheritance.
|
|
841
834
|
|
|
842
|
-
Three types of savings accounts are considered: taxable, tax-deferred, and tax-
|
|
835
|
+
Three types of savings accounts are considered: taxable, tax-deferred, and tax-free,
|
|
843
836
|
which are all tracked separately for married individuals. Asset transition to the surviving spouse
|
|
844
837
|
is done according to beneficiary fractions for each type of savings account.
|
|
845
838
|
Tax status covers married filing jointly and single, depending on the number of individuals reported.
|
|
@@ -898,8 +891,10 @@ assets to support, even with no estate being left.
|
|
|
898
891
|
|
|
899
892
|
Copyright © 2024 - Martin-D. Lacasse
|
|
900
893
|
|
|
901
|
-
Disclaimers:
|
|
902
|
-
|
|
894
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
895
|
+
|
|
896
|
+
Code output has been verified with analytical solutions and other approaches.
|
|
897
|
+
Nevertheless, accuracy of results are not guaranteed.
|
|
903
898
|
|
|
904
899
|
--------------------------------------------------------
|
|
905
900
|
|
|
@@ -42,13 +42,6 @@ This is exactly where this tool fits it. Given your savings capabilities and spe
|
|
|
42
42
|
it can generate different future realizations of
|
|
43
43
|
your strategy under different market assumptions, helping to better understand your financial situation.
|
|
44
44
|
|
|
45
|
-
Disclaimers: I am not a financial planner. You make your own decisions.
|
|
46
|
-
This program comes with no guarantee. Use at your own risk.
|
|
47
|
-
|
|
48
|
-
More disclaimers: While some output of the code has been verified with other approaches,
|
|
49
|
-
this code is still under development and I cannot guarantee the accuracy of the results.
|
|
50
|
-
Use at your own risk.
|
|
51
|
-
|
|
52
45
|
-------------------------------------------------------------------------------------
|
|
53
46
|
## Purpose and vision
|
|
54
47
|
The goal of Owl is to create a free and open-source ecosystem that has cutting-edge optimization capabilities,
|
|
@@ -133,7 +126,7 @@ an Excel spreadsheet that contains future wages, contributions
|
|
|
133
126
|
to savings accounts, and planned *big-ticket items* such as the purchase of a lake house,
|
|
134
127
|
the sale of a boat, large gifts, or inheritance.
|
|
135
128
|
|
|
136
|
-
Three types of savings accounts are considered: taxable, tax-deferred, and tax-
|
|
129
|
+
Three types of savings accounts are considered: taxable, tax-deferred, and tax-free,
|
|
137
130
|
which are all tracked separately for married individuals. Asset transition to the surviving spouse
|
|
138
131
|
is done according to beneficiary fractions for each type of savings account.
|
|
139
132
|
Tax status covers married filing jointly and single, depending on the number of individuals reported.
|
|
@@ -192,8 +185,10 @@ assets to support, even with no estate being left.
|
|
|
192
185
|
|
|
193
186
|
Copyright © 2024 - Martin-D. Lacasse
|
|
194
187
|
|
|
195
|
-
Disclaimers:
|
|
196
|
-
|
|
188
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
189
|
+
|
|
190
|
+
Code output has been verified with analytical solutions and other approaches.
|
|
191
|
+
Nevertheless, accuracy of results are not guaranteed.
|
|
197
192
|
|
|
198
193
|
--------------------------------------------------------
|
|
199
194
|
|
|
@@ -20,7 +20,7 @@ import owlplanner as owl
|
|
|
20
20
|
# Jack was born in 1962 and expects to live to age 89. Jill was born in 1965 and hopes to live to age 92.
|
|
21
21
|
# Plan starts on Jan 1st of this year.
|
|
22
22
|
plan = owl.Plan(['Jack', 'Jill'], [1962, 1965], [89, 92], 'jack & jill - tutorial', startDate='01-01')
|
|
23
|
-
# Jack has $90.5k in a taxable investment account, $600.5k in a tax-deferred account and $70k from 2 tax-
|
|
23
|
+
# Jack has $90.5k in a taxable investment account, $600.5k in a tax-deferred account and $70k from 2 tax-free accounts.
|
|
24
24
|
# Jill has $60.2k in her taxable account, $150k in a 403b, and $40k in a Roth IRA.
|
|
25
25
|
plan.setAccountBalances(taxable=[90.5, 60.2], taxDeferred=[600.5, 150], taxFree=[50.6 + 20, 40.8])
|
|
26
26
|
# An Excel file contains 2 tabs (one for Jill, one for Jack) describing anticipated wages and contributions.
|
|
Binary file
|
|
@@ -81,7 +81,7 @@ index name as a subscript, e.g., $N_i$ for index $i$.
|
|
|
81
81
|
is denoted by $i_d$ while the survivor is $i_s$.
|
|
82
82
|
\item [$j$]
|
|
83
83
|
Type of savings account. $j$ goes from 0 to $N_j - 1$, for taxable, tax-deferred,
|
|
84
|
-
and tax-
|
|
84
|
+
and tax-free accounts respectively. Therefore $N_j = 3$.
|
|
85
85
|
\item[$k$]
|
|
86
86
|
Type of asset class. $k$ goes from 0 to $N_k -1 $, for S\&P 500,
|
|
87
87
|
Baa corporate bonds, Treasury notes, and cash, respectively. $N_k = 4$.
|
|
@@ -583,7 +583,7 @@ add the market returns to the savings balances.
|
|
|
583
583
|
Changes include contributions $\kappa$, distributions and withdrawals $w$,
|
|
584
584
|
conversions $x$, surplus deposits $d$, and growth $\tau$ on the account through the year.
|
|
585
585
|
For each spouse $i$, we track each savings account $j$ separately, and tax-deferred accounts
|
|
586
|
-
are coupled to the corresponding tax-
|
|
586
|
+
are coupled to the corresponding tax-free account through Roth conversions.
|
|
587
587
|
|
|
588
588
|
The timing of Roth conversions, withdrawals, and deposits brings
|
|
589
589
|
additional coupling between these variables, and is worth a detailed discussion.
|
|
@@ -605,7 +605,7 @@ add the market returns to the savings balances.
|
|
|
605
605
|
a direct withdrawal from the tax-deferred account at mid-year will always
|
|
606
606
|
be unfavorable when compared to a Roth conversion
|
|
607
607
|
at the beginning of the year, followed
|
|
608
|
-
by a tax-
|
|
608
|
+
by a tax-free withdrawal later in the same year.
|
|
609
609
|
This is because the second
|
|
610
610
|
scenario involves gains which are tax-free over the half-year, while
|
|
611
611
|
the first one does not. Moving account withdrawals at the beginning
|
|
@@ -620,7 +620,7 @@ add the market returns to the savings balances.
|
|
|
620
620
|
\end{eqnarray*}
|
|
621
621
|
for $j \neq 1$, i.e., for all withdrawals except those from tax-deferred accounts.
|
|
622
622
|
For example, to favor tax-deferred withdrawals in most reasonable situations,
|
|
623
|
-
it is desirable to make Roth conversions and tax-
|
|
623
|
+
it is desirable to make Roth conversions and tax-free distributions exclusive events
|
|
624
624
|
by introducing binary variables $z_{in} \in \{0, 1\}$ with the following constraints:
|
|
625
625
|
\begin{alignat}{2}
|
|
626
626
|
\label{Eq:Binary}
|
|
@@ -1087,9 +1087,9 @@ with
|
|
|
1087
1087
|
|
|
1088
1088
|
\section{Other considerations}
|
|
1089
1089
|
\paragraph*{Beneficiaries}
|
|
1090
|
-
Tax-
|
|
1090
|
+
Tax-free and tax-deferred accounts have special tax rules that allow giving part
|
|
1091
1091
|
or the entire value of
|
|
1092
|
-
tax-
|
|
1092
|
+
tax-free accounts to a spouse who can then consider it as his/her own.
|
|
1093
1093
|
These accounts typically use percentages to designate beneficiaries.
|
|
1094
1094
|
Let $\phi_j$ be the fraction of the account $j$ that a spouse $i_d$ wishes
|
|
1095
1095
|
to leave to his/her surviving spouse $i_s$
|
|
@@ -16,9 +16,9 @@ solvers for comparison.
|
|
|
16
16
|
This approach has been successful with the MOSEK and the HiGHS solvers.
|
|
17
17
|
A for matrix, B for bounds, C for constraints. Thus the name ABCAPI.
|
|
18
18
|
|
|
19
|
-
Copyright
|
|
19
|
+
Copyright © 2024 - Martin-D. Lacasse
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
22
22
|
|
|
23
23
|
"""
|
|
24
24
|
|
|
@@ -4,9 +4,10 @@ Owl/conftoml
|
|
|
4
4
|
|
|
5
5
|
This file contains utility functions to save case parameters.
|
|
6
6
|
|
|
7
|
-
Copyright
|
|
7
|
+
Copyright © 2024 - Martin-D. Lacasse
|
|
8
|
+
|
|
9
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
8
10
|
|
|
9
|
-
Disclaimer: This program comes with no guarantee. Use at your own risk.
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
13
|
import toml as toml
|
|
@@ -4,9 +4,9 @@ Owl/logging
|
|
|
4
4
|
|
|
5
5
|
This file contains routines for handling error messages.
|
|
6
6
|
|
|
7
|
-
Copyright
|
|
7
|
+
Copyright © 2024 - Martin-D. Lacasse
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
10
10
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
@@ -8,9 +8,10 @@ A retirement planner using linear programming optimization.
|
|
|
8
8
|
See companion PDF document for an explanation of the underlying
|
|
9
9
|
mathematical model and a description of all variables and parameters.
|
|
10
10
|
|
|
11
|
-
Copyright
|
|
11
|
+
Copyright © 2024 - Martin-D. Lacasse
|
|
12
|
+
|
|
13
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
12
14
|
|
|
13
|
-
Disclaimer: This program comes with no guarantee. Use at your own risk.
|
|
14
15
|
"""
|
|
15
16
|
|
|
16
17
|
###########################################################################
|
|
@@ -307,7 +308,7 @@ class Plan(object):
|
|
|
307
308
|
self.kappa_ijn = np.zeros((self.N_i, self.N_j, self.N_n))
|
|
308
309
|
|
|
309
310
|
# Previous 3 years for Medicare.
|
|
310
|
-
self.prevMAGI = np.zeros((
|
|
311
|
+
self.prevMAGI = np.zeros((2))
|
|
311
312
|
|
|
312
313
|
# Init previous balance to none.
|
|
313
314
|
self.beta_ij = None
|
|
@@ -498,7 +499,7 @@ class Plan(object):
|
|
|
498
499
|
def setBeneficiaryFractions(self, phi):
|
|
499
500
|
"""
|
|
500
501
|
Set fractions of savings accounts that is left to surviving spouse.
|
|
501
|
-
Default is [1, 1, 1] for taxable, tax-deferred,
|
|
502
|
+
Default is [1, 1, 1] for taxable, tax-deferred, and tax-free accounts.
|
|
502
503
|
"""
|
|
503
504
|
if len(phi) != self.N_j:
|
|
504
505
|
raise ValueError(f"Fractions must have {self.N_j} entries.")
|
|
@@ -901,7 +902,7 @@ class Plan(object):
|
|
|
901
902
|
try:
|
|
902
903
|
filename, self.timeLists = timelists.read(filename, self.inames, self.horizons, self.mylog)
|
|
903
904
|
except Exception as e:
|
|
904
|
-
raise Exception(f"Unsuccessful read of
|
|
905
|
+
raise Exception(f"Unsuccessful read of Wages and Contributions: {e}") from e
|
|
905
906
|
|
|
906
907
|
self.timeListsFileName = filename
|
|
907
908
|
self.setContributions()
|
|
@@ -909,6 +910,9 @@ class Plan(object):
|
|
|
909
910
|
return True
|
|
910
911
|
|
|
911
912
|
def setContributions(self, timeLists=None):
|
|
913
|
+
"""
|
|
914
|
+
If no argument is given, use the values that have been stored in self.timeLists.
|
|
915
|
+
"""
|
|
912
916
|
if timeLists is not None:
|
|
913
917
|
timelists.check(timeLists, self.inames, self.horizons)
|
|
914
918
|
self.timeLists = timeLists
|
|
@@ -1074,8 +1078,6 @@ class Plan(object):
|
|
|
1074
1078
|
Utility function that builds constraint matrix and vectors.
|
|
1075
1079
|
Refactored for clarity and maintainability.
|
|
1076
1080
|
"""
|
|
1077
|
-
self._setup_constraint_shortcuts(options)
|
|
1078
|
-
|
|
1079
1081
|
self.A = abc.ConstraintMatrix(self.nvars)
|
|
1080
1082
|
self.B = abc.Bounds(self.nvars, self.nbins)
|
|
1081
1083
|
|
|
@@ -1098,12 +1100,6 @@ class Plan(object):
|
|
|
1098
1100
|
|
|
1099
1101
|
return None
|
|
1100
1102
|
|
|
1101
|
-
def _setup_constraint_shortcuts(self, options):
|
|
1102
|
-
# Set up all the local variables as attributes for use in helpers.
|
|
1103
|
-
oppCostX = options.get("oppCostX", 0.)
|
|
1104
|
-
self.xnet = 1 - oppCostX / 100.
|
|
1105
|
-
self.optionsUnits = u.getUnits(options.get("units", "k"))
|
|
1106
|
-
|
|
1107
1103
|
def _add_rmd_inequalities(self):
|
|
1108
1104
|
for i in range(self.N_i):
|
|
1109
1105
|
if self.beta_ij[i, 1] > 0:
|
|
@@ -1579,14 +1575,18 @@ class Plan(object):
|
|
|
1579
1575
|
if objective == "maxSpending" and "bequest" not in myoptions:
|
|
1580
1576
|
self.mylog.vprint("Using bequest of $1.")
|
|
1581
1577
|
|
|
1582
|
-
self.
|
|
1578
|
+
self.optionsUnits = u.getUnits(myoptions.get("units", "k"))
|
|
1579
|
+
|
|
1580
|
+
oppCostX = options.get("oppCostX", 0.)
|
|
1581
|
+
self.xnet = 1 - oppCostX / 100.
|
|
1582
|
+
|
|
1583
|
+
self.prevMAGI = np.zeros(2)
|
|
1583
1584
|
if "previousMAGIs" in myoptions:
|
|
1584
1585
|
magi = myoptions["previousMAGIs"]
|
|
1585
|
-
if len(magi)
|
|
1586
|
-
raise ValueError("previousMAGIs must have
|
|
1586
|
+
if 3 < len(magi) < 2:
|
|
1587
|
+
raise ValueError("previousMAGIs must have 2 values.")
|
|
1587
1588
|
|
|
1588
|
-
|
|
1589
|
-
self.prevMAGI = units * np.array(magi)
|
|
1589
|
+
self.prevMAGI = self.optionsUnits * np.array(magi)
|
|
1590
1590
|
|
|
1591
1591
|
lambdha = myoptions.get("spendingSlack", 0)
|
|
1592
1592
|
if lambdha < 0 or lambdha > 50:
|
|
@@ -2058,7 +2058,7 @@ class Plan(object):
|
|
|
2058
2058
|
dic = {}
|
|
2059
2059
|
# Results
|
|
2060
2060
|
dic["Plan name"] = self._name
|
|
2061
|
-
dic["Net yearly spending basis"] = u.d(self.g_n[0] / self.xi_n[0])
|
|
2061
|
+
dic["Net yearly spending basis" + 26*" ."] = u.d(self.g_n[0] / self.xi_n[0])
|
|
2062
2062
|
dic[f"Net spending for year {now}"] = u.d(self.g_n[0])
|
|
2063
2063
|
dic[f"Net spending remaining in year {now}"] = u.d(self.g_n[0] * self.yearFracLeft)
|
|
2064
2064
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plotting backends for Owl.
|
|
3
|
+
|
|
4
|
+
Copyright © 2025 - Martin-D. Lacasse
|
|
5
|
+
|
|
6
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .factory import PlotFactory
|
|
11
|
+
|
|
12
|
+
__all__ = ['PlotFactory']
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Matplotlib implementation of plot backend.
|
|
3
|
+
|
|
4
|
+
Copyright © 2025 - Martin-D. Lacasse
|
|
5
|
+
|
|
6
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
7
|
+
|
|
3
8
|
"""
|
|
4
9
|
|
|
5
10
|
import numpy as np
|
|
@@ -372,17 +377,19 @@ class MatplotlibBackend(PlotBackend):
|
|
|
372
377
|
raise ValueError(f"Unknown coordination {ARCoord}.")
|
|
373
378
|
figures = []
|
|
374
379
|
assetDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
|
|
380
|
+
blank = ["", ""]
|
|
375
381
|
for i in range(count):
|
|
376
382
|
y2stack = {}
|
|
377
383
|
for acType in acList:
|
|
378
384
|
stackNames = []
|
|
379
385
|
for key in assetDic:
|
|
380
|
-
aname = key + " / " + acType
|
|
386
|
+
# aname = key + " / " + acType
|
|
387
|
+
aname = key
|
|
381
388
|
stackNames.append(aname)
|
|
382
389
|
y2stack[aname] = np.zeros((count, len(year_n)))
|
|
383
390
|
y2stack[aname][i][:] = alpha_ijkn[i, acList.index(acType), assetDic[key], : len(year_n)]
|
|
384
|
-
t = title + f" - {acType}"
|
|
385
|
-
fig, ax = self._stack_plot(year_n,
|
|
391
|
+
t = title + f" - {acType} {inames[i]}"
|
|
392
|
+
fig, ax = self._stack_plot(year_n, blank, t, [i], y2stack, stackNames, "upper left", "percent")
|
|
386
393
|
figures.append(fig)
|
|
387
394
|
|
|
388
395
|
return figures
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Plotly implementation of plot backend.
|
|
3
|
+
|
|
4
|
+
Copyright © 2025 - Martin-D. Lacasse
|
|
5
|
+
|
|
6
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
7
|
+
|
|
3
8
|
"""
|
|
4
9
|
|
|
5
10
|
import numpy as np
|
|
@@ -815,7 +820,8 @@ class PlotlyBackend(PlotBackend):
|
|
|
815
820
|
stack_data = []
|
|
816
821
|
stack_names = []
|
|
817
822
|
for key in assetDic:
|
|
818
|
-
aname = f"{key} / {acType}"
|
|
823
|
+
# aname = f"{key} / {acType}"
|
|
824
|
+
aname = key
|
|
819
825
|
stack_names.append(aname)
|
|
820
826
|
|
|
821
827
|
# Get allocation data
|
|
@@ -834,7 +840,7 @@ class PlotlyBackend(PlotBackend):
|
|
|
834
840
|
))
|
|
835
841
|
|
|
836
842
|
# Update layout
|
|
837
|
-
plot_title = f"{title} - {acType}"
|
|
843
|
+
plot_title = f"{title} - {acType} {inames[i]}"
|
|
838
844
|
fig.update_layout(
|
|
839
845
|
title=plot_title,
|
|
840
846
|
# xaxis_title="year",
|
|
@@ -21,11 +21,10 @@ Rate lists will need to be updated with values for current year.
|
|
|
21
21
|
When doing so, the TO bound defined below will need to be adjusted
|
|
22
22
|
to the last current data year.
|
|
23
23
|
|
|
24
|
-
Copyright
|
|
24
|
+
Copyright © 2024 - Martin-D. Lacasse
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
27
27
|
|
|
28
|
-
Disclaimer: This program comes with no guarantee. Use at your own risk.
|
|
29
28
|
"""
|
|
30
29
|
|
|
31
30
|
###################################################################
|
|
@@ -10,9 +10,10 @@ of all variables and parameters.
|
|
|
10
10
|
|
|
11
11
|
Module to handle all tax calculations.
|
|
12
12
|
|
|
13
|
-
Copyright
|
|
13
|
+
Copyright © 2024 - Martin-D. Lacasse
|
|
14
|
+
|
|
15
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
14
16
|
|
|
15
|
-
Disclaimer: This program comes with no guarantee. Use at your own risk.
|
|
16
17
|
"""
|
|
17
18
|
|
|
18
19
|
import numpy as np
|
|
@@ -93,9 +94,9 @@ def mediCosts(yobs, horizons, magi, prevmagi, gamma_n, Nn):
|
|
|
93
94
|
status = 0 if Ni == 1 else 1 if n < horizons[0] and n < horizons[1] else 0
|
|
94
95
|
for i in range(Ni):
|
|
95
96
|
if thisyear + n - yobs[i] >= 65 and n < horizons[i]:
|
|
96
|
-
# Start with the (
|
|
97
|
+
# Start with the (inflation-adjusted) basic Medicare part B premium.
|
|
97
98
|
costs[n] += gamma_n[n] * irmaaFees[0]
|
|
98
|
-
if n <
|
|
99
|
+
if n < 2:
|
|
99
100
|
mymagi = prevmagi[n]
|
|
100
101
|
else:
|
|
101
102
|
mymagi = magi[n - 2]
|
|
@@ -10,9 +10,10 @@ of all variables and parameters.
|
|
|
10
10
|
|
|
11
11
|
Utility functions to read and check timelists.
|
|
12
12
|
|
|
13
|
-
Copyright
|
|
13
|
+
Copyright © 2024 - Martin-D. Lacasse
|
|
14
|
+
|
|
15
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
14
16
|
|
|
15
|
-
Disclaimer: This program comes with no guarantee. Use at your own risk.
|
|
16
17
|
"""
|
|
17
18
|
|
|
18
19
|
from datetime import date
|
|
@@ -47,7 +48,7 @@ def read(finput, inames, horizons, mylog):
|
|
|
47
48
|
mylog.vprint("Reading wages, contributions, conversions, and big-ticket items over time...")
|
|
48
49
|
|
|
49
50
|
if isinstance(finput, dict):
|
|
50
|
-
|
|
51
|
+
dfDict = finput
|
|
51
52
|
finput = "dictionary of DataFrames"
|
|
52
53
|
streamName = "dictionary of DataFrames"
|
|
53
54
|
else:
|
|
@@ -58,7 +59,7 @@ def read(finput, inames, horizons, mylog):
|
|
|
58
59
|
raise Exception(f"Could not read file {finput}: {e}.") from e
|
|
59
60
|
streamName = f"file '{finput}'"
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
timeLists = condition(dfDict, inames, horizons, mylog)
|
|
62
63
|
|
|
63
64
|
mylog.vprint(f"Successfully read time horizons from {streamName}.")
|
|
64
65
|
|
|
@@ -4,9 +4,9 @@ Owl/utils
|
|
|
4
4
|
|
|
5
5
|
This file contains functions for handling data.
|
|
6
6
|
|
|
7
|
-
Copyright
|
|
7
|
+
Copyright © 2024 - Martin-D. Lacasse
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
10
10
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2025.06.03"
|
|
@@ -4,7 +4,7 @@ import sskeys as kz
|
|
|
4
4
|
import owlbridge as owb
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
st.write("# About Owl 🦉")
|
|
7
|
+
st.write("# :material/info: About Owl 🦉")
|
|
8
8
|
kz.divider("orange")
|
|
9
9
|
|
|
10
10
|
st.write(f"This is Owl version {owb.version()} running on Streamlit {st.__version__}.")
|
|
@@ -56,6 +56,7 @@ own data to your computer before closing the session.
|
|
|
56
56
|
[Gnu General Public License v3](https://www.gnu.org/licenses/gpl-3.0.html#license-text).
|
|
57
57
|
|
|
58
58
|
#### :orange[Disclaimer]
|
|
59
|
-
|
|
59
|
+
This program is for educatonal purposes only and does not constitute financial advice.
|
|
60
|
+
|
|
60
61
|
"""
|
|
61
62
|
)
|
|
@@ -24,9 +24,8 @@ ASSET = ["S&P 500", "Corp Bonds Baa", "T-Notes", "Cash Assets"]
|
|
|
24
24
|
DEFALLOC = [60, 20, 10, 10]
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def getIndividualAllocs(i, title, deco):
|
|
27
|
+
def getIndividualAllocs(i, iname, title, deco):
|
|
28
28
|
mydeco = "j3_" + deco
|
|
29
|
-
iname = kz.getKey("iname" + str(i))
|
|
30
29
|
st.write(f"###### {iname}'s {title} allocation for all accounts (%)")
|
|
31
30
|
cols = st.columns(4, gap="large", vertical_alignment="top")
|
|
32
31
|
for k1 in range(4):
|
|
@@ -35,8 +34,7 @@ def getIndividualAllocs(i, title, deco):
|
|
|
35
34
|
checkIndividualAllocs(i, mydeco)
|
|
36
35
|
|
|
37
36
|
|
|
38
|
-
def getAccountAllocs(i, j, title, deco):
|
|
39
|
-
iname = kz.getKey("iname" + str(i))
|
|
37
|
+
def getAccountAllocs(i, iname, j, title, deco):
|
|
40
38
|
mydeco = f"j{j}_" + deco
|
|
41
39
|
st.write(f"###### {iname}'s {title} allocation for {ACC[j]} account (%)")
|
|
42
40
|
cols = st.columns(4, gap="large", vertical_alignment="top")
|
|
@@ -81,38 +79,47 @@ def checkAllAllocs():
|
|
|
81
79
|
return result
|
|
82
80
|
|
|
83
81
|
|
|
84
|
-
ret = kz.titleBar("Asset Allocation")
|
|
82
|
+
ret = kz.titleBar(":material/percent: Asset Allocation")
|
|
85
83
|
|
|
86
84
|
if ret is None or kz.caseHasNoPlan():
|
|
87
85
|
st.info("Case(s) must be first created before running this page.")
|
|
88
86
|
else:
|
|
87
|
+
st.write("#### :orange[Type of Allocation]")
|
|
89
88
|
choices = ["individual", "account"]
|
|
90
89
|
key = "allocType"
|
|
91
90
|
kz.initKey(key, choices[0])
|
|
92
91
|
helpmsg = "Allocation ratios can be equal across all accounts or not."
|
|
93
92
|
ret = kz.getRadio("Asset allocation method", choices, key, help=helpmsg)
|
|
93
|
+
st.divider()
|
|
94
94
|
if ret == "individual":
|
|
95
|
+
iname0 = kz.getKey("iname0")
|
|
96
|
+
st.write(f"#### :orange[Individual Asset Allocation ({iname0})]")
|
|
97
|
+
getIndividualAllocs(0, iname0, "initial", "init%")
|
|
98
|
+
getIndividualAllocs(0, iname0, "final", "fin%")
|
|
95
99
|
st.divider()
|
|
96
|
-
getIndividualAllocs(0, "initial", "init%")
|
|
97
|
-
getIndividualAllocs(0, "final", "fin%")
|
|
98
100
|
|
|
99
101
|
if kz.getKey("status") == "married":
|
|
102
|
+
iname1 = kz.getKey("iname1")
|
|
103
|
+
st.write(f"#### :orange[Individual Asset Allocation ({iname1})]")
|
|
104
|
+
getIndividualAllocs(1, iname1, "initial", "init%")
|
|
105
|
+
getIndividualAllocs(1, iname1, "final", "fin%")
|
|
100
106
|
st.divider()
|
|
101
|
-
getIndividualAllocs(1, "initial", "init%")
|
|
102
|
-
getIndividualAllocs(1, "final", "fin%")
|
|
103
107
|
else:
|
|
108
|
+
iname0 = kz.getKey("iname0")
|
|
109
|
+
st.write(f"#### :orange[Account Asset Allocation ({iname0})]")
|
|
104
110
|
for j in range(3):
|
|
111
|
+
getAccountAllocs(0, iname0, j, "initial", "init%")
|
|
112
|
+
getAccountAllocs(0, iname0, j, "final", "fin%")
|
|
105
113
|
st.divider()
|
|
106
|
-
getAccountAllocs(0, j, "initial", "init%")
|
|
107
|
-
getAccountAllocs(0, j, "final", "fin%")
|
|
108
114
|
if kz.getKey("status") == "married":
|
|
109
|
-
|
|
115
|
+
iname1 = kz.getKey("iname1")
|
|
116
|
+
st.write(f"#### :orange[Account Asset Allocation ({iname1})]")
|
|
110
117
|
for j in range(3):
|
|
118
|
+
getAccountAllocs(1, iname1, j, "initial", "init%")
|
|
119
|
+
getAccountAllocs(1, iname1, j, "final", "fin%")
|
|
111
120
|
st.divider()
|
|
112
|
-
getAccountAllocs(1, j, "initial", "init%")
|
|
113
|
-
getAccountAllocs(1, j, "final", "fin%")
|
|
114
121
|
|
|
115
|
-
st.
|
|
122
|
+
st.write("#### :orange[Interpolation]")
|
|
116
123
|
choices = ["linear", "s-curve"]
|
|
117
124
|
key = "interpMethod"
|
|
118
125
|
kz.initKey(key, choices[0])
|