owlplanner 2025.5.30__tar.gz → 2025.6.21__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.21}/INSTALL.md +4 -2
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/PKG-INFO +9 -12
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/README.md +8 -11
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/USER_GUIDE.md +1 -1
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/owl.pdf +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/owl.tex +25 -7
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/pyproject.toml +1 -1
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/requirements.txt +1 -1
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/abcapi.py +2 -2
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/config.py +3 -2
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/mylogging.py +2 -2
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/plan.py +82 -38
- owlplanner-2025.6.21/src/owlplanner/plotting/__init__.py +12 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/plotting/base.py +5 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/plotting/factory.py +5 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/plotting/matplotlib_backend.py +10 -3
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/plotting/plotly_backend.py +8 -2
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/progress.py +4 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/rates.py +2 -3
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/tax2025.py +5 -4
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/timelists.py +13 -12
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/utils.py +2 -2
- owlplanner-2025.6.21/src/owlplanner/version.py +1 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/tests/test_repro.py +15 -15
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/About_Owl.py +9 -7
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Asset_Allocation.py +22 -15
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Create_Case.py +4 -3
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Current_Assets.py +18 -12
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Documentation.py +154 -121
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Fixed_Income.py +3 -3
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Graphs.py +1 -1
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Historical_Range.py +1 -1
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Logs.py +1 -1
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Monte_Carlo.py +1 -1
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Optimization_Parameters.py +8 -8
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Output_Files.py +16 -16
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Quick_Start.py +7 -7
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Rates_Selection.py +22 -15
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Settings.py +4 -4
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Wages_and_Contributions.py +74 -72
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Worksheets.py +1 -1
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/main.py +1 -1
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/owlbridge.py +21 -22
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/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.21}/.devcontainer/devcontainer.json +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/.flake8 +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/.gitattributes +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/.github/workflows/github-actions-runtests.yml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/.gitignore +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/.streamlit/config.toml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/.streamlit/fullconfig.toml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/LICENSE +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docker/Dockerfile.build +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docker/Dockerfile.run +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docker/README.md +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docker/buildentrypoint.sh +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docker/docker-compose.yml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docker/runentrypoint.sh +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/AD-taxDef.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/AD-taxFree.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/AD-taxable.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/Hist_Bequest.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/Hist_Spending.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/MC-tutorial2a.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/MC-tutorial2b.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/OwlUI.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/allocations.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/owl.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/profile.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/ratesCorrelations.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/ratesPlot.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/savingsPlot.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/sourcesPlot.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/spendingPlot.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/taxIncomePlot.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/taxesPlot.png +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/case_jack+jill.toml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/case_joe.toml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/case_john+sally.toml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/case_jon+jane.toml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/case_kim+sam-bequest.toml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/case_kim+sam-spending.toml +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/jack+jill.xlsx +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/joe.xlsx +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/john+sally.xlsx +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/jon+jane.xlsx +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/template.xlsx +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/notebooks/john+sally.ipynb +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/notebooks/kim+sam.ipynb +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/notebooks/template.ipynb +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/notebooks/tutorial_1.ipynb +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/notebooks/tutorial_2.ipynb +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/notebooks/tutorial_3.ipynb +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/owlplanner.cmd +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/owlplanner.sh +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/pytest.ini +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/__init__.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/data/__init__.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/data/rates.csv +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/tests/test_logger.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/tests/test_regressions.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/tests/test_toml_cases.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/tests/test_ui_asset_allocation.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/tests/test_ui_compare_summaries.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/tests/test_ui_sskeys.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/tests/test_units.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/README.md +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/__init__.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/progress.py +0 -0
- {owlplanner-2025.5.30 → owlplanner-2025.6.21}/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.21
|
|
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,11 +832,13 @@ 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.
|
|
846
839
|
|
|
840
|
+
Maturation rules for Roth contributions and conversions are implemented as constraints
|
|
841
|
+
limiting withdrawal amounts to cover Roth account balances for 5 years after the events.
|
|
847
842
|
Medicare and IRMAA calculations are performed through a self-consistent loop on cash flow constraints.
|
|
848
843
|
Future values are simple projections of current values with the assumed inflation rates.
|
|
849
844
|
|
|
@@ -892,14 +887,16 @@ assets to support, even with no estate being left.
|
|
|
892
887
|
- Streamlit Community Cloud [Streamlit](https://streamlit.io)
|
|
893
888
|
- Contributors: Josh (noimjosh@gmail.com) for Docker image code,
|
|
894
889
|
Dale Seng (sengsational) for great insights and suggestions,
|
|
895
|
-
Robert E. Anderson (NH-RedAnt) for bug fixes and suggestions.
|
|
890
|
+
Robert E. Anderson (NH-RedAnt) for bug fixes and suggestions, Clark Jefcoat (hubcity) for fruitful interactions.
|
|
896
891
|
|
|
897
892
|
---------------------------------------------------------------------
|
|
898
893
|
|
|
899
894
|
Copyright © 2024 - Martin-D. Lacasse
|
|
900
895
|
|
|
901
|
-
Disclaimers:
|
|
902
|
-
|
|
896
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
897
|
+
|
|
898
|
+
Code output has been verified with analytical solutions when applicable, and comparative approaches otherwise.
|
|
899
|
+
Nevertheless, accuracy of results is not guaranteed.
|
|
903
900
|
|
|
904
901
|
--------------------------------------------------------
|
|
905
902
|
|
|
@@ -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,11 +126,13 @@ 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.
|
|
140
133
|
|
|
134
|
+
Maturation rules for Roth contributions and conversions are implemented as constraints
|
|
135
|
+
limiting withdrawal amounts to cover Roth account balances for 5 years after the events.
|
|
141
136
|
Medicare and IRMAA calculations are performed through a self-consistent loop on cash flow constraints.
|
|
142
137
|
Future values are simple projections of current values with the assumed inflation rates.
|
|
143
138
|
|
|
@@ -186,14 +181,16 @@ assets to support, even with no estate being left.
|
|
|
186
181
|
- Streamlit Community Cloud [Streamlit](https://streamlit.io)
|
|
187
182
|
- Contributors: Josh (noimjosh@gmail.com) for Docker image code,
|
|
188
183
|
Dale Seng (sengsational) for great insights and suggestions,
|
|
189
|
-
Robert E. Anderson (NH-RedAnt) for bug fixes and suggestions.
|
|
184
|
+
Robert E. Anderson (NH-RedAnt) for bug fixes and suggestions, Clark Jefcoat (hubcity) for fruitful interactions.
|
|
190
185
|
|
|
191
186
|
---------------------------------------------------------------------
|
|
192
187
|
|
|
193
188
|
Copyright © 2024 - Martin-D. Lacasse
|
|
194
189
|
|
|
195
|
-
Disclaimers:
|
|
196
|
-
|
|
190
|
+
Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
|
|
191
|
+
|
|
192
|
+
Code output has been verified with analytical solutions when applicable, and comparative approaches otherwise.
|
|
193
|
+
Nevertheless, accuracy of results is not guaranteed.
|
|
197
194
|
|
|
198
195
|
--------------------------------------------------------
|
|
199
196
|
|
|
@@ -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
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
\begin{document}
|
|
33
33
|
\title{Formulation of the optimization model in Owl}
|
|
34
34
|
\author{Martin-D. Lacasse}
|
|
35
|
-
\date{
|
|
35
|
+
\date{June 13, 2025}
|
|
36
36
|
\maketitle
|
|
37
37
|
\thispagestyle{fancy}
|
|
38
38
|
\fancyfoot[R]{\copyright\ 2024 - Martin-D. Lacasse}
|
|
@@ -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}
|
|
@@ -709,6 +709,24 @@ add the market returns to the savings balances.
|
|
|
709
709
|
x_{in} \le \min(b_{i1n}, x_{max}).
|
|
710
710
|
\end{equation}
|
|
711
711
|
|
|
712
|
+
Roth conversions are also governed by a 5-year maturation rule for withdrawals. This means
|
|
713
|
+
that withdrawals will need to be smaller than the balance minus the sum of all contributions
|
|
714
|
+
and conversions that happened over the last 5 years. For that purpose, the Wages and Contributions
|
|
715
|
+
file which stores $\omega_{in}, \kappa_{ijn}, \ldots$, will go back 5 years, and the Roth
|
|
716
|
+
conversions in that year range will be interpreted as having happened. We will use
|
|
717
|
+
these arrays to store previous conversions and contributions at the end of the array so that
|
|
718
|
+
they can be retrieved with negative indices in Python. Mathematically, we want that
|
|
719
|
+
\begin{equation}
|
|
720
|
+
w_{i2n} \le b_{i2n} - \sum_{n'=n-5}^n [ \kappa{i2n'} + x_{in'}.
|
|
721
|
+
\end{equation}
|
|
722
|
+
However, conversions are sometimes a variable $x_{in}$
|
|
723
|
+
and sometimes a parameter $X_{in}$, depending on the sign of $n$.
|
|
724
|
+
This leads to
|
|
725
|
+
\begin{equation}
|
|
726
|
+
b_{i2n} - w_{i2n} - \sum_{n'=\max(n-5, 0}^{n-1} x_{in'}
|
|
727
|
+
\ge \sum_{n'=n-5}^{\min(-1, n-1)} X_{in} + \sum_{n'=n-5}^{n-1} \kappa{i2n'}.
|
|
728
|
+
\end{equation}
|
|
729
|
+
|
|
712
730
|
\paragraph*{Net spending}
|
|
713
731
|
For calculating the net spending $g_n$, we consider the cash flow of all withdrawals,
|
|
714
732
|
wages, social security and pension benefits, and big-ticket items.
|
|
@@ -1087,9 +1105,9 @@ with
|
|
|
1087
1105
|
|
|
1088
1106
|
\section{Other considerations}
|
|
1089
1107
|
\paragraph*{Beneficiaries}
|
|
1090
|
-
Tax-
|
|
1108
|
+
Tax-free and tax-deferred accounts have special tax rules that allow giving part
|
|
1091
1109
|
or the entire value of
|
|
1092
|
-
tax-
|
|
1110
|
+
tax-free accounts to a spouse who can then consider it as his/her own.
|
|
1093
1111
|
These accounts typically use percentages to designate beneficiaries.
|
|
1094
1112
|
Let $\phi_j$ be the fraction of the account $j$ that a spouse $i_d$ wishes
|
|
1095
1113
|
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
|
###########################################################################
|
|
@@ -303,11 +304,11 @@ class Plan(object):
|
|
|
303
304
|
# Parameters from timeLists initialized to zero.
|
|
304
305
|
self.omega_in = np.zeros((self.N_i, self.N_n))
|
|
305
306
|
self.Lambda_in = np.zeros((self.N_i, self.N_n))
|
|
306
|
-
self.myRothX_in = np.zeros((self.N_i, self.N_n))
|
|
307
|
-
self.kappa_ijn = np.zeros((self.N_i, self.N_j, self.N_n))
|
|
307
|
+
self.myRothX_in = np.zeros((self.N_i, self.N_n + 5))
|
|
308
|
+
self.kappa_ijn = np.zeros((self.N_i, self.N_j, self.N_n + 5))
|
|
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
|
|
@@ -916,14 +920,17 @@ class Plan(object):
|
|
|
916
920
|
# Now fill in parameters which are in $.
|
|
917
921
|
for i, iname in enumerate(self.inames):
|
|
918
922
|
h = self.horizons[i]
|
|
919
|
-
self.omega_in[i, :h] = self.timeLists[iname]["anticipated wages"].iloc[:h]
|
|
920
|
-
self.
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
self.kappa_ijn[i,
|
|
925
|
-
self.
|
|
926
|
-
self.
|
|
923
|
+
self.omega_in[i, :h] = self.timeLists[iname]["anticipated wages"].iloc[5:5+h]
|
|
924
|
+
self.Lambda_in[i, :h] = self.timeLists[iname]["big-ticket items"].iloc[5:5+h]
|
|
925
|
+
|
|
926
|
+
# Values for last 5 years of Roth conversion and contributions stored at the end
|
|
927
|
+
# of array and accessed with negative index.
|
|
928
|
+
self.kappa_ijn[i, 0, :h+5] = np.roll(self.timeLists[iname]["taxable ctrb"], -5)
|
|
929
|
+
self.kappa_ijn[i, 1, :h+5] = np.roll(self.timeLists[iname]["401k ctrb"], -5)
|
|
930
|
+
self.kappa_ijn[i, 1, :h+5] += np.roll(self.timeLists[iname]["IRA ctrb"], -5)
|
|
931
|
+
self.kappa_ijn[i, 2, :h+5] = np.roll(self.timeLists[iname]["Roth 401k ctrb"], -5)
|
|
932
|
+
self.kappa_ijn[i, 2, :h+5] += np.roll(self.timeLists[iname]["Roth IRA ctrb"], -5)
|
|
933
|
+
self.myRothX_in[i, :h+5] = np.roll(self.timeLists[iname]["Roth conv"], -5)
|
|
927
934
|
|
|
928
935
|
self.caseStatus = "modified"
|
|
929
936
|
|
|
@@ -980,8 +987,9 @@ class Plan(object):
|
|
|
980
987
|
]
|
|
981
988
|
for i, iname in enumerate(self.inames):
|
|
982
989
|
h = self.horizons[i]
|
|
983
|
-
df = pd.DataFrame(0, index=np.arange(h), columns=cols)
|
|
984
|
-
df["year"] = self.year_n[:h]
|
|
990
|
+
df = pd.DataFrame(0, index=np.arange(0, h+5), columns=cols)
|
|
991
|
+
# df["year"] = self.year_n[:h]
|
|
992
|
+
df["year"] = np.arange(self.year_n[0] - 5, self.year_n[h-1]+1)
|
|
985
993
|
self.timeLists[iname] = df
|
|
986
994
|
|
|
987
995
|
self.caseStatus = "modified"
|
|
@@ -1074,8 +1082,6 @@ class Plan(object):
|
|
|
1074
1082
|
Utility function that builds constraint matrix and vectors.
|
|
1075
1083
|
Refactored for clarity and maintainability.
|
|
1076
1084
|
"""
|
|
1077
|
-
self._setup_constraint_shortcuts(options)
|
|
1078
|
-
|
|
1079
1085
|
self.A = abc.ConstraintMatrix(self.nvars)
|
|
1080
1086
|
self.B = abc.Bounds(self.nvars, self.nbins)
|
|
1081
1087
|
|
|
@@ -1084,6 +1090,7 @@ class Plan(object):
|
|
|
1084
1090
|
self._add_standard_exemption_bounds()
|
|
1085
1091
|
self._add_defunct_constraints()
|
|
1086
1092
|
self._add_roth_conversion_constraints(options)
|
|
1093
|
+
self._add_roth_maturation_constraints()
|
|
1087
1094
|
self._add_withdrawal_limits()
|
|
1088
1095
|
self._add_conversion_limits()
|
|
1089
1096
|
self._add_objective_constraints(objective, options)
|
|
@@ -1098,12 +1105,6 @@ class Plan(object):
|
|
|
1098
1105
|
|
|
1099
1106
|
return None
|
|
1100
1107
|
|
|
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
1108
|
def _add_rmd_inequalities(self):
|
|
1108
1109
|
for i in range(self.N_i):
|
|
1109
1110
|
if self.beta_ij[i, 1] > 0:
|
|
@@ -1131,6 +1132,45 @@ class Plan(object):
|
|
|
1131
1132
|
for j in range(self.N_j):
|
|
1132
1133
|
self.B.setRange(_q3(self.C["w"], self.i_d, j, n, self.N_i, self.N_j, self.N_n), 0, 0)
|
|
1133
1134
|
|
|
1135
|
+
def _add_roth_maturation_constraints(self):
|
|
1136
|
+
"""
|
|
1137
|
+
Withdrawals from Roth accounts are subject to the 5-year rule for conversion.
|
|
1138
|
+
Conversions and gains are subject to the 5-year rule since conversion.
|
|
1139
|
+
Contributions can be withdrawn at any time (without 59.5 penalty) but
|
|
1140
|
+
gains on contributions are subject to the 5-year rule since the opening of the account.
|
|
1141
|
+
A retainer is put on all conversions and associated gains, and gains on all recent contributions.
|
|
1142
|
+
"""
|
|
1143
|
+
# Assume 10% per year for contributions and conversions for past 5 years.
|
|
1144
|
+
# Future years will use the assumed returns.
|
|
1145
|
+
oldTau1 = 1.10
|
|
1146
|
+
for i in range(self.N_i):
|
|
1147
|
+
h = self.horizons[i]
|
|
1148
|
+
for n in range(h):
|
|
1149
|
+
rhs = 0
|
|
1150
|
+
# To add compounded gains to original amount.
|
|
1151
|
+
cgains = 1
|
|
1152
|
+
row = self.A.newRow()
|
|
1153
|
+
row.addElem(_q3(self.C["b"], i, 2, n, self.N_i, self.N_j, self.N_n + 1), 1)
|
|
1154
|
+
row.addElem(_q3(self.C["w"], i, 2, n, self.N_i, self.N_j, self.N_n), -1)
|
|
1155
|
+
for dn in range(1, 6):
|
|
1156
|
+
nn = n - dn
|
|
1157
|
+
if nn < 0: # Past of future is in the past:
|
|
1158
|
+
# Parameters are stored at the end of contributions and conversions arrays.
|
|
1159
|
+
cgains *= oldTau1
|
|
1160
|
+
# If only an contribution - without conversion.
|
|
1161
|
+
# rhs += (cgains - 1) * self.kappa_ijn[i, 2, nn] + cgains * self.myRothX_in[i, nn]
|
|
1162
|
+
rhs += cgains * self.kappa_ijn[i, 2, nn] + cgains * self.myRothX_in[i, nn]
|
|
1163
|
+
else: # Past of future is in the future: use variables and parameters.
|
|
1164
|
+
ksum2 = np.sum(self.alpha_ijkn[i, 2, :, nn] * self.tau_kn[:, nn], axis=0)
|
|
1165
|
+
Tau1 = 1 + ksum2
|
|
1166
|
+
cgains *= Tau1
|
|
1167
|
+
row.addElem(_q2(self.C["x"], i, nn, self.N_i, self.N_n), -cgains)
|
|
1168
|
+
# If only a contribution - without conversion.
|
|
1169
|
+
# rhs += (cgains - 1) * self.kappa_ijn[i, 2, nn]
|
|
1170
|
+
rhs += cgains * self.kappa_ijn[i, 2, nn]
|
|
1171
|
+
|
|
1172
|
+
self.A.addRow(row, rhs, np.inf)
|
|
1173
|
+
|
|
1134
1174
|
def _add_roth_conversion_constraints(self, options):
|
|
1135
1175
|
if "maxRothConversion" in options and options["maxRothConversion"] == "file":
|
|
1136
1176
|
for i in range(self.N_i):
|
|
@@ -1579,14 +1619,18 @@ class Plan(object):
|
|
|
1579
1619
|
if objective == "maxSpending" and "bequest" not in myoptions:
|
|
1580
1620
|
self.mylog.vprint("Using bequest of $1.")
|
|
1581
1621
|
|
|
1582
|
-
self.
|
|
1622
|
+
self.optionsUnits = u.getUnits(myoptions.get("units", "k"))
|
|
1623
|
+
|
|
1624
|
+
oppCostX = options.get("oppCostX", 0.)
|
|
1625
|
+
self.xnet = 1 - oppCostX / 100.
|
|
1626
|
+
|
|
1627
|
+
self.prevMAGI = np.zeros(2)
|
|
1583
1628
|
if "previousMAGIs" in myoptions:
|
|
1584
1629
|
magi = myoptions["previousMAGIs"]
|
|
1585
|
-
if len(magi)
|
|
1586
|
-
raise ValueError("previousMAGIs must have
|
|
1630
|
+
if 3 < len(magi) < 2:
|
|
1631
|
+
raise ValueError("previousMAGIs must have 2 values.")
|
|
1587
1632
|
|
|
1588
|
-
|
|
1589
|
-
self.prevMAGI = units * np.array(magi)
|
|
1633
|
+
self.prevMAGI = self.optionsUnits * np.array(magi)
|
|
1590
1634
|
|
|
1591
1635
|
lambdha = myoptions.get("spendingSlack", 0)
|
|
1592
1636
|
if lambdha < 0 or lambdha > 50:
|
|
@@ -1949,7 +1993,7 @@ class Plan(object):
|
|
|
1949
1993
|
self.Q_n = np.sum(
|
|
1950
1994
|
(
|
|
1951
1995
|
self.mu
|
|
1952
|
-
* (self.b_ijn[:, 0, :-1] - self.w_ijn[:, 0, :] + self.d_in[:, :] + 0.5 * self.kappa_ijn[:, 0, :])
|
|
1996
|
+
* (self.b_ijn[:, 0, :-1] - self.w_ijn[:, 0, :] + self.d_in[:, :] + 0.5 * self.kappa_ijn[:, 0, :Nn])
|
|
1953
1997
|
+ tau_0prev * self.w_ijn[:, 0, :]
|
|
1954
1998
|
)
|
|
1955
1999
|
* self.alpha_ijkn[:, 0, 0, :-1],
|
|
@@ -2058,7 +2102,7 @@ class Plan(object):
|
|
|
2058
2102
|
dic = {}
|
|
2059
2103
|
# Results
|
|
2060
2104
|
dic["Plan name"] = self._name
|
|
2061
|
-
dic["Net yearly spending basis"] = u.d(self.g_n[0] / self.xi_n[0])
|
|
2105
|
+
dic["Net yearly spending basis" + 26*" ."] = u.d(self.g_n[0] / self.xi_n[0])
|
|
2062
2106
|
dic[f"Net spending for year {now}"] = u.d(self.g_n[0])
|
|
2063
2107
|
dic[f"Net spending remaining in year {now}"] = u.d(self.g_n[0] * self.yearFracLeft)
|
|
2064
2108
|
|
|
@@ -2370,7 +2414,7 @@ class Plan(object):
|
|
|
2370
2414
|
the default behavior of setDefaultPlots().
|
|
2371
2415
|
"""
|
|
2372
2416
|
value = self._checkValue(value)
|
|
2373
|
-
title = self._name + "\
|
|
2417
|
+
title = self._name + "\nFederal Income Tax"
|
|
2374
2418
|
if tag:
|
|
2375
2419
|
title += " - " + tag
|
|
2376
2420
|
# All taxes: ordinary income and dividends.
|
|
@@ -2489,16 +2533,16 @@ class Plan(object):
|
|
|
2489
2533
|
# Account balances except final year.
|
|
2490
2534
|
accDic = {
|
|
2491
2535
|
"taxable bal": self.b_ijn[:, 0, :-1],
|
|
2492
|
-
"taxable ctrb": self.kappa_ijn[:, 0, :],
|
|
2536
|
+
"taxable ctrb": self.kappa_ijn[:, 0, :self.N_n],
|
|
2493
2537
|
"taxable dep": self.d_in,
|
|
2494
2538
|
"taxable wdrwl": self.w_ijn[:, 0, :],
|
|
2495
2539
|
"tax-deferred bal": self.b_ijn[:, 1, :-1],
|
|
2496
|
-
"tax-deferred ctrb": self.kappa_ijn[:, 1, :],
|
|
2540
|
+
"tax-deferred ctrb": self.kappa_ijn[:, 1, :self.N_n],
|
|
2497
2541
|
"tax-deferred wdrwl": self.w_ijn[:, 1, :],
|
|
2498
2542
|
"(included RMDs)": self.rmd_in[:, :],
|
|
2499
2543
|
"Roth conv": self.x_in,
|
|
2500
2544
|
"tax-free bal": self.b_ijn[:, 2, :-1],
|
|
2501
|
-
"tax-free ctrb": self.kappa_ijn[:, 2, :],
|
|
2545
|
+
"tax-free ctrb": self.kappa_ijn[:, 2, :self.N_n],
|
|
2502
2546
|
"tax-free wdrwl": self.w_ijn[:, 2, :],
|
|
2503
2547
|
}
|
|
2504
2548
|
for i in range(self.N_i):
|
|
@@ -2595,12 +2639,12 @@ class Plan(object):
|
|
|
2595
2639
|
planData[self.inames[i] + " txbl dep"] = self.d_in[i, :]
|
|
2596
2640
|
planData[self.inames[i] + " txbl wrdwl"] = self.w_ijn[i, 0, :]
|
|
2597
2641
|
planData[self.inames[i] + " tx-def bal"] = self.b_ijn[i, 1, :-1]
|
|
2598
|
-
planData[self.inames[i] + " tx-def ctrb"] = self.kappa_ijn[i, 1, :]
|
|
2642
|
+
planData[self.inames[i] + " tx-def ctrb"] = self.kappa_ijn[i, 1, :self.N_n]
|
|
2599
2643
|
planData[self.inames[i] + " tx-def wdrl"] = self.w_ijn[i, 1, :]
|
|
2600
2644
|
planData[self.inames[i] + " (RMD)"] = self.rmd_in[i, :]
|
|
2601
2645
|
planData[self.inames[i] + " Roth conv"] = self.x_in[i, :]
|
|
2602
2646
|
planData[self.inames[i] + " tx-free bal"] = self.b_ijn[i, 2, :-1]
|
|
2603
|
-
planData[self.inames[i] + " tx-free ctrb"] = self.kappa_ijn[i, 2, :]
|
|
2647
|
+
planData[self.inames[i] + " tx-free ctrb"] = self.kappa_ijn[i, 2, :self.N_n]
|
|
2604
2648
|
planData[self.inames[i] + " tax-free wdrwl"] = self.w_ijn[i, 2, :]
|
|
2605
2649
|
planData[self.inames[i] + " big-ticket items"] = self.Lambda_in[i, :]
|
|
2606
2650
|
|
|
@@ -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",
|