owlplanner 2025.8.1__tar.gz → 2025.9.15__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 (134) hide show
  1. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/PKG-INFO +2 -2
  2. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/README.md +1 -1
  3. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/examples/case_drawdowncalc-comparison-1.toml +2 -1
  4. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/pyproject.toml +1 -1
  5. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/requirements.txt +1 -1
  6. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/src/owlplanner/config.py +4 -0
  7. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/src/owlplanner/plan.py +9 -10
  8. owlplanner-2025.9.15/src/owlplanner/version.py +1 -0
  9. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/Documentation.py +10 -14
  10. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/Optimization_Parameters.py +15 -10
  11. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/Output_Files.py +1 -1
  12. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/owlbridge.py +4 -4
  13. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/sskeys.py +1 -1
  14. owlplanner-2025.8.1/examples/joe.xlsx +0 -0
  15. owlplanner-2025.8.1/examples/john+sally.xlsx +0 -0
  16. owlplanner-2025.8.1/examples/jon+jane.xlsx +0 -0
  17. owlplanner-2025.8.1/examples/kim+sam.xlsx +0 -0
  18. owlplanner-2025.8.1/examples/template.xlsx +0 -0
  19. owlplanner-2025.8.1/examples.new/case_drawdowncalc-comparison-1.toml +0 -56
  20. owlplanner-2025.8.1/examples.new/case_jack+jill.toml +0 -57
  21. owlplanner-2025.8.1/examples.new/case_joe.toml +0 -54
  22. owlplanner-2025.8.1/examples.new/case_john+sally.toml +0 -53
  23. owlplanner-2025.8.1/examples.new/case_jon+jane.toml +0 -56
  24. owlplanner-2025.8.1/examples.new/case_kim+sam-bequest.toml +0 -56
  25. owlplanner-2025.8.1/examples.new/case_kim+sam-spending.toml +0 -56
  26. owlplanner-2025.8.1/examples.new/jack+jill.xlsx +0 -0
  27. owlplanner-2025.8.1/mediopt/owl.pdf +0 -0
  28. owlplanner-2025.8.1/mediopt/owl.tex +0 -1524
  29. owlplanner-2025.8.1/mediopt/owl.tex.merged +0 -1524
  30. owlplanner-2025.8.1/mediopt/piecewiseConstant.png +0 -0
  31. owlplanner-2025.8.1/mediopt/plan.py +0 -3077
  32. owlplanner-2025.8.1/mediopt/tax2025.py +0 -294
  33. owlplanner-2025.8.1/src/owlplanner/version.py +0 -1
  34. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/.devcontainer/devcontainer.json +0 -0
  35. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/.flake8 +0 -0
  36. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/.gitattributes +0 -0
  37. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/.github/workflows/github-actions-runtests.yml +0 -0
  38. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/.gitignore +0 -0
  39. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/.streamlit/config.toml +0 -0
  40. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/.streamlit/fullconfig.toml +0 -0
  41. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/INSTALL.md +0 -0
  42. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/LICENSE +0 -0
  43. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/RELEASE_NOTES.md +0 -0
  44. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/USER_GUIDE.md +0 -0
  45. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docker/Dockerfile.build +0 -0
  46. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docker/Dockerfile.run +0 -0
  47. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docker/README.md +0 -0
  48. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docker/buildentrypoint.sh +0 -0
  49. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docker/docker-compose.yml +0 -0
  50. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docker/runentrypoint.sh +0 -0
  51. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/images/AD-taxDef.png +0 -0
  52. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/images/AD-taxFree.png +0 -0
  53. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/images/AD-taxable.png +0 -0
  54. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/images/Hist_Bequest.png +0 -0
  55. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/images/Hist_Spending.png +0 -0
  56. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/images/MC-tutorial2a.png +0 -0
  57. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/images/MC-tutorial2b.png +0 -0
  58. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/images/OwlUI.png +0 -0
  59. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/images/allocations.png +0 -0
  60. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/images/owl.png +0 -0
  61. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/images/piecewiseConstant.png +0 -0
  62. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/images/profile.png +0 -0
  63. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/images/ratesCorrelations.png +0 -0
  64. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/images/ratesPlot.png +0 -0
  65. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/images/savingsPlot.png +0 -0
  66. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/images/sourcesPlot.png +0 -0
  67. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/images/spendingPlot.png +0 -0
  68. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/images/taxIncomePlot.png +0 -0
  69. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/images/taxesPlot.png +0 -0
  70. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/owl.pdf +0 -0
  71. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/docs/owl.tex +0 -0
  72. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/examples/case_jack+jill.toml +0 -0
  73. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/examples/case_joe.toml +0 -0
  74. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/examples/case_john+sally.toml +0 -0
  75. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/examples/case_jon+jane.toml +0 -0
  76. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/examples/case_kim+sam-bequest.toml +0 -0
  77. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/examples/case_kim+sam-spending.toml +0 -0
  78. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/examples/jack+jill.xlsx +0 -0
  79. {owlplanner-2025.8.1/examples.new → owlplanner-2025.9.15/examples}/joe.xlsx +0 -0
  80. {owlplanner-2025.8.1/examples.new → owlplanner-2025.9.15/examples}/john+sally.xlsx +0 -0
  81. {owlplanner-2025.8.1/examples.new → owlplanner-2025.9.15/examples}/jon+jane.xlsx +0 -0
  82. {owlplanner-2025.8.1/examples.new → owlplanner-2025.9.15/examples}/kim+sam.xlsx +0 -0
  83. {owlplanner-2025.8.1/examples.new → owlplanner-2025.9.15/examples}/template.xlsx +0 -0
  84. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/notebooks/john+sally.ipynb +0 -0
  85. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/notebooks/kim+sam.ipynb +0 -0
  86. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/notebooks/template.ipynb +0 -0
  87. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/notebooks/tutorial_1.ipynb +0 -0
  88. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/notebooks/tutorial_2.ipynb +0 -0
  89. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/notebooks/tutorial_3.ipynb +0 -0
  90. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/owlplanner.cmd +0 -0
  91. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/owlplanner.sh +0 -0
  92. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/pytest.ini +0 -0
  93. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/src/owlplanner/__init__.py +0 -0
  94. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/src/owlplanner/abcapi.py +0 -0
  95. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/src/owlplanner/data/__init__.py +0 -0
  96. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/src/owlplanner/data/rates.csv +0 -0
  97. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/src/owlplanner/mylogging.py +0 -0
  98. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/src/owlplanner/plotting/__init__.py +0 -0
  99. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/src/owlplanner/plotting/base.py +0 -0
  100. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/src/owlplanner/plotting/factory.py +0 -0
  101. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/src/owlplanner/plotting/matplotlib_backend.py +0 -0
  102. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/src/owlplanner/plotting/plotly_backend.py +0 -0
  103. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/src/owlplanner/progress.py +0 -0
  104. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/src/owlplanner/rates.py +0 -0
  105. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/src/owlplanner/tax2025.py +0 -0
  106. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/src/owlplanner/timelists.py +0 -0
  107. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/src/owlplanner/utils.py +0 -0
  108. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/tests/test_logger.py +0 -0
  109. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/tests/test_regressions.py +0 -0
  110. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/tests/test_repro.py +0 -0
  111. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/tests/test_toml_cases.py +0 -0
  112. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/tests/test_ui_asset_allocation.py +0 -0
  113. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/tests/test_ui_compare_summaries.py +0 -0
  114. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/tests/test_ui_sskeys.py +0 -0
  115. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/tests/test_units.py +0 -0
  116. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/About_Owl.py +0 -0
  117. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/Asset_Allocation.py +0 -0
  118. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/Create_Case.py +0 -0
  119. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/Current_Assets.py +0 -0
  120. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/Fixed_Income.py +0 -0
  121. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/Graphs.py +0 -0
  122. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/Historical_Range.py +0 -0
  123. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/Logs.py +0 -0
  124. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/Monte_Carlo.py +0 -0
  125. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/Quick_Start.py +0 -0
  126. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/README.md +0 -0
  127. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/Rates_Selection.py +0 -0
  128. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/Settings.py +0 -0
  129. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/Wages_and_Contributions.py +0 -0
  130. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/Worksheets.py +0 -0
  131. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/__init__.py +0 -0
  132. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/main.py +0 -0
  133. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/progress.py +0 -0
  134. {owlplanner-2025.8.1 → owlplanner-2025.9.15}/ui/tomlexamples.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owlplanner
3
- Version: 2025.8.1
3
+ Version: 2025.9.15
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
@@ -744,7 +744,7 @@ Look at *Basic capabilities* below for more detail.
744
744
  One can certainly have a savings plan, but due to the volatility of financial investments,
745
745
  it is impossible to have a certain asset earnings plan. This does not mean one cannot make decisions.
746
746
  These decisions need to be guided with an understanding of the sensitivity of the parameters.
747
- This is exactly where this tool fits it. Given your savings capabilities and spending desires,
747
+ This is exactly where this tool fits in. Given your savings capabilities and spending desires,
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
 
@@ -38,7 +38,7 @@ Look at *Basic capabilities* below for more detail.
38
38
  One can certainly have a savings plan, but due to the volatility of financial investments,
39
39
  it is impossible to have a certain asset earnings plan. This does not mean one cannot make decisions.
40
40
  These decisions need to be guided with an understanding of the sensitivity of the parameters.
41
- This is exactly where this tool fits it. Given your savings capabilities and spending desires,
41
+ This is exactly where this tool fits in. Given your savings capabilities and spending desires,
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
 
@@ -1,5 +1,5 @@
1
1
  "Plan Name" = "drawdowncalc-comparison-1"
2
- Description = "This is a case involving a single individual. Case is used for comparing with @hugcity's DrawdownCalc. For max bequest, it should yield $90,792, while an $80k net spending should leave $673,156 as a bequest."
2
+ Description = "This is a case involving a single individual. Case is used for comparing with @hugcity's DrawdownCalc. For max bequest, it should yield $90,882, while an $80k net spending should leave $678,625 as a bequest."
3
3
 
4
4
  ["Basic Info"]
5
5
  Status = "single"
@@ -51,6 +51,7 @@ startRothConversions = 2025
51
51
  withSCLoop = false
52
52
  solver = "HiGHS"
53
53
  spendingSlack = 0
54
+ withMedicare = "None"
54
55
 
55
56
  [Results]
56
57
  "Default plots" = "today"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "owlplanner"
7
- version = "2025.08.01"
7
+ version = "2025.09.15"
8
8
  authors = [
9
9
  { name="Martin-D. Lacasse", email="martin.d.lacasse@gmail.com" },
10
10
  ]
@@ -8,5 +8,5 @@ odfpy
8
8
  plotly
9
9
  pulp
10
10
  scipy
11
- streamlit >= 1.46.1
11
+ streamlit >= 1.49
12
12
  toml
@@ -296,6 +296,10 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
296
296
  # Solver Options.
297
297
  p.solverOptions = diconf["Solver Options"]
298
298
 
299
+ # Address legacy case files.
300
+ if diconf["Solver Options"].get("withMedicare", None) is True:
301
+ p.solverOptions["withMedicare"] = "loop"
302
+
299
303
  # Check consistency of noRothConversions.
300
304
  name = p.solverOptions.get("noRothConversions", "None")
301
305
  if name != "None" and name not in p.inames:
@@ -1021,7 +1021,7 @@ class Plan(object):
1021
1021
  Refer to companion document for explanations.
1022
1022
  All binary variables must be lumped at the end of the vector.
1023
1023
  """
1024
- medi = options.get("optimizeMedicare", False)
1024
+ medi = options.get("withMedicare", "loop") == "optimize"
1025
1025
 
1026
1026
  # Stack all variables in a single block vector with all binary variables at the end.
1027
1027
  C = {}
@@ -1386,7 +1386,7 @@ class Plan(object):
1386
1386
  self.B.setRange(_q3(self.C["zx"], i, n, 1, self.N_i, self.N_n, self.N_zx), 0, 0)
1387
1387
 
1388
1388
  def _configure_Medicare_binary_variables(self, options):
1389
- if not options.get("optimizeMedicare", False):
1389
+ if options.get("withMedicare", "loop") != "optimize":
1390
1390
  return
1391
1391
 
1392
1392
  bigM = options.get("bigM", 5e6)
@@ -1444,7 +1444,7 @@ class Plan(object):
1444
1444
  self.A.addRow(row2, -np.inf, rhs2)
1445
1445
 
1446
1446
  def _add_Medicare_costs(self, options):
1447
- if not options.get("optimizeMedicare", False):
1447
+ if options.get("withMedicare", "loop") != "optimize":
1448
1448
  return
1449
1449
 
1450
1450
  for n in range(self.nm):
@@ -1627,7 +1627,7 @@ class Plan(object):
1627
1627
  "netSpending",
1628
1628
  "noRothConversions",
1629
1629
  "oppCostX",
1630
- "optimizeMedicare",
1630
+ "withMedicare",
1631
1631
  "previousMAGIs",
1632
1632
  "solver",
1633
1633
  "spendingSlack",
@@ -1635,7 +1635,6 @@ class Plan(object):
1635
1635
  "units",
1636
1636
  "xorConstraints",
1637
1637
  "withSCLoop",
1638
- "withMedicare", # Ignore keyword.
1639
1638
  ]
1640
1639
  # We might modify options if required.
1641
1640
  options = {} if options is None else options
@@ -1713,7 +1712,7 @@ class Plan(object):
1713
1712
  """
1714
1713
  Self-consistent loop, regardless of solver.
1715
1714
  """
1716
- optimizeMedicare = options.get("optimizeMedicare", False)
1715
+ includeMedicare = options.get("withMedicare", "loop") == "loop"
1717
1716
  withSCLoop = options.get("withSCLoop", True)
1718
1717
 
1719
1718
  if objective == "maxSpending":
@@ -1724,7 +1723,7 @@ class Plan(object):
1724
1723
  it = 0
1725
1724
  old_x = np.zeros(self.nvars)
1726
1725
  old_objfns = [np.inf]
1727
- self._computeNLstuff(None, optimizeMedicare)
1726
+ self._computeNLstuff(None, includeMedicare)
1728
1727
  while True:
1729
1728
  objfn, xx, solverSuccess, solverMsg = solverMethod(objective, options)
1730
1729
 
@@ -1735,7 +1734,7 @@ class Plan(object):
1735
1734
  if not withSCLoop:
1736
1735
  break
1737
1736
 
1738
- self._computeNLstuff(xx, optimizeMedicare)
1737
+ self._computeNLstuff(xx, includeMedicare)
1739
1738
 
1740
1739
  delta = xx - old_x
1741
1740
  absSolDiff = np.sum(np.abs(delta), axis=0)/100
@@ -1955,7 +1954,7 @@ class Plan(object):
1955
1954
 
1956
1955
  return J_n
1957
1956
 
1958
- def _computeNLstuff(self, x, optimizeMedicare):
1957
+ def _computeNLstuff(self, x, includeMedicare):
1959
1958
  """
1960
1959
  Compute MAGI, Medicare costs, long-term capital gain tax rate, and
1961
1960
  net investment income tax (NIIT).
@@ -1972,7 +1971,7 @@ class Plan(object):
1972
1971
  self.J_n = self._computeNIIT(self.MAGI_n, self.I_n, self.Q_n)
1973
1972
  self.psi_n = tx.capitalGainTaxRate(self.N_i, self.MAGI_n, self.gamma_n[:-1], self.n_d, self.N_n)
1974
1973
  # Compute Medicare through self-consistent loop.
1975
- if not optimizeMedicare:
1974
+ if includeMedicare:
1976
1975
  self.M_n = tx.mediCosts(self.yobs, self.horizons, self.MAGI_n, self.prevMAGI, self.gamma_n[:-1], self.N_n)
1977
1976
 
1978
1977
  return None
@@ -0,0 +1 @@
1
+ __version__ = "2025.09.15"
@@ -400,27 +400,23 @@ no Roth conversions will be allowed before the year specified.
400
400
  A self-consistent loop is used to compute values that are difficult
401
401
  to integrate into a linear program. These includes the net investment income tax (NIIT),
402
402
  the rate on capital gains (0, 15, or 20%), the phase out of the additional exemption for seniors,
403
- and the Medicare and IRMAA premiums.
403
+ and potentially the Medicare and IRMAA premiums.
404
404
  Turning off the self-consistent loop will default all these values to zero.
405
+
405
406
  An additional setting allows to turn off mutually exclusive operations,
406
407
  such as Roth conversions and withdrawals from the tax-free account.
407
- Surprinsingly, dropping these constraints can lead to slightly different optimal points.
408
-
409
- Mutually exclusive constraints can be imposed between Roth conversions and
410
- withdrawals from tax-free accounts. Similarly, withdrawals from taxable
411
- and tax-free accounts should not exist if there is a surplus.
412
408
  Enabling these mutually exclusive constraints avoids both these situations.
413
- Enabling these constraints will slightly increase the computation time due to the
414
- binary variables involved. Also, the resulting optimized values will be slightly
415
- different between cases with the constraints on or off for reasons that escape me.
409
+ Surprinsingly, dropping these constraints can lead to slightly different optimal points
410
+ for reasons that escape me.
416
411
 
417
412
  Medicare premiums start automatically in the year each individual reach age 65.
418
- Calculations of Medicare and IRMAA can either be optimized or be calculated through
419
- the self-consistent loop.
420
- Optimizing (as opposed to calculating) Medicare and IRMAA is slower given
413
+ Calculations of Medicare and IRMAA can be turned off, calculated through
414
+ the self-consistent loop, or be integrated into the optimization.
415
+ Optimizing (as opposed to calculating) Medicare and IRMAA can sometimes be much slower given
421
416
  the additional number of binary variables involved. This option should be used
422
- for a single case, and compared with results obtained from the self-consistent calculations.
423
- Medicare optimization should not be used when running Monte Carlo simulations.
417
+ for a single case, and compared with results obtained from self-consistent calculations.
418
+ Medicare optimization should not be used when running multiple scenarios such
419
+ as in Monte Carlo simulations.
424
420
 
425
421
  If the current age of any individual in the plan makes them eligible
426
422
  for Medicare within the next two years,
@@ -11,6 +11,8 @@ kz.initKey("survivor", 60)
11
11
  kz.initKey("smileDip", 15)
12
12
  kz.initKey("smileIncrease", 12)
13
13
  kz.initKey("smileDelay", 0)
14
+ mediChoices = ["None", "loop", "optimize"]
15
+ kz.initKey("withMedicare", mediChoices[1])
14
16
 
15
17
 
16
18
  def initProfile():
@@ -78,8 +80,8 @@ else:
78
80
  col1, col2 = st.columns(2, gap="large", vertical_alignment="top")
79
81
  with col1:
80
82
  helpmsg = ("Option to use a self-consistent loop to adjust additional values such as the net"
81
- " investment income tax (NIIT), and adjust capital gain tax rates."
82
- " This loop also computes Medicare and IRMAA if not optimized.")
83
+ " investment income tax (NIIT), and capital gain tax rates."
84
+ " If selected below, this loop will also compute Medicare and IRMAA.")
83
85
  ret = kz.getToggle("Self-consistent loop calculations", "withSCLoop", help=helpmsg)
84
86
  with col2:
85
87
  helpmsg = ("Enable mutually exclusive constraints between surplus deposits,"
@@ -89,16 +91,19 @@ else:
89
91
 
90
92
  st.divider()
91
93
  st.write("#### :orange[Medicare]")
92
- kz.initKey("optimizeMedicare", False)
93
94
  col1, col2 = st.columns(2, gap="large", vertical_alignment="top")
94
- with col2:
95
- st.markdown("##### :material/warning: Slow convergence - maybe time for a coffee :coffee:?")
96
- helpmsg = ("Optimize for Medicare and IRMAA."
97
- " Due to the binary variables involved, this requires a few minutes of computation."
98
- " Be patient.")
99
- ret = kz.getToggle("Optimize Medicare and IRMAA", "optimizeMedicare", help=helpmsg)
100
95
  with col1:
101
- if kz.getKey("optimizeMedicare") or kz.getKey("withSCLoop"):
96
+ helpmsg = ("How to compute Medicare and IRMAA premiums:"
97
+ " ignore, use self-consistent loop, or use additional variables in optimization.")
98
+ ret = kz.getRadio("Medicare and IRMAA calculations", mediChoices, "withMedicare", help=helpmsg)
99
+ if ret == "optimize":
100
+ st.markdown(":material/warning: Medicare optimization can sometimes have slow convergence -"
101
+ " time for :coffee: ?")
102
+ elif ret == "loop" and not kz.getKey("withSCLoop"):
103
+ st.markdown(":material/warning: Medicare set to 'loop' while self-consistent loop is off.")
104
+ with col2:
105
+ medi = kz.getKey("withMedicare")
106
+ if medi == "optimize" or (medi == "loop" and kz.getKey("withSCLoop")):
102
107
  helpmsg = "MAGI in nominal $k for current and previous years."
103
108
  years = owb.backYearsMAGI()
104
109
  for ii in range(2):
@@ -23,7 +23,7 @@ else:
23
23
  "This table provides a summary of the current case and"
24
24
  " compares it with other similar cases that ran successfully.")
25
25
  styledDf = df[1:].style.map(kz.colorBySign)
26
- st.dataframe(styledDf, use_container_width=True)
26
+ st.dataframe(styledDf, width="stretch")
27
27
  st.caption("Values with [legend] are nominal, otherwise in today's \\$. "
28
28
  "Lines starting with » indicate itemized subtotals.")
29
29
  col1, col2 = st.columns(2, gap="large")
@@ -559,7 +559,7 @@ def showWorkbook(plan):
559
559
  colfor[col] = st.column_config.NumberColumn(None, format="%.3f")
560
560
 
561
561
  st.write(f"#### :orange[{name}]")
562
- st.dataframe(df.astype(str), use_container_width=True, column_config=colfor, hide_index=True)
562
+ st.dataframe(df.astype(str), width="stretch", column_config=colfor, hide_index=True)
563
563
 
564
564
  if dollars:
565
565
  st.caption("Values are in nominal $, rounded to the nearest dollar.")
@@ -705,7 +705,7 @@ def genDic(plan):
705
705
 
706
706
  optionKeys = list(plan.solverOptions)
707
707
  optList = ["netSpending", "maxRothConversion", "noRothConversions",
708
- "startRothConversions", "optimizeMedicare", "bequest", "solver",
708
+ "startRothConversions", "withMedicare", "bequest", "solver",
709
709
  "spendingSlack", "oppCostX", "xorConstraints", "withSCLoop",]
710
710
  for key in optList:
711
711
  if key in optionKeys:
@@ -779,9 +779,9 @@ def renderPlot(fig, col=None):
779
779
  # Check if it's a plotly figure.
780
780
  if hasattr(fig, 'to_dict'): # plotly figures have to_dict method.
781
781
  if col:
782
- col.plotly_chart(fig, use_container_width=True)
782
+ col.plotly_chart(fig, width="stretch")
783
783
  else:
784
- st.plotly_chart(fig, use_container_width=True)
784
+ st.plotly_chart(fig, width="stretch")
785
785
  else: # matplotlib figure.
786
786
  if col:
787
787
  col.pyplot(fig)
@@ -369,7 +369,7 @@ def getSolveParameters():
369
369
 
370
370
  options = {}
371
371
  optList = ["netSpending", "maxRothConversion", "noRothConversions",
372
- "startRothConversions", "optimizeMedicare", "bequest", "solver",
372
+ "startRothConversions", "withMedicare", "bequest", "solver",
373
373
  "spendingSlack", "oppCostX", "xorConstraints", "withSCLoop",]
374
374
  for opt in optList:
375
375
  val = getKey(opt)
Binary file
Binary file
Binary file
Binary file
@@ -1,56 +0,0 @@
1
- "Plan Name" = "drawdowncalc-comparison-1"
2
- Description = "This is a case involving a single individual. Case is used for comparing with @hugcity's DrawdownCalc. For max bequest, it should yield $90,792, while an $80k net spending should leave $673,156 as a bequest."
3
-
4
- ["Basic Info"]
5
- Status = "single"
6
- Names = [ "Charles",]
7
- "Birth year" = [ 1966,]
8
- "Life expectancy" = [ 89,]
9
- "Start date" = "2025-01-01"
10
-
11
- [Assets]
12
- "taxable savings balances" = [ 250.0,]
13
- "tax-deferred savings balances" = [ 600.0,]
14
- "tax-free savings balances" = [ 600.0,]
15
-
16
- ["Wages and Contributions"]
17
- "Contributions file name" = "edited values"
18
-
19
- ["Fixed Income"]
20
- "Pension amounts" = [ 0.0,]
21
- "Pension ages" = [ 65,]
22
- "Pension indexed" = [ true,]
23
- "Social security amounts" = [ 36.0,]
24
- "Social security ages" = [ 70,]
25
-
26
- ["Rates Selection"]
27
- "Heirs rate on tax-deferred estate" = 0.0
28
- "Dividend rate" = 0.0
29
- "TCJA expiration year" = 2099
30
- Method = "user"
31
- Values = [ 6.5, 0.0, 0.0, 2.8,]
32
- From = 1928
33
- To = 2024
34
-
35
- ["Asset Allocation"]
36
- "Interpolation method" = "s-curve"
37
- "Interpolation center" = 15.0
38
- "Interpolation width" = 5.0
39
- Type = "individual"
40
- generic = [ [ [ 100, 0, 0, 0,], [ 100, 0, 0, 0,],],]
41
-
42
- ["Optimization Parameters"]
43
- "Spending profile" = "flat"
44
- "Surviving spouse spending percent" = 60
45
- Objective = "maxBequest"
46
-
47
- ["Solver Options"]
48
- netSpending = 80.0
49
- maxRothConversion = 50
50
- startRothConversions = 2025
51
- withMedicare = false
52
- solver = "HiGHS"
53
- spendingSlack = 0
54
-
55
- [Results]
56
- "Default plots" = "today"
@@ -1,57 +0,0 @@
1
- "Plan Name" = "jack+jill"
2
- Description = "This example aims to demonstrate some of Owl's capabilities. Jack and Jill are a married couple a few years from retirement. A wages and contributions file called 'jack+jill.xlsx' is associated with this case. This case uses the historical rate sequence of 1969 as a test case for guiding spending amounts from a near worst-case historical scenario. This case also demonstrates that the optimal strategy for Roth conversions does not necessarily involve surfing a tax bracket. \nA good exercise for learning Owl's capabilities is to duplicate this case and compare two scenarios: one with optimized Roth conversions and one without. Another possible exercise could involve comparing a historical retirement in 1969 vs. one taken in 1966. Or anything else you can think of..."
3
-
4
- ["Basic Info"]
5
- Status = "married"
6
- Names = [ "Jack", "Jill",]
7
- "Birth year" = [ 1962, 1965,]
8
- "Life expectancy" = [ 89, 92,]
9
- "Start date" = "01-01"
10
-
11
- [Assets]
12
- "taxable savings balances" = [ 90.5, 60.2,]
13
- "tax-deferred savings balances" = [ 600.2, 150.0,]
14
- "tax-free savings balances" = [ 70.6, 40.8,]
15
- "Beneficiary fractions" = [ 1, 1, 1,]
16
- "Spousal surplus deposit fraction" = 0.0
17
-
18
- ["Wages and Contributions"]
19
- "Contributions file name" = "jack+jill.xlsx"
20
-
21
- ["Fixed Income"]
22
- "Pension amounts" = [ 0.0, 10.5,]
23
- "Pension ages" = [ 65, 65,]
24
- "Pension indexed" = [ false, false,]
25
- "Social security amounts" = [ 28.4, 19.7,]
26
- "Social security ages" = [ 70, 62,]
27
-
28
- ["Rates Selection"]
29
- "Heirs rate on tax-deferred estate" = 30.0
30
- "Dividend rate" = 1.8
31
- "TCJA expiration year" = 2026
32
- Method = "historical"
33
- From = 1969
34
- To = 2002
35
-
36
- ["Asset Allocation"]
37
- "Interpolation method" = "s-curve"
38
- "Interpolation center" = 15
39
- "Interpolation width" = 5
40
- Type = "individual"
41
- generic = [ [ [ 60, 40, 0, 0,], [ 70, 30, 0, 0,],], [ [ 60, 40, 0, 0,], [ 80, 0, 10, 10,],],]
42
-
43
- ["Optimization Parameters"]
44
- "Spending profile" = "smile"
45
- "Smile dip" = 15
46
- "Smile increase" = 12
47
- "Smile delay" = 0
48
- "Surviving spouse spending percent" = 60
49
- Objective = "maxSpending"
50
-
51
- ["Solver Options"]
52
- maxRothConversion = 100
53
- bequest = 500
54
- noRothConversions = "Jill"
55
-
56
- [Results]
57
- "Default plots" = "today"
@@ -1,54 +0,0 @@
1
- "Plan Name" = "joe"
2
- Description = "This is an example of a case involving a single individual. Joe is single and will retire in a few years. His wages and contributions are contained in the 'joe.xlsx' spreadsheet."
3
-
4
- ["Basic Info"]
5
- Status = "single"
6
- Names = [ "Joe",]
7
- "Birth year" = [ 1966,]
8
- "Life expectancy" = [ 89,]
9
- "Start date" = "01-01"
10
-
11
- [Assets]
12
- "taxable savings balances" = [ 338.5,]
13
- "tax-deferred savings balances" = [ 650.2,]
14
- "tax-free savings balances" = [ 60.6,]
15
-
16
- ["Wages and Contributions"]
17
- "Contributions file name" = "joe.xlsx"
18
-
19
- ["Fixed Income"]
20
- "Pension amounts" = [ 18.0,]
21
- "Pension ages" = [ 65,]
22
- "Pension indexed" = [ true,]
23
- "Social security amounts" = [ 28.4,]
24
- "Social security ages" = [ 67,]
25
-
26
- ["Rates Selection"]
27
- "Heirs rate on tax-deferred estate" = 30.0
28
- "Dividend rate" = 1.8
29
- "TCJA expiration year" = 2026
30
- Method = "historical average"
31
- From = 1969
32
- To = 2002
33
-
34
- ["Asset Allocation"]
35
- "Interpolation method" = "s-curve"
36
- "Interpolation center" = 15
37
- "Interpolation width" = 5
38
- Type = "individual"
39
- generic = [ [ [ 60, 40, 0, 0,], [ 70, 30, 0, 0,],],]
40
-
41
- ["Optimization Parameters"]
42
- "Spending profile" = "smile"
43
- "Smile dip" = 15
44
- "Smile increase" = 12
45
- "Smile delay" = 0
46
- "Surviving spouse spending percent" = 60
47
- Objective = "maxSpending"
48
-
49
- ["Solver Options"]
50
- maxRothConversion = 50
51
- bequest = 300
52
-
53
- [Results]
54
- "Default plots" = "nominal"
@@ -1,53 +0,0 @@
1
- "Plan Name" = "john+sally"
2
- Description = "This example reproduces the case of John and Sally, discussed by Eric Sajdak. This case can be used to compare the heuristic strategy of surfing a tax bracket for performing Roth conversions to a solution optimized by linear programming. The former is a good approach when one assumes fixed rates. When rates are varying, and the market drops, an optimized solution shows that it is sometime advantageous to convert above the target tax bracket. File 'john+sally.xlsx' contains wages and contributions associated with this case. Run this case with different rates to see the effects on Roth conversions."
3
-
4
- ["Basic Info"]
5
- Status = "married"
6
- Names = [ "John", "Sally",]
7
- "Birth year" = [ 1962, 1962,]
8
- "Life expectancy" = [ 92, 92,]
9
- "Start date" = "01-01"
10
-
11
- [Assets]
12
- "taxable savings balances" = [ 200.0, 200.0,]
13
- "tax-deferred savings balances" = [ 750.0, 750.0,]
14
- "tax-free savings balances" = [ 50.0, 50.0,]
15
- "Beneficiary fractions" = [ 1, 1, 1,]
16
- "Spousal surplus deposit fraction" = 0.5
17
-
18
- ["Wages and Contributions"]
19
- "Contributions file name" = "john+sally.xlsx"
20
-
21
- ["Fixed Income"]
22
- "Pension amounts" = [ 0.0, 0.0,]
23
- "Pension ages" = [ 65, 65,]
24
- "Pension indexed" = [ false, false,]
25
- "Social security amounts" = [ 36.0, 21.6,]
26
- "Social security ages" = [ 67, 67,]
27
-
28
- ["Rates Selection"]
29
- "Heirs rate on tax-deferred estate" = 30.0
30
- "Dividend rate" = 1.8
31
- "TCJA expiration year" = 2026
32
- Method = "historical average"
33
- From = 1990
34
- To = 2023
35
-
36
- ["Asset Allocation"]
37
- "Interpolation method" = "linear"
38
- "Interpolation center" = 15
39
- "Interpolation width" = 5
40
- Type = "individual"
41
- generic = [ [ [ 60, 40, 0, 0,], [ 60, 40, 0, 0,],], [ [ 60, 40, 0, 0,], [ 60, 40, 0, 0,],],]
42
-
43
- ["Optimization Parameters"]
44
- "Spending profile" = "flat"
45
- "Surviving spouse spending percent" = 60
46
- Objective = "maxBequest"
47
-
48
- ["Solver Options"]
49
- maxRothConversion = 200
50
- netSpending = 100
51
-
52
- [Results]
53
- "Default plots" = "today"
@@ -1,56 +0,0 @@
1
- "Plan Name" = "Jon+Jane"
2
- Description = "This case reproduces a similar case discussed a while back on i-orp. It involves Jon and Jane close to retirement, and assumes a base case of optimistic returns of 10% with an inflation of 3.5%. A wages and contributions file called 'jon+jane.xlsx' is associated with this case."
3
-
4
- ["Basic Info"]
5
- Status = "married"
6
- Names = [ "Jon", "Jane",]
7
- "Birth year" = [ 1965, 1968,]
8
- "Life expectancy" = [ 92, 92,]
9
- "Start date" = "01-01"
10
-
11
- [Assets]
12
- "taxable savings balances" = [ 50.0, 0.0,]
13
- "tax-deferred savings balances" = [ 900.0, 500.0,]
14
- "tax-free savings balances" = [ 0.0, 0.0,]
15
- "Beneficiary fractions" = [ 1.0, 1.0, 1.0,]
16
- "Spousal surplus deposit fraction" = 0.0
17
-
18
- ["Wages and Contributions"]
19
- "Contributions file name" = "jon+jane.xslx"
20
-
21
- ["Fixed Income"]
22
- "Pension amounts" = [ 0.0, 0.0,]
23
- "Pension ages" = [ 65, 65,]
24
- "Pension indexed" = [ false, false,]
25
- "Social security amounts" = [ 21.0, 21.0,]
26
- "Social security ages" = [ 65, 65,]
27
-
28
- ["Rates Selection"]
29
- "Heirs rate on tax-deferred estate" = 30.0
30
- "Dividend rate" = 1.8
31
- "TCJA expiration year" = 2026
32
- Method = "user"
33
- Values = [ 10.0, 6.0, 5.0, 3.5000000000000004,]
34
- From = 1928
35
- To = 2024
36
-
37
- ["Asset Allocation"]
38
- "Interpolation method" = "linear"
39
- "Interpolation center" = 15.0
40
- "Interpolation width" = 5.0
41
- Type = "individual"
42
- generic = [ [ [ 100, 0, 0, 0,], [ 100, 0, 0, 0,],], [ [ 100, 0, 0, 0,], [ 100, 0, 0, 0,],],]
43
-
44
- ["Optimization Parameters"]
45
- "Spending profile" = "flat"
46
- "Surviving spouse spending percent" = 60
47
- Objective = "maxSpending"
48
-
49
- ["Solver Options"]
50
- maxRothConversion = 0.0
51
- noRothConversions = "None"
52
- withMedicare = true
53
- bequest = 30.0
54
-
55
- [Results]
56
- "Default plots" = "today"
@@ -1,56 +0,0 @@
1
- "Plan Name" = "kim+sam-bequest"
2
- Description = "This is the case of Kim and Sam used as an example case for optimizing bequest and Roth conversions. No wages and contributions file is associated with this case."
3
-
4
- ["Basic Info"]
5
- Status = "married"
6
- Names = [ "Kim", "Sam",]
7
- "Birth year" = [ 1966, 1967,]
8
- "Life expectancy" = [ 86, 89,]
9
- "Start date" = "01-01"
10
-
11
- [Assets]
12
- "taxable savings balances" = [ 1000.0, 0.0,]
13
- "tax-deferred savings balances" = [ 2000.0, 800.0,]
14
- "tax-free savings balances" = [ 500.0, 25.0,]
15
- "Beneficiary fractions" = [ 1, 1, 1,]
16
- "Spousal surplus deposit fraction" = 0.5
17
-
18
- ["Wages and Contributions"]
19
- "Contributions file name" = "kim+sam.xlsx"
20
-
21
- ["Fixed Income"]
22
- "Pension amounts" = [ 0.0, 0.0,]
23
- "Pension ages" = [ 65, 65,]
24
- "Pension indexed" = [ false, false,]
25
- "Social security amounts" = [ 45.0, 25.0,]
26
- "Social security ages" = [ 70, 68,]
27
-
28
- ["Rates Selection"]
29
- "Heirs rate on tax-deferred estate" = 33.0
30
- "Dividend rate" = 1.8
31
- "TCJA expiration year" = 2026
32
- Method = "conservative"
33
- From = 1922
34
- To = 2023
35
-
36
- ["Asset Allocation"]
37
- "Interpolation method" = "s-curve"
38
- "Interpolation center" = 15
39
- "Interpolation width" = 5
40
- Type = "individual"
41
- generic = [ [ [ 60, 40, 0, 0,], [ 70, 30, 0, 0,],], [ [ 60, 40, 0, 0,], [ 70, 30, 0, 0,],],]
42
-
43
- ["Optimization Parameters"]
44
- "Spending profile" = "smile"
45
- "Smile dip" = 15
46
- "Smile increase" = 12
47
- "Smile delay" = 0
48
- "Surviving spouse spending percent" = 60
49
- Objective = "maxBequest"
50
-
51
- ["Solver Options"]
52
- maxRothConversion = 300
53
- netSpending = 180
54
-
55
- [Results]
56
- "Default plots" = "today"