owlplanner 2025.3.30__tar.gz → 2025.4.2__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.3.30 → owlplanner-2025.4.2}/.flake8 +1 -1
  2. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/PKG-INFO +1 -1
  3. owlplanner-2025.4.2/Papers/bv_cvxbook.pdf +0 -0
  4. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/owl.tex +1 -1
  5. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/pyproject.toml +1 -1
  6. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/requirements.txt +1 -0
  7. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/src/owlplanner/config.py +6 -0
  8. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/src/owlplanner/plan.py +39 -25
  9. owlplanner-2025.4.2/src/owlplanner/version.py +1 -0
  10. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/About_Owl.py +2 -1
  11. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/Current_Assets.py +2 -1
  12. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/Documentation.py +10 -2
  13. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/Optimization_Parameters.py +7 -1
  14. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/requirements.txt +2 -1
  15. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/sskeys.py +1 -1
  16. owlplanner-2025.3.30/src/owlplanner/version.py +0 -1
  17. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/.devcontainer/devcontainer.json +0 -0
  18. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/.gitattributes +0 -0
  19. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/.github/workflows/github-actions-runtests.yml +0 -0
  20. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/.gitignore +0 -0
  21. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/INSTALL.md +0 -0
  22. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/LICENSE +0 -0
  23. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/Papers/FE00006821-Class-VI-Injection-Permit--Salient-Features-and-Regulatory-Challenges_Final.pdf +0 -0
  24. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/Papers/Kou-OptionPricingDouble-2004.pdf +0 -0
  25. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/Papers/Multi-Period Mean Expected-Shortfall Strategies Cut Your Losses and Ride Your Gains .pdf +0 -0
  26. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/Papers/Optimal Asset Allocation for Retirement Saving Deterministic Vs. Time Consistent Adaptive Strategies.pdf +0 -0
  27. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/Papers/Rule-based_strategies_for_dynamic_life_cycle_inves.pdf +0 -0
  28. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/Papers/s10436-006-0062-y.pdf +0 -0
  29. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/README.md +0 -0
  30. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/USER_GUIDE.md +0 -0
  31. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docker/Dockerfile +0 -0
  32. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docker/README.md +0 -0
  33. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docker/docker-compose.yml +0 -0
  34. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docker/fastentrypoint.sh +0 -0
  35. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/images/AD-taxDef.png +0 -0
  36. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/images/AD-taxFree.png +0 -0
  37. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/images/AD-taxable.png +0 -0
  38. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/images/Hist_Bequest.png +0 -0
  39. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/images/Hist_Spending.png +0 -0
  40. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/images/MC-tutorial2a.png +0 -0
  41. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/images/MC-tutorial2b.png +0 -0
  42. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/images/OwlUI.png +0 -0
  43. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/images/allocations.png +0 -0
  44. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/images/owl.png +0 -0
  45. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/images/profile.png +0 -0
  46. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/images/ratesCorrelations.png +0 -0
  47. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/images/ratesPlot.png +0 -0
  48. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/images/savingsPlot.png +0 -0
  49. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/images/sourcesPlot.png +0 -0
  50. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/images/spendingPlot.png +0 -0
  51. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/images/taxIncomePlot.png +0 -0
  52. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/images/taxesPlot.png +0 -0
  53. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/docs/owl.pdf +0 -0
  54. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/examples/case_jack+jill.toml +0 -0
  55. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/examples/case_joe.toml +0 -0
  56. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/examples/case_john+sally.toml +0 -0
  57. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/examples/case_jon+jane.toml +0 -0
  58. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/examples/case_kim+sam-bequest.toml +0 -0
  59. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/examples/case_kim+sam-spending.toml +0 -0
  60. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/examples/jack+jill.xlsx +0 -0
  61. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/examples/joe.xlsx +0 -0
  62. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/examples/john+sally.xlsx +0 -0
  63. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/examples/jon+jane.xlsx +0 -0
  64. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/examples/template.xlsx +0 -0
  65. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/notebooks/john+sally.ipynb +0 -0
  66. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/notebooks/kim+sam.ipynb +0 -0
  67. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/notebooks/template.ipynb +0 -0
  68. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/notebooks/tutorial_1.ipynb +0 -0
  69. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/notebooks/tutorial_2.ipynb +0 -0
  70. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/notebooks/tutorial_3.ipynb +0 -0
  71. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/owlplanner.cmd +0 -0
  72. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/owlplanner.sh +0 -0
  73. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/src/owlplanner/__init__.py +0 -0
  74. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/src/owlplanner/abcapi.py +0 -0
  75. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/src/owlplanner/data/__init__.py +0 -0
  76. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/src/owlplanner/data/rates.csv +0 -0
  77. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/src/owlplanner/logging.py +0 -0
  78. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/src/owlplanner/progress.py +0 -0
  79. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/src/owlplanner/rates.py +0 -0
  80. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/src/owlplanner/tax2025.py +0 -0
  81. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/src/owlplanner/timelists.py +0 -0
  82. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/src/owlplanner/utils.py +0 -0
  83. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/tests/test_logger.py +0 -0
  84. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/tests/test_regressions.py +0 -0
  85. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/tests/test_repro.py +0 -0
  86. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/tests/test_toml_cases.py +0 -0
  87. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/tests/test_units.py +0 -0
  88. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ttt.py +0 -0
  89. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ttt2.py +0 -0
  90. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ttt3.py +0 -0
  91. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/Asset_Allocation.py +0 -0
  92. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/Create_Case.py +0 -0
  93. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/Fixed_Income.py +0 -0
  94. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/Graphs.py +0 -0
  95. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/Historical_Range.py +0 -0
  96. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/Logs.py +0 -0
  97. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/Monte_Carlo.py +0 -0
  98. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/Output_Files.py +0 -0
  99. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/Quick_Start.py +0 -0
  100. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/README.md +0 -0
  101. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/Rates_Selection.py +0 -0
  102. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/Settings.py +0 -0
  103. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/Wages_And_Contributions.py +0 -0
  104. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/Worksheets.py +0 -0
  105. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/main+fonts.py +0 -0
  106. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/main.py +0 -0
  107. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/owlbridge.py +0 -0
  108. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/plots.py +0 -0
  109. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/progress.py +0 -0
  110. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/sskeys.py.color +0 -0
  111. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/style.css +0 -0
  112. {owlplanner-2025.3.30 → owlplanner-2025.4.2}/ui/tomlexamples.py +0 -0
