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.
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/PKG-INFO +1 -1
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/RELEASE_NOTES.md +4 -0
- owlplanner-2025.12.5/docker/buildPackage.cmd +7 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/case_jon+jane.toml +1 -1
- owlplanner-2025.12.5/examples/ttt.toml +58 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/plan.py +9 -8
- owlplanner-2025.12.5/src/owlplanner/version.py +1 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/tests/test_repro.py +11 -11
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/About_Owl.py +1 -1
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Documentation.py +20 -11
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Fixed_Income.py +3 -1
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Quick_Start.py +12 -10
- owlplanner-2025.12.3/src/owlplanner/version.py +0 -1
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/.devcontainer/devcontainer.json +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/.flake8 +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/.gitattributes +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/.github/workflows/github-actions-runtests.yml +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/.gitignore +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/.streamlit/config.toml +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/.streamlit/fullconfig.toml +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/INSTALL.md +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/LICENSE +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/README.md +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/USER_GUIDE.md +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docker/Dockerfile.bare +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docker/Dockerfile.static +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docker/README.md +0 -0
- /owlplanner-2025.12.3/docker/build.cmd → /owlplanner-2025.12.5/docker/buildContainers.cmd +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docker/buildentrypoint.sh +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docker/docker-compose.yml +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docker/runentrypoint.sh +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/AD-taxDef.png +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/AD-taxFree.png +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/AD-taxable.png +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/Hist_Bequest.png +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/Hist_Spending.png +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/MC-tutorial2a.png +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/MC-tutorial2b.png +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/OwlUI.png +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/allocations.png +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/owl.png +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/piecewiseConstant.png +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/profile.png +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/ratesCorrelations.png +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/ratesPlot.png +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/savingsPlot.png +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/sourcesPlot.png +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/spendingPlot.png +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/taxIncomePlot.png +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/images/taxesPlot.png +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/owl.pdf +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/docs/owl.tex +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/case_drawdowncalc-comparison-1.toml +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/case_jack+jill.toml +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/case_joe.toml +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/case_john+sally.toml +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/case_kim+sam-bequest.toml +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/case_kim+sam-spending.toml +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/jack+jill.xlsx +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/joe.xlsx +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/john+sally.xlsx +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/jon+jane.xlsx +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/kim+sam.xlsx +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/examples/template.xlsx +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/notebooks/john+sally.ipynb +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/notebooks/kim+sam.ipynb +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/notebooks/template.ipynb +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/notebooks/tutorial_1.ipynb +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/notebooks/tutorial_2.ipynb +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/notebooks/tutorial_3.ipynb +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/owlplanner.cmd +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/owlplanner.sh +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/pyproject.toml +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/pytest.ini +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/requirements.txt +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/__init__.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/abcapi.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/config.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/data/__init__.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/data/rates.csv +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/mylogging.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/plotting/__init__.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/plotting/base.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/plotting/factory.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/plotting/matplotlib_backend.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/plotting/plotly_backend.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/progress.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/rates.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/socialsecurity.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/tax2025.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/tax2026.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/timelists.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/src/owlplanner/utils.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/tests/test_logger.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/tests/test_regressions.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/tests/test_socsec.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/tests/test_toml_cases.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/tests/test_ui_asset_allocation.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/tests/test_ui_compare_summaries.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/tests/test_ui_sskeys.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/tests/test_units.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/AI +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Asset_Allocation.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Create_Case.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Current_Assets.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Graphs.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Historical_Range.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Logs.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Monte_Carlo.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Optimization_Parameters.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Output_Files.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/README.md +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Rates_Selection.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Settings.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Wages_and_Contributions.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/Worksheets.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/__init__.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/main.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/owlbridge.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/progress.py +0 -0
- {owlplanner-2025.12.3 → owlplanner-2025.12.5}/ui/sskeys.py +0 -0
- {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
|
+
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
|
|
@@ -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,
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
1180
|
-
#
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
282
|
-
The PIA monthly amounts are always
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
The SSA also provides a future estimate of
|
|
286
|
-
earnings until reaching FRA.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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(
|
|
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]
|
|
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
|
|
17
|
-
the Social Security Administration.
|
|
18
|
-
- Month granularity for
|
|
19
|
-
more
|
|
20
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|