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.
Files changed (112) hide show
  1. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/INSTALL.md +4 -2
  2. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/PKG-INFO +9 -12
  3. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/README.md +8 -11
  4. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/USER_GUIDE.md +1 -1
  5. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/owl.pdf +0 -0
  6. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/owl.tex +25 -7
  7. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/pyproject.toml +1 -1
  8. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/requirements.txt +1 -1
  9. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/abcapi.py +2 -2
  10. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/config.py +3 -2
  11. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/mylogging.py +2 -2
  12. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/plan.py +82 -38
  13. owlplanner-2025.6.21/src/owlplanner/plotting/__init__.py +12 -0
  14. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/plotting/base.py +5 -0
  15. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/plotting/factory.py +5 -0
  16. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/plotting/matplotlib_backend.py +10 -3
  17. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/plotting/plotly_backend.py +8 -2
  18. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/progress.py +4 -0
  19. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/rates.py +2 -3
  20. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/tax2025.py +5 -4
  21. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/timelists.py +13 -12
  22. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/utils.py +2 -2
  23. owlplanner-2025.6.21/src/owlplanner/version.py +1 -0
  24. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/tests/test_repro.py +15 -15
  25. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/About_Owl.py +9 -7
  26. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Asset_Allocation.py +22 -15
  27. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Create_Case.py +4 -3
  28. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Current_Assets.py +18 -12
  29. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Documentation.py +154 -121
  30. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Fixed_Income.py +3 -3
  31. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Graphs.py +1 -1
  32. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Historical_Range.py +1 -1
  33. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Logs.py +1 -1
  34. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Monte_Carlo.py +1 -1
  35. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Optimization_Parameters.py +8 -8
  36. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Output_Files.py +16 -16
  37. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Quick_Start.py +7 -7
  38. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Rates_Selection.py +22 -15
  39. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Settings.py +4 -4
  40. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Wages_and_Contributions.py +74 -72
  41. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/Worksheets.py +1 -1
  42. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/main.py +1 -1
  43. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/owlbridge.py +21 -22
  44. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/sskeys.py +5 -5
  45. owlplanner-2025.5.30/src/owlplanner/plotting/__init__.py +0 -7
  46. owlplanner-2025.5.30/src/owlplanner/version.py +0 -1
  47. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/.devcontainer/devcontainer.json +0 -0
  48. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/.flake8 +0 -0
  49. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/.gitattributes +0 -0
  50. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/.github/workflows/github-actions-runtests.yml +0 -0
  51. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/.gitignore +0 -0
  52. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/.streamlit/config.toml +0 -0
  53. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/.streamlit/fullconfig.toml +0 -0
  54. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/LICENSE +0 -0
  55. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docker/Dockerfile.build +0 -0
  56. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docker/Dockerfile.run +0 -0
  57. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docker/README.md +0 -0
  58. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docker/buildentrypoint.sh +0 -0
  59. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docker/docker-compose.yml +0 -0
  60. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docker/runentrypoint.sh +0 -0
  61. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/AD-taxDef.png +0 -0
  62. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/AD-taxFree.png +0 -0
  63. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/AD-taxable.png +0 -0
  64. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/Hist_Bequest.png +0 -0
  65. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/Hist_Spending.png +0 -0
  66. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/MC-tutorial2a.png +0 -0
  67. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/MC-tutorial2b.png +0 -0
  68. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/OwlUI.png +0 -0
  69. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/allocations.png +0 -0
  70. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/owl.png +0 -0
  71. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/profile.png +0 -0
  72. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/ratesCorrelations.png +0 -0
  73. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/ratesPlot.png +0 -0
  74. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/savingsPlot.png +0 -0
  75. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/sourcesPlot.png +0 -0
  76. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/spendingPlot.png +0 -0
  77. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/taxIncomePlot.png +0 -0
  78. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/docs/images/taxesPlot.png +0 -0
  79. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/case_jack+jill.toml +0 -0
  80. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/case_joe.toml +0 -0
  81. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/case_john+sally.toml +0 -0
  82. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/case_jon+jane.toml +0 -0
  83. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/case_kim+sam-bequest.toml +0 -0
  84. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/case_kim+sam-spending.toml +0 -0
  85. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/jack+jill.xlsx +0 -0
  86. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/joe.xlsx +0 -0
  87. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/john+sally.xlsx +0 -0
  88. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/jon+jane.xlsx +0 -0
  89. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/examples/template.xlsx +0 -0
  90. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/notebooks/john+sally.ipynb +0 -0
  91. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/notebooks/kim+sam.ipynb +0 -0
  92. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/notebooks/template.ipynb +0 -0
  93. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/notebooks/tutorial_1.ipynb +0 -0
  94. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/notebooks/tutorial_2.ipynb +0 -0
  95. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/notebooks/tutorial_3.ipynb +0 -0
  96. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/owlplanner.cmd +0 -0
  97. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/owlplanner.sh +0 -0
  98. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/pytest.ini +0 -0
  99. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/__init__.py +0 -0
  100. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/data/__init__.py +0 -0
  101. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/src/owlplanner/data/rates.csv +0 -0
  102. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/tests/test_logger.py +0 -0
  103. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/tests/test_regressions.py +0 -0
  104. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/tests/test_toml_cases.py +0 -0
  105. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/tests/test_ui_asset_allocation.py +0 -0
  106. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/tests/test_ui_compare_summaries.py +0 -0
  107. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/tests/test_ui_sskeys.py +0 -0
  108. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/tests/test_units.py +0 -0
  109. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/README.md +0 -0
  110. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/__init__.py +0 -0
  111. {owlplanner-2025.5.30 → owlplanner-2025.6.21}/ui/progress.py +0 -0
  112. {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`, `ui/requirements.txt`, and in `pyproject.toml`. Then,
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.5.30
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-exempt,
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: I am not a financial planner. You make your own decisions.
902
- This program comes with no guarantee. Use at your own risk.
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-exempt,
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: I am not a financial planner. You make your own decisions.
196
- This program comes with no guarantee. Use at your own risk.
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-exempt accounts.
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.
@@ -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{March 14, 2025}
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-exempt accounts respectively. Therefore $N_j = 3$.
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-exempt account through Roth conversions.
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-exempt withdrawal later in the same year.
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-exempt distributions exclusive events
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-exempt and tax-deferred accounts have special tax rules that allow giving part
1108
+ Tax-free and tax-deferred accounts have special tax rules that allow giving part
1091
1109
  or the entire value of
1092
- tax-exempt accounts to a spouse who can then consider it as his/her own.
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$
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "owlplanner"
7
- version = "2025.05.30"
7
+ version = "2025.06.21"
8
8
  authors = [
9
9
  { name="Martin-D. Lacasse", email="martin.d.lacasse@gmail.com" },
10
10
  ]
@@ -7,5 +7,5 @@ odfpy
7
7
  plotly
8
8
  pulp
9
9
  scipy
10
- streamlit
10
+ streamlit >= 1.46
11
11
  toml
@@ -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 (C) 2024 -- Martin-D. Lacasse
19
+ Copyright © 2024 - Martin-D. Lacasse
20
20
 
21
- Disclaimer: This program comes with no guarantee. Use at your own risk.
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 (C) 2024 -- Martin-D. Lacasse
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 (C) 2024 -- Martin-D. Lacasse
7
+ Copyright © 2024 - Martin-D. Lacasse
8
8
 
9
- Disclaimer: This program comes with no guarantee. Use at your own risk.
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 (C) 2024 -- Martin-D. Lacasse
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((3))
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, adn tax-exempt accounts.
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 contributions: {e}") from e
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.kappa_ijn[i, 0, :h] = self.timeLists[iname]["taxable ctrb"].iloc[:h]
921
- self.kappa_ijn[i, 1, :h] = self.timeLists[iname]["401k ctrb"].iloc[:h]
922
- self.kappa_ijn[i, 2, :h] = self.timeLists[iname]["Roth 401k ctrb"].iloc[:h]
923
- self.kappa_ijn[i, 1, :h] += self.timeLists[iname]["IRA ctrb"].iloc[:h]
924
- self.kappa_ijn[i, 2, :h] += self.timeLists[iname]["Roth IRA ctrb"].iloc[:h]
925
- self.myRothX_in[i, :h] = self.timeLists[iname]["Roth conv"].iloc[:h]
926
- self.Lambda_in[i, :h] = self.timeLists[iname]["big-ticket items"].iloc[:h]
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.prevMAGI = np.zeros(3)
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) != 3:
1586
- raise ValueError("previousMAGIs must have 3 values.")
1630
+ if 3 < len(magi) < 2:
1631
+ raise ValueError("previousMAGIs must have 2 values.")
1587
1632
 
1588
- units = u.getUnits(options.get("units", "k"))
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 + "\nIncome Tax"
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 &copy; 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
  Base classes for plot backends.
3
+
4
+ Copyright &copy; 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
  from abc import ABC, abstractmethod
@@ -1,5 +1,10 @@
1
1
  """
2
2
  Factory for creating plot backends.
3
+
4
+ Copyright &copy; 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
  from .base import PlotBackend
@@ -1,5 +1,10 @@
1
1
  """
2
2
  Matplotlib implementation of plot backend.
3
+
4
+ Copyright &copy; 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, inames, t, [i], y2stack, stackNames, "upper left", "percent")
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 &copy; 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",
@@ -1,6 +1,10 @@
1
1
  """
2
2
  A simple object to display progress.
3
3
 
4
+ Copyright &copy; 2024 - Martin-D. Lacasse
5
+
6
+ Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
7
+
4
8
  """
5
9
 
6
10
  from owlplanner import utils as u