@@ -1,6 +1,6 @@
1
1
  [flake8]
2
2
  max-line-length = 120
3
- max-complexity = 60
3
+ max-complexity = 70
4
4
  # These checks violate PEP8 so let's ignore them
5
5
  extend-ignore = E203
6
6
  # extend-exclude = */site-packages/*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owlplanner
3
- Version: 2025.3.30
3
+ Version: 2025.4.2
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
@@ -329,7 +329,7 @@ or an s-curve as in
329
329
  Income tax rate on long-term capital gain and qualified dividends, typically 15\%.
330
330
  \item [$\eta$]
331
331
  Spousal ratio for surplus deposits, which goes from 0 to 1, as the fraction
332
- that goes to the $i = 1$ second spouse's account. Therefore, a surplus $s_n$ in year $n$
332
+ that goes to the $i = 1$ spouse's account. Therefore, a surplus $s_n$ in year $n$
333
333
  will result in a deposit $d$ in the taxable account of individual $i$ as
334
334
  \begin{eqnarray}
335
335
  \label{Eq:eta}
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "owlplanner"
7
- version = "2025.03.30"
7
+ version = "2025.04.02"
8
8
  authors = [
9
9
  { name="Martin-D. Lacasse", email="martin.d.lacasse@gmail.com" },
10
10
  ]
@@ -3,6 +3,7 @@ matplotlib
3
3
  seaborn
4
4
  pandas
5
5
  openpyxl
6
+ odfpy
6
7
  scipy
7
8
  streamlit
8
9
  toml
@@ -12,6 +12,7 @@ Disclaimer: This program comes with no guarantee. Use at your own risk.
12
12
  import toml as toml
