owlplanner 2025.12.3__tar.gz → 2025.12.5__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 (122) hide show
  1. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/PKG-INFO +1 -1
  2. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/RELEASE_NOTES.md +4 -0
  3. owlplanner-2025.12.5/docker/buildPackage.cmd +7 -0
  4. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/case_jon+jane.toml +1 -1
  5. owlplanner-2025.12.5/examples/ttt.toml +58 -0
  6. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/plan.py +9 -8
  7. owlplanner-2025.12.5/src/owlplanner/version.py +1 -0
  8. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/tests/test_repro.py +11 -11
  9. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/About_Owl.py +1 -1
  10. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Documentation.py +20 -11
  11. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Fixed_Income.py +3 -1
  12. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Quick_Start.py +12 -10
  13. owlplanner-2025.12.3/src/owlplanner/version.py +0 -1
  14. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/.devcontainer/devcontainer.json +0 -0
  15. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/.flake8 +0 -0
  16. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/.gitattributes +0 -0
  17. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/.github/workflows/github-actions-runtests.yml +0 -0
  18. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/.gitignore +0 -0
  19. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/.streamlit/config.toml +0 -0
  20. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/.streamlit/fullconfig.toml +0 -0
  21. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/INSTALL.md +0 -0
  22. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/LICENSE +0 -0
  23. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/README.md +0 -0
  24. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/USER_GUIDE.md +0 -0
  25. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docker/Dockerfile.bare +0 -0
  26. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docker/Dockerfile.static +0 -0
  27. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docker/README.md +0 -0
  28. /owlplanner-2025.12.3/docker/build.cmd → /owlplanner-2025.12.5/docker/buildContainers.cmd +0 -0
  29. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docker/buildentrypoint.sh +0 -0
  30. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docker/docker-compose.yml +0 -0
  31. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docker/runentrypoint.sh +0 -0
  32. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/AD-taxDef.png +0 -0
  33. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/AD-taxFree.png +0 -0
  34. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/AD-taxable.png +0 -0
  35. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/Hist_Bequest.png +0 -0
  36. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/Hist_Spending.png +0 -0
  37. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/MC-tutorial2a.png +0 -0
  38. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/MC-tutorial2b.png +0 -0
  39. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/OwlUI.png +0 -0
  40. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/allocations.png +0 -0
  41. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/owl.png +0 -0
  42. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/piecewiseConstant.png +0 -0
  43. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/profile.png +0 -0
  44. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/ratesCorrelations.png +0 -0
  45. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/ratesPlot.png +0 -0
  46. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/savingsPlot.png +0 -0
  47. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/sourcesPlot.png +0 -0
  48. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/spendingPlot.png +0 -0
  49. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/taxIncomePlot.png +0 -0
  50. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/taxesPlot.png +0 -0
  51. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/owl.pdf +0 -0
  52. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/owl.tex +0 -0
  53. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/case_drawdowncalc-comparison-1.toml +0 -0
  54. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/case_jack+jill.toml +0 -0
  55. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/case_joe.toml +0 -0
  56. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/case_john+sally.toml +0 -0
  57. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/case_kim+sam-bequest.toml +0 -0
  58. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/case_kim+sam-spending.toml +0 -0
  59. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/jack+jill.xlsx +0 -0
  60. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/joe.xlsx +0 -0
  61. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/john+sally.xlsx +0 -0
  62. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/jon+jane.xlsx +0 -0
  63. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/kim+sam.xlsx +0 -0
  64. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/template.xlsx +0 -0
  65. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/notebooks/john+sally.ipynb +0 -0
  66. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/notebooks/kim+sam.ipynb +0 -0
  67. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/notebooks/template.ipynb +0 -0
  68. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/notebooks/tutorial_1.ipynb +0 -0
  69. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/notebooks/tutorial_2.ipynb +0 -0
  70. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/notebooks/tutorial_3.ipynb +0 -0
  71. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/owlplanner.cmd +0 -0
  72. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/owlplanner.sh +0 -0
  73. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/pyproject.toml +0 -0
  74. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/pytest.ini +0 -0
  75. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/requirements.txt +0 -0
  76. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/__init__.py +0 -0
  77. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/abcapi.py +0 -0
  78. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/config.py +0 -0
  79. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/data/__init__.py +0 -0
  80. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/data/rates.csv +0 -0
  81. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/mylogging.py +0 -0
  82. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/plotting/__init__.py +0 -0
  83. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/plotting/base.py +0 -0
  84. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/plotting/factory.py +0 -0
  85. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/plotting/matplotlib_backend.py +0 -0
  86. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/plotting/plotly_backend.py +0 -0
  87. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/progress.py +0 -0
  88. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/rates.py +0 -0
  89. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/socialsecurity.py +0 -0
  90. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/tax2025.py +0 -0
  91. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/tax2026.py +0 -0
  92. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/timelists.py +0 -0
  93. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/utils.py +0 -0
  94. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/tests/test_logger.py +0 -0
  95. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/tests/test_regressions.py +0 -0
  96. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/tests/test_socsec.py +0 -0
  97. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/tests/test_toml_cases.py +0 -0
  98. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/tests/test_ui_asset_allocation.py +0 -0
  99. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/tests/test_ui_compare_summaries.py +0 -0
  100. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/tests/test_ui_sskeys.py +0 -0
  101. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/tests/test_units.py +0 -0
  102. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/AI +0 -0
  103. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Asset_Allocation.py +0 -0
  104. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Create_Case.py +0 -0
  105. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Current_Assets.py +0 -0
  106. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Graphs.py +0 -0
  107. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Historical_Range.py +0 -0
  108. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Logs.py +0 -0
  109. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Monte_Carlo.py +0 -0
  110. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Optimization_Parameters.py +0 -0
  111. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Output_Files.py +0 -0
  112. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/README.md +0 -0
  113. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Rates_Selection.py +0 -0
  114. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Settings.py +0 -0
  115. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Wages_and_Contributions.py +0 -0
  116. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Worksheets.py +0 -0
  117. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/__init__.py +0 -0
  118. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/main.py +0 -0
  119. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/owlbridge.py +0 -0
  120. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/progress.py +0 -0
  121. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/sskeys.py +0 -0
  122. {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/tomlexamples.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owlplanner
3
- Version: 2025.12.3
3
+ Version: 2025.12.5
4
4
  Summary: Owl - Optimal Wealth Lab: 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
@@ -1,3 +1,7 @@
1
+ ### Version 2025.12.05
2
+ - Added instructions for obtaining PIA
3
+ - Bug fix in Fixed Income UI
4
+
1
5
  ### Version 2025.12.03
2
6
  - Coded social security to use monthly PIA instead of annual amount
3
7
  - Added exact routines for FRA and increase/decrease factors due to claiming age
@@ -0,0 +1,7 @@
1
+ ::
2
+ :: A simple script to build Python package
3
+ ::
4
+ cd ..
5
+ del /Q dist\*
6
+ python -m build .
7
+ twine upload --repository pypi dist\*
@@ -12,7 +12,7 @@ Names = [ "Jon", "Jane",]
12
12
  [Assets]
13
13
  "taxable savings balances" = [ 50.0, 0.0,]
14
14
  "tax-deferred savings balances" = [ 900.0, 500.0,]
15
- "tax-free savings balances" = [ 0.0, 0.0,]
15
+ "tax-free savings balances" = [ 0.0, 50.0,]
16
16
  "Beneficiary fractions" = [ 1.0, 1.0, 1.0,]
17
17
  "Spousal surplus deposit fraction" = 0.0
18
18
 
@@ -0,0 +1,58 @@
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
+ "Birth month" = [ 1, 1,]
9
+ "Life expectancy" = [ 89, 92,]
10
+ "Start date" = "01-01"
11
+
12
+ [Assets]
13
+ "taxable savings balances" = [ 120.5, 60.2,]
14
+ "tax-deferred savings balances" = [ 600.2, 150.0,]
15
+ "tax-free savings balances" = [ 280.6, 260.8,]
16
+ "Beneficiary fractions" = [ 1, 1, 1,]
17
+ "Spousal surplus deposit fraction" = 0.0
18
+
19
+ ["Wages and Contributions"]
20
+ "Contributions file name" = "jack+jill.xlsx"
21
+
22
+ ["Fixed Income"]
23
+ "Pension amounts" = [ 0.0, 10.5,]
24
+ "Pension ages" = [ 65, 65,]
25
+ "Pension indexed" = [ false, false,]
26
+ "Social security PIA amounts" = [ 2360, 1642,]
27
+ "Social security ages" = [ 70, 62,]
28
+
29
+ ["Rates Selection"]
30
+ "Heirs rate on tax-deferred estate" = 30.0
31
+ "Dividend rate" = 1.8
32
+ "OBBBA expiration year" = 2032
33
+ Method = "historical"
34
+ From = 1969
35
+ To = 2002
36
+
37
+ ["Asset Allocation"]
38
+ "Interpolation method" = "s-curve"
39
+ "Interpolation center" = 15
40
+ "Interpolation width" = 5
41
+ Type = "individual"
42
+ generic = [ [ [ 60, 40, 0, 0,], [ 70, 30, 0, 0,],], [ [ 60, 40, 0, 0,], [ 80, 0, 10, 10,],],]
43
+
44
+ ["Optimization Parameters"]
45
+ "Spending profile" = "smile"
46
+ "Smile dip" = 15
47
+ "Smile increase" = 12
48
+ "Smile delay" = 0
49
+ "Surviving spouse spending percent" = 60
50
+ Objective = "maxSpending"
51
+
52
+ ["Solver Options"]
53
+ maxRothConversion = 100
54
+ bequest = 500
55
+ noRothConversions = "Jill"
56
+
57
+ [Results]
58
+ "Default plots" = "today"
@@ -1160,25 +1160,26 @@ class Plan(object):
1160
1160
  h = self.horizons[i]
1161
1161
  for n in range(h):
1162
1162
  rhs = 0
1163
- # To add compounded gains to original amount.
1163
+ # To add compounded gains to cumulative amounts. Always keep cgains >= 1.
1164
1164
  cgains = 1
1165
1165
  row = self.A.newRow()
1166
1166
  row.addElem(_q3(self.C["b"], i, 2, n, self.N_i, self.N_j, self.N_n + 1), 1)
1167
1167
  row.addElem(_q3(self.C["w"], i, 2, n, self.N_i, self.N_j, self.N_n), -1)
1168
1168
  for dn in range(1, 6):
1169
1169
  nn = n - dn
1170
- if nn >= 0: # Past of future is now or in the future: use variables and parameters.
1170
+ if nn >= 0: # Past of future is now or in the future: use variables or parameters.
1171
1171
  Tau1 = 1 + np.sum(self.alpha_ijkn[i, 2, :, nn] * self.tau_kn[:, nn], axis=0)
1172
- cgains *= Tau1
1172
+ # Ignore market downs.
1173
+ cgains *= max(1, Tau1)
1173
1174
  row.addElem(_q2(self.C["x"], i, nn, self.N_i, self.N_n), -cgains)
1174
- # If a contribution - it can be withdrawn but not the gains.
1175
+ # If a contribution, it has only penalty on gains, not on deposited amount.
1175
1176
  rhs += (cgains - 1) * self.kappa_ijn[i, 2, nn]
1176
1177
  else: # Past of future is in the past:
1177
- # Parameters are stored at the end of contributions and conversions arrays.
1178
1178
  cgains *= oldTau1
1179
- # If a contribution, it has no penalty, but assume a conversion.
1180
- # rhs += (cgains - 1) * self.kappa_ijn[i, 2, nn] + cgains * self.myRothX_in[i, nn]
1181
- rhs += cgains * self.kappa_ijn[i, 2, nn] + cgains * self.myRothX_in[i, nn]
1179
+ # Past years are stored at the end of contributions and conversions arrays.
1180
+ # Use negative index to access tail of array.
1181
+ rhs += (cgains - 1) * self.kappa_ijn[i, 2, nn] + cgains * self.myRothX_in[i, nn]
1182
+ # rhs += cgains * self.kappa_ijn[i, 2, nn] + cgains * self.myRothX_in[i, nn]
1182
1183
 
1183
1184
  self.A.addRow(row, rhs, np.inf)
1184
1185
 
@@ -0,0 +1 @@
1
+ __version__ = "2025.12.05"
@@ -38,7 +38,7 @@ def test_case1():
38
38
  p.setRates('historical', 1969)
39
39
  p.solve('maxSpending', options={'maxRothConversion': 100, 'bequest': 500})
40
40
  assert p.caseStatus == "solved"
41
- assert p.basis == pytest.approx(87372.2, abs=0.5)
41
+ assert p.basis == pytest.approx(87373.1, abs=0.5)
42
42
  assert p.bequest == pytest.approx(500000, abs=0.5)
43
43
 
44
44
 
@@ -48,7 +48,7 @@ def test_case2():
48
48
  p.solve('maxBequest', options={'maxRothConversion': 100, 'netSpending': 80})
49
49
  assert p.caseStatus == "solved"
50
50
  assert p.basis == pytest.approx(80000, abs=0.5)
51
- assert p.bequest == pytest.approx(855709.4, abs=0.5)
51
+ assert p.bequest == pytest.approx(855562.5, abs=0.5)
52
52
 
53
53
 
54
54
  def test_config1():
@@ -58,7 +58,7 @@ def test_config1():
58
58
  p.solve('maxBequest', options={'maxRothConversion': 100, 'netSpending': 80})
59
59
  assert p.caseStatus == "solved"
60
60
  assert p.basis == pytest.approx(80000, abs=0.5)
61
- assert p.bequest == pytest.approx(855709.4, abs=0.5)
61
+ assert p.bequest == pytest.approx(855562.5, abs=0.5)
62
62
  p.saveConfig()
63
63
  base_filename = 'case_' + name
64
64
  full_filename = 'case_' + name + '.toml'
@@ -67,12 +67,12 @@ def test_config1():
67
67
  p2.solve('maxBequest', options={'maxRothConversion': 100, 'netSpending': 80})
68
68
  assert p2.caseStatus == "solved"
69
69
  assert p2.basis == pytest.approx(80000, abs=0.5)
70
- assert p2.bequest == pytest.approx(855709.4, abs=0.5)
70
+ assert p2.bequest == pytest.approx(855562.5, abs=0.5)
71
71
  p3 = owl.readConfig(full_filename)
72
72
  p3.solve('maxBequest', options={'maxRothConversion': 100, 'netSpending': 80})
73
73
  assert p3.caseStatus == "solved"
74
74
  assert p3.basis == pytest.approx(80000, abs=0.5)
75
- assert p3.bequest == pytest.approx(855709.4, abs=0.5)
75
+ assert p3.bequest == pytest.approx(855562.5, abs=0.5)
76
76
  os.remove(full_filename)
77
77
 
78
78
 
@@ -83,7 +83,7 @@ def test_config2():
83
83
  p.solve('maxBequest', options={'maxRothConversion': 100, 'netSpending': 80})
84
84
  assert p.caseStatus == "solved"
85
85
  assert p.basis == pytest.approx(80000, abs=0.5)
86
- assert p.bequest == pytest.approx(855709.4, abs=0.5)
86
+ assert p.bequest == pytest.approx(855562.5, abs=0.5)
87
87
  iostring = StringIO()
88
88
  p.saveConfig(iostring)
89
89
  # print('iostream:', iostream.getvalue())
@@ -91,7 +91,7 @@ def test_config2():
91
91
  p2.solve('maxBequest', options={'maxRothConversion': 100, 'netSpending': 80})
92
92
  assert p2.caseStatus == "solved"
93
93
  assert p2.basis == pytest.approx(80000, abs=0.5)
94
- assert p2.bequest == pytest.approx(855709.4, abs=0.5)
94
+ assert p2.bequest == pytest.approx(855562.5, abs=0.5)
95
95
 
96
96
 
97
97
  def test_clone1():
@@ -101,13 +101,13 @@ def test_clone1():
101
101
  p.solve('maxBequest', options={'maxRothConversion': 100, 'netSpending': 80})
102
102
  assert p.caseStatus == "solved"
103
103
  assert p.basis == pytest.approx(80000, abs=0.5)
104
- assert p.bequest == pytest.approx(855709.4, abs=0.5)
104
+ assert p.bequest == pytest.approx(855562.5, abs=0.5)
105
105
  name2 = 'testclone1.2'
106
106
  p2 = owl.clone(p, name2)
107
107
  p2.solve('maxBequest', options={'maxRothConversion': 100, 'netSpending': 80})
108
108
  assert p2.caseStatus == "solved"
109
109
  assert p2.basis == pytest.approx(80000, abs=0.5)
110
- assert p2.bequest == pytest.approx(855709.4, abs=0.5)
110
+ assert p2.bequest == pytest.approx(855562.5, abs=0.5)
111
111
 
112
112
 
113
113
  def test_clone2():
@@ -116,11 +116,11 @@ def test_clone2():
116
116
  p.setRates('historical', 1969)
117
117
  p.solve('maxSpending', options={'maxRothConversion': 100, 'bequest': 10})
118
118
  assert p.caseStatus == "solved"
119
- assert p.basis == pytest.approx(97550.5, abs=0.5)
119
+ assert p.basis == pytest.approx(97549.8, abs=0.5)
120
120
  assert p.bequest == pytest.approx(10000, abs=0.5)
121
121
  name2 = 'testclone2.2'
122
122
  p2 = owl.clone(p, name2)
123
123
  p2.solve('maxSpending', options={'maxRothConversion': 100, 'bequest': 10})
124
124
  assert p2.caseStatus == "solved"
125
- assert p2.basis == pytest.approx(97550.5, abs=0.5)
125
+ assert p2.basis == pytest.approx(97549.8, abs=0.5)
126
126
  assert p2.bequest == pytest.approx(10000, abs=0.5)
@@ -42,7 +42,7 @@ It can also run on [MOSEK](https://mosek.com) if available on your computer.
42
42
  Dale Seng (sengsational) for great insights and suggestions,
43
43
  Robert E. Anderson (NH-RedAnt) for bug fixes and suggestions,
44
44
  Clark Jefcoat (hubcity) for fruitful interactions,
45
- Benjamin Quinn (blquinn) and Gene Wood (gene1wood) for improvements and for bug fixes.
45
+ Benjamin Quinn (blquinn) and Gene Wood (gene1wood) for improvements and bug fixes.
46
46
  - Owl image is from [freepik](https://freepik.com).
47
47
 
48
48
  #### :orange[Bugs and Feature Requests]
@@ -275,19 +275,28 @@ by reloading the same **Wages and Contributions** file.
275
275
  #### :material/currency_exchange: Fixed Income
276
276
  This page is for entering data related to the individual's anticipated fixed income
277
277
  from pensions and social security.
278
- Unlike other parts of the interface, amounts on this page are
278
+ Unlike other parts of the user interface, amounts on this page are
279
279
  monthly amounts in today's \\$ and not in thousands.
280
280
  The monthly amounts to be entered for social security are the Primary Insurance Amounts (PIA)
281
- as reported on annual statements issued by the Social Security Administration (SSA).
282
- The PIA monthly amounts are always reported in today's \\$ by the SSA.
283
- Before the full retirement age (FRA),
284
- the current value of the PIA is close to the amount reported for disability benefits.
285
- The SSA also provides a future estimate of the PIA at FRA by projecting current
286
- earnings until reaching FRA. For individuals planning to continue to work, use the projection at FRA
287
- or use a future benefit calculator such
288
- as the one provided by [SSA](https://www.ssa.gov/OACT/quickcalc/).
289
- For those part of the FIRE crowd and planning to retire early,
290
- the number reported for disability benefits should be used instead.
281
+ which are a critical part used for calculating benefits by the Social Security Administration (SSA).
282
+ The PIA monthly amounts are always in today's \\$. It is equivalent to the monthly benefit
283
+ that you would receive at full retirement age (FRA), which varies between 65 and 67 depending
284
+ on your birth year.
285
+ The SSA also provides a future estimate of benefits at FRA by projecting current
286
+ earnings until reaching FRA. You can use this number if you are comfortable
287
+ with the underlying assumption that you will continue to work until FRA, at a salary
288
+ similar to last year's.
289
+ A way to get a more robust PIA estimate
290
+ is to use an online calculator such as [ssa.tools](https://ssa.tools/calculator).
291
+ To use this tool, you will need to get your full earning records from your personal account
292
+ on the SSA website. Copy the table listing your records from the SSA web page and paste
293
+ into the tool. Follow instructions carefully and do not cut from the PDF version
294
+ as it can contain aggregated years.
295
+ Please see
296
+ [this page](https://ssa.tools/guides/earnings-record-paste) for common input errors.
297
+ After making sure that all entries are valid, paste in the tool.
298
+ Enter your birth year and month and the number of years you are planning
299
+ to continue to work, if any.
291
300
 
292
301
  Owl considers the exact FRA associated with the individual's birth year and adjusts the PIA
293
302
  according to the age (year and month) when benefits are claimed. Total amount received
@@ -52,6 +52,7 @@ else:
52
52
  msg1 = "This is the **monthly** amount at Full Retirement Age (FRA)."
53
53
  msg2 = "Starting age of benefits in years and months."
54
54
  getIntInput(0, "ssAmt", "**monthly** PIA amount (in today's \\$)", helpmsg=msg1)
55
+ st.markdown(f"Use this [tool](https://ssa.tools/calculator) to get {kz.getCaseKey('iname0')}'s PIA.")
55
56
  incol1, incol2 = st.columns(2, gap="large", vertical_alignment="top")
56
57
  with incol1:
57
58
  ret = getIntInput(0, "ssAge_y", "claiming at age...", 67, msg2, max_val=70)
@@ -62,12 +63,13 @@ else:
62
63
  with col2:
63
64
  if kz.getCaseKey("status") == "married":
64
65
  getIntInput(1, "ssAmt", "**monthly** PIA amount (in today's \\$)", helpmsg=msg1)
66
+ st.markdown(f"Use this [tool](https://ssa.tools/calculator) to get {kz.getCaseKey('iname1')}'s PIA.")
65
67
  incol1, incol2 = st.columns(2, gap="large", vertical_alignment="top")
66
68
  with incol1:
67
69
  ret = getIntInput(1, "ssAge_y", "claiming at age...", 67, msg2, max_val=70)
68
70
  with incol2:
69
71
  maxmonth = 0 if ret == 70 else 11
70
- getIntInput(-1, "ssAge_m", "...and month(s)", 0, msg2, max_val=maxmonth, prompt=False)
72
+ getIntInput(1, "ssAge_m", "...and month(s)", 0, msg2, max_val=maxmonth, prompt=False)
71
73
 
72
74
  st.divider()
73
75
  st.write("#### :orange[Pension]")
@@ -7,20 +7,22 @@ with col3:
7
7
  st.image("http://github.com/mdlacasse/Owl/blob/main/docs/images/owl.png?raw=true")
8
8
  st.caption("Retirement planner with great wisdom")
9
9
  with col1:
10
- st.write("# :orange[Welcome to Owl - Optimal Wealth Lab]\nA retirement financial exploration tool based on linear programming")
10
+ st.write("""# :orange[Welcome to Owl - Optimal Wealth Lab]
11
+ \nA retirement financial exploration tool based on linear programming""")
11
12
  kz.divider("orange")
12
13
  st.write("### :material/campaign: News")
13
14
  st.markdown("""
14
15
  This version :mega: includes:
15
16
  - A much improved social security calculator.
16
- New approach is to use the monthly Primary Insurance Amount as reported in statements from
17
- the Social Security Administration.
18
- - Month granularity for birthday and fixed income claiming age. This approach enables
19
- more precise calculations for determining benefits and for representing potentially partial
20
- years when social security starts.
17
+ New approach uses the monthly Primary Insurance Amount as reported in
18
+ personal statements issued by the Social Security Administration.
19
+ - Month granularity for birth and fixed-income claiming age. This addition enables
20
+ more accurate calculations for determining benefits and for capturing partial
21
+ year of benefits when social security starts.
21
22
 
22
- Older case files can be read, but please verify your birthday month
23
- and the `Fixed Income` page to ensure your information is correctly entered.
23
+ Older case files can be read, but please verify your birth month
24
+ and the `Fixed Income` page to ensure your information is correctly entered
25
+ (Hint: Use `duplicate` for updating birth month).
24
26
 
25
27
  As always, please report bugs :bug: and suggestions through the
26
28
  GitHub [channel](http://github.com/mdlacasse/Owl/issues)
@@ -33,8 +35,8 @@ Take the time to give a :star: on GitHub if you use Owl. That's all you have to
33
35
  st.write("### :material/rocket_launch: Quick Start")
34
36
  st.markdown("""
35
37
  To respect your privacy, Owl does not store any information related to a case:
36
- all is lost after a session is closed. For this reason,
37
- two files can be used to store the specifications of a case so that it can be reproduced
38
+ all informattion is lost after a session is closed. For this reason,
39
+ two ancillary files can be used to store the specifications of a case so that it can be reproduced
38
40
  at a later time:
39
41
  - A *case* parameter file
40
42
  specifying account balances, asset allocation, social security and pension, rates,
@@ -1 +0,0 @@
1
- __version__ = "2025.12.03"
File without changes
File without changes
File without changes
File without changes