13
13
  from io import StringIO, BytesIO
14
14
  import numpy as np
15
+ from datetime import date
15
16
  import os
16
17
 
17
18
  from owlplanner import plan
@@ -302,6 +303,11 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
302
303
  if name != "None" and name not in p.inames:
303
304
  raise ValueError(f"Unknown name {name} for noRothConversions.")
304
305
 
306
+ # Rebase startRothConversions on year change.
307
+ thisyear = date.today().year
308
+ year = p.solverOptions.get("startRothConversions", thisyear)
309
+ diconf["Solver Options"]["startRothConversions"] = max(year, thisyear)
310
+
305
311
  # Results.
306
312
  p.setDefaultPlots(diconf["Results"]["Default plots"])
307
313
 
@@ -1122,16 +1122,17 @@ class Plan(object):
1122
1122
  B.set0_Ub(_q1(Ce, n, Nn), self.sigmaBar_n[n])
1123
1123
 
1124
1124
  # Roth conversions equalities/inequalities.
1125
- if "maxRothConversion" in options:
1126
- if options["maxRothConversion"] == "file":
1127
- # self.mylog.vprint(f"Fixing Roth conversions to those from file {self.timeListsFileName}.")
1128
- for i in range(Ni):
1129
- for n in range(self.horizons[i]):
1130
- rhs = self.myRothX_in[i][n]
1131
- B.setRange(_q2(Cx, i, n, Ni, Nn), rhs, rhs)
1132
- else:
1125
+ # This condition supercedes everything else.
1126
+ if "maxRothConversion" in options and options["maxRothConversion"] == "file":
1127
+ # self.mylog.vprint(f"Fixing Roth conversions to those from file {self.timeListsFileName}.")
1128
+ for i in range(Ni):
1129
+ for n in range(self.horizons[i]):
1130
+ rhs = self.myRothX_in[i][n]
1131
+ B.setRange(_q2(Cx, i, n, Ni, Nn), rhs, rhs)
1132
+ else:
1133
+ if "maxRothConversion" in options:
1133
1134
  rhsopt = options["maxRothConversion"]
1134
- assert isinstance(rhsopt, (int, float)), "Specified maxConversion is not a number."
1135
+ assert isinstance(rhsopt, (int, float)), "Specified maxRothConversion is not a number."
1135
1136
  rhsopt *= units
1136
1137
  if rhsopt < 0:
1137
1138
  # self.mylog.vprint('Unlimited Roth conversions (<0)')
@@ -1143,16 +1144,28 @@ class Plan(object):
1143
1144
  # Should we adjust Roth conversion cap with inflation?
1144
1145
  B.set0_Ub(_q2(Cx, i, n, Ni, Nn), rhsopt)
1145
1146
 
1146
- # Process noRothConversions option. Also valid when N_i == 1, why not?
1147
- if "noRothConversions" in options and options["noRothConversions"] != "None":
1148
- rhsopt = options["noRothConversions"]
1149
- try:
1150
- i_x = self.inames.index(rhsopt)
1151
- except ValueError:
1152
- raise ValueError(f"Unknown individual {rhsopt} for noRothConversions:")
1147
+ # Process startRothConversions option.
1148
+ if "startRothConversions" in options:
1149
+ rhsopt = options["startRothConversions"]
1150
+ assert isinstance(rhsopt, (int, float)), "Specified startRothConversions is not a number."
1151
+ thisyear = date.today().year
1152
+ yearn = max(rhsopt - thisyear, 0)
1153
1153
 
1154
- for n in range(Nn):
1155
- B.set0_Ub(_q2(Cx, i_x, n, Ni, Nn), zero)
1154
+ for i in range(Ni):
1155
+ nstart = min(yearn, self.horizons[i])
1156
+ for n in range(0, nstart):
1157
+ B.set0_Ub(_q2(Cx, i, n, Ni, Nn), zero)
1158
+
1159
+ # Process noRothConversions option. Also valid when N_i == 1, why not?
1160
+ if "noRothConversions" in options and options["noRothConversions"] != "None":
1161
+ rhsopt = options["noRothConversions"]
1162
+ try:
1163
+ i_x = self.inames.index(rhsopt)
1164
+ except ValueError:
1165
+ raise ValueError(f"Unknown individual {rhsopt} for noRothConversions:")
1166
+
1167
+ for n in range(Nn):
1168
+ B.set0_Ub(_q2(Cx, i_x, n, Ni, Nn), zero)
1156
1169
 
1157
1170
  # Impose withdrawal limits on taxable and tax-exempt accounts.
1158
1171
  for i in range(Ni):
@@ -1197,7 +1210,7 @@ class Plan(object):
1197
1210
  # Account for time elapsed in the current year.
1198
1211
  spending *= units * self.yearFracLeft
1199
1212
  # self.mylog.vprint('Maximizing bequest with desired net spending of:', u.d(spending))
1200
- # To allow slack in first year, Cg can be made Nn+1 and store basis in g[Nn].
1213
+ # To allow slack in first year, Cg can be made Nn+1 and store basis in g[Nn].
1201
1214
  A.addNewRow({_q1(Cg, 0, Nn): 1}, spending, spending)
1202
1215
 
1203
1216
  # Set initial balances through constraints.
@@ -1616,16 +1629,17 @@ class Plan(object):
1616
1629
  knownObjectives = ["maxBequest", "maxSpending"]
1617
1630
  knownSolvers = ["HiGHS", "MOSEK"]
1618
1631
  knownOptions = [
1619
- "units",
1620
- "maxRothConversion",
1621
- "netSpending",
1622
- "spendingSlack",
1623
1632
  "bequest",
1624
1633
  "bigM",
1634
+ "maxRothConversion",
1635
+ "netSpending",
1625
1636
  "noRothConversions",
1626
- "withMedicare",
1627
- "solver",
1628
1637
  "previousMAGIs",
1638
+ "solver",
1639
+ "spendingSlack",
1640
+ "startRothConversions",
1641
+ "units",
1642
+ "withMedicare",
1629
1643
  ]
1630
1644
  # We will modify options if required.
1631
1645
  if options is None:
@@ -0,0 +1 @@
1
+ __version__ = "2025.04.02"
@@ -30,6 +30,7 @@ Copyright &copy; 2024 - Martin-D. Lacasse
30
30
  [Seaborn](https://seaborn.pydata.org),
31
31
  [Scipy](https://scipy.org),
32
32
  [openpyxl](https://openpyxl.readthedocs.io),
33
+ [odfpy](https://https://pypi.org/project/odfpy),
33
34
  [toml](https://toml.io),
34
35
  and [Streamlit](https://streamlit.io) for the front-end
35
36
  - Contributors: Josh (noimjosh@gmail.com) for Docker image code.
@@ -41,7 +42,7 @@ or directly by [email](mailto://martin.d.lacasse@gmail.com).
41
42
 
42
43
  #### :orange[Privacy]
43
44
  - This app does not store or forward any information. All data entered is lost
44
- after a session is closed. You can choose to download selected part of your
45
+ after a session is closed. You can choose to download selected parts of your
45
46
  own data to your computer before closing the session.
46
47
 
47
48
  #### :orange[Disclaimer]
@@ -51,7 +51,8 @@ else:
51
51
  col1, col2, col3 = st.columns(3, gap="large", vertical_alignment="top")
52
52
  with col1:
53
53
  kz.initKey("surplusFraction", 0.5)
54
- helpmsg = "When beneficiary fractions are not all 1, set surplus deposits to all go to survivor's account."
54
+ helpmsg = ("When beneficiary fractions not all 1, "
55
+ "set surplus deposits to all go to account of first spouse to pass.")
55
56
  ret = kz.getNum(
56
57
  f"Fraction deposited in {iname1}'s taxable account",
57
58
  "surplusFraction",
@@ -55,7 +55,8 @@ are entered and reported in unit dollars.
55
55
 
56
56
  If you are accessing Owl through the Chrome browser,
57
57
  the performance manager might be configured to disable hidden or inactive tabs.
58
- This will cause your Owl session to inadvertently reset, and losing the state of the calculator.
58
+ This could cause your Owl session to inadvertently reset when idling for too long,
59
+ and losing the state of the calculator.
59
60
  To avoid this, configure Chrome to keep the page active using
60
61
  `More Tools` -> `Performance` -> `Always keep these sites active` and
61
62
  add the site *owlplanner.streamlit.app*.
@@ -80,8 +81,13 @@ and life expectancies are required. A starting date for the plan determines when
80
81
  starts in the first year. Plan still ends at the end of the year when all individuals
81
82
  have passed according to the specified life expectancies.
82
83
 
84
+ A typical workflow will involve creating
85
+ a base case, and duplicating it a few times with slight parameter changes
86
+ for investigating the resulting effects.
87
+ It is recommended to rename the case to reflect the change in parameters.
83
88
  When duplicating a scenario, make sure to visit all pages in the **Case Setup** section
84
- and verify that all parameters are as intended.
89
+ and verify that all parameters are as intended. When all cases were succussfully run,
90
+ results of the different cases can be compared side-by-side in the `Output Files` section.
85
91
 
86
92
  ##### Initializing the life parameters for the realization
87
93
  Start with the `Case selector` box and choose one of `New case...` or `Upload case file...`.
@@ -276,6 +282,8 @@ button is toggled, in which case Roth conversions will not be optimized,
276
282
  but will rather be performed according to
277
283
  the `Roth conv` column on the
278
284
  [Wages and Contributions](#wages-and-contributions) page.
285
+ A year from which Roth conversions can begin to be considered can also be selected: no Roth
286
+ conversions will be allowed before the year specified.
279
287
 
280
288
  Calculations of Medicare and IRMAA can be turned on or off. This will typically speed up
281
289
  the calculations by a factor of 2 to 3, which can be useful when running Monte Carlo simulations.
@@ -1,4 +1,5 @@
1
1
  import streamlit as st
2
+ from datetime import date
2
3
 
3
4
  import sskeys as kz
4
5
  import owlbridge as owb
@@ -49,10 +50,15 @@ else:
49
50
  kz.initKey("readRothX", False)
50
51
  fromFile = kz.getKey("readRothX")
51
52
  kz.initKey("maxRothConversion", 50)
52
- ret = kz.getNum("Maximum Roth conversion (\\$k)", "maxRothConversion", disabled=fromFile, help=helpmsg)
53
+ ret = kz.getNum("Maximum annual Roth conversion (\\$k)", "maxRothConversion", disabled=fromFile, help=helpmsg)
53
54
  ret = kz.getToggle("Convert as in wages and contributions tables", "readRothX")
54
55
 
55
56
  with col2:
57
+ helpmsg = "Do not perform Roth conversions before that year."
58
+ thisyear = date.today().year
59
+ kz.initKey("startRothConversions", thisyear)
60
+ ret = kz.getIntNum("Year to start considering Roth conversions", "startRothConversions", min_value=thisyear,
61
+ disabled=fromFile, help=helpmsg)
56
62
  if kz.getKey("status") == "married":
57
63
  iname1 = kz.getKey("iname1")
58
64
  choices = ["None", iname0, iname1]
@@ -3,8 +3,9 @@ matplotlib
3
3
  seaborn
4
4
  pandas
5
5
  openpyxl
6
+ odfpy
6
7
  scipy
7
8
  streamlit
8
9
  toml
9
10
  # --extra-index-url https://test.pypi.org/simple
10
- owlplanner >= 2025.03.30
11
+ owlplanner >= 2025.04.02
@@ -360,7 +360,7 @@ def getSolveParameters():
360
360
  objective = "maxBequest"
361
361
 
362
362
  options = {}
363
- optList = ["netSpending", "maxRothConversion", "noRothConversions",
363
+ optList = ["netSpending", "maxRothConversion", "noRothConversions", "startRothConversions",
364
364
  "withMedicare", "bequest", "solver", "spendingSlack"]
365
365
  for opt in optList:
366
366
  val = getKey(opt)
@@ -1 +0,0 @@
1
- __version__ = "2025.03.30"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes