owlplanner 2025.2.3__tar.gz → 2025.2.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.2.3 → owlplanner-2025.2.5}/PKG-INFO +25 -40
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/README.md +24 -39
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/pyproject.toml +1 -1
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/src/owlplanner/plan.py +102 -113
- owlplanner-2025.2.5/src/owlplanner/version.py +1 -0
- owlplanner-2025.2.5/ui/Case_Summary.py +46 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/Documentation.py +16 -13
- owlplanner-2025.2.5/ui/Graphs.py +32 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/Quick_Start.py +1 -1
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/Rates_Selection.py +4 -4
- owlplanner-2025.2.3/ui/Case_Worksheets.py → owlplanner-2025.2.5/ui/Worksheets.py +4 -1
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/main.py +2 -2
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/owlbridge.py +18 -16
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/requirements.txt +1 -1
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/sskeys.py +4 -0
- owlplanner-2025.2.3/src/owlplanner/version.py +0 -1
- owlplanner-2025.2.3/ui/Case_Results.py +0 -51
- owlplanner-2025.2.3/ui/Case_Summary.py +0 -21
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/.devcontainer/devcontainer.json +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/.flake8 +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/.github/workflows/github-actions-runtests.yml +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/.gitignore +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/INSTALL.md +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/LICENSE +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/images/AD-taxDef.png +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/images/AD-taxFree.png +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/images/AD-taxable.png +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/images/Hist_Bequest.png +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/images/Hist_Spending.png +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/images/MC-tutorial2a.png +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/images/MC-tutorial2b.png +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/images/OwlUI.png +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/images/allocations.png +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/images/owl.png +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/images/profile.png +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/images/ratesCorrelations.png +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/images/ratesPlot.png +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/images/savingsPlot.png +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/images/sourcesPlot.png +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/images/spendingPlot.png +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/images/taxIncomePlot.png +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/images/taxesPlot.png +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/owl.pdf +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/docs/owl.tex +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/examples/case_jack+jill.toml +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/examples/case_joe.toml +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/examples/case_john+sally.toml +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/examples/case_kim+sam-bequest.toml +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/examples/case_kim+sam-spending.toml +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/examples/jack+jill.xlsx +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/examples/joe.xlsx +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/examples/john+sally.xlsx +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/examples/template.xlsx +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/notebooks/john+sally.ipynb +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/notebooks/kim+sam.ipynb +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/notebooks/template.ipynb +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/notebooks/tutorial_1.ipynb +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/notebooks/tutorial_2.ipynb +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/notebooks/tutorial_3.ipynb +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/owlplanner.cmd +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/requirements.txt +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/src/owlplanner/__init__.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/src/owlplanner/abcapi.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/src/owlplanner/config.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/src/owlplanner/data/__init__.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/src/owlplanner/data/rates.csv +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/src/owlplanner/logging.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/src/owlplanner/progress.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/src/owlplanner/rates.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/src/owlplanner/tax2025.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/src/owlplanner/timelists.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/src/owlplanner/utils.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/tests/test_logger.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/tests/test_regressions.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/tests/test_repro.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/tests/test_toml_cases.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/tests/test_units.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/About_Owl.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/Asset_Allocation.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/Assets.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/Basic_Info.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/Fixed_Income.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/Historical_Range.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/Logs.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/Monte_Carlo.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/Optimization_Parameters.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/README.md +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/Settings.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/Wages_And_Contributions.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/plots.py +0 -0
- {owlplanner-2025.2.3 → owlplanner-2025.2.5}/ui/progress.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: owlplanner
|
|
3
|
-
Version: 2025.2.
|
|
3
|
+
Version: 2025.2.5
|
|
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
|
|
@@ -731,7 +731,8 @@ This is exactly where this tool fits it. Given your savings capabilities and spe
|
|
|
731
731
|
it can generate different future realizations of
|
|
732
732
|
your strategy under different market assumptions, helping to better understand your financial situation.
|
|
733
733
|
|
|
734
|
-
Disclaimers: I am not a financial planner. You make your own decisions.
|
|
734
|
+
Disclaimers: I am not a financial planner. You make your own decisions.
|
|
735
|
+
This program comes with no guarantee. Use at your own risk.
|
|
735
736
|
|
|
736
737
|
More disclaimers: While some output of the code has been verified with other approaches,
|
|
737
738
|
this code is still under development and I cannot guarantee the accuracy of the results.
|
|
@@ -975,47 +976,31 @@ The output of the last command reports that if future rates are exactly like tho
|
|
|
975
976
|
starting from 1969 and the following years, Jack and Jill could afford an annual spending of
|
|
976
977
|
\\$97k starting this year
|
|
977
978
|
(with a basis of \\$88.8k - the basis multiplies the profile which can vary over the course of the plan).
|
|
978
|
-
The summary also contains
|
|
979
|
+
The summary also contains some details:
|
|
979
980
|
```
|
|
980
981
|
SUMMARY ================================================================
|
|
982
|
+
Net yearly spending basis in 2025$: $91,812
|
|
983
|
+
Net yearly spending for year 2025: $100,448
|
|
984
|
+
Net spending remaining in year 2025: $100,448
|
|
985
|
+
Total net spending in 2025$: $2,809,453 ($7,757,092 nominal)
|
|
986
|
+
Total Roth conversions in 2025$: $320,639 ($456,454 nominal)
|
|
987
|
+
Total income tax paid on ordinary income in 2025$: $247,788 ($469,522 nominal)
|
|
988
|
+
Total tax paid on gains and dividends in 2025$: $3,313 ($3,768 nominal)
|
|
989
|
+
Total Medicare premiums paid in 2025$: $117,660 ($343,388 nominal)
|
|
990
|
+
Spousal wealth transfer from Jack to Jill in year 2051 (nominal): taxable: $0 tax-def: $57,224 tax-free: $2,102,173
|
|
991
|
+
Sum of spousal bequests to Jill in year 2051 in 2025$: $499,341 ($2,159,397 nominal)
|
|
992
|
+
Post-tax non-spousal bequests from Jack in year 2051 (nominal): taxable: $0 tax-def: $0 tax-free: $0
|
|
993
|
+
Sum of post-tax non-spousal bequests from Jack in year 2051 in 2025$: $0 ($0 nominal)
|
|
994
|
+
Post-tax account values at the end of final plan year 2057 (nominal): taxable: $0 tax-def: $0 tax-free: $2,488,808
|
|
995
|
+
Total estate value at the end of final plan year 2057 in 2025$: $500,000 ($2,488,808 nominal)
|
|
996
|
+
Plan starting date: 01-01
|
|
997
|
+
Cumulative inflation factor from start date to end of plan: 4.98
|
|
998
|
+
Jack's 27-year life horizon: 2025 -> 2051
|
|
999
|
+
Jill's 33-year life horizon: 2025 -> 2057
|
|
981
1000
|
Plan name: jack & jill - tutorial
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
Initial balances [taxable, tax-deferred, tax-free]:
|
|
986
|
-
Jack's accounts: ['$90,500', '$600,500', '$70,000']
|
|
987
|
-
Jill's accounts: ['$60,200', '$150,000', '$40,000']
|
|
988
|
-
Return rates: historical
|
|
989
|
-
Rates used: from 1969 to 2002
|
|
990
|
-
This year's starting date: 01-01
|
|
991
|
-
Optimized for: maxSpending
|
|
992
|
-
Solver options: {'maxRothConversion': 100, 'bequest': 500, 'noRothConversions': 'Jill'}
|
|
993
|
-
Number of decision variables: 1026
|
|
994
|
-
Number of constraints: 894
|
|
995
|
-
Spending profile: smile
|
|
996
|
-
Surviving spouse spending needs: 60%
|
|
997
|
-
Net yearly spending in year 2024: $97,098
|
|
998
|
-
Net spending remaining in year 2024: $97,098
|
|
999
|
-
Net yearly spending profile basis in 2024$: $88,763
|
|
1000
|
-
Assumed heirs tax rate: 30%
|
|
1001
|
-
Spousal surplus deposit fraction: 0.5
|
|
1002
|
-
Spousal beneficiary fractions to Jill: [1, 1, 1]
|
|
1003
|
-
Spousal wealth transfer from Jack to Jill in year 2051 (nominal):
|
|
1004
|
-
taxable: $0 tax-def: $63,134 tax-free: $2,583,303
|
|
1005
|
-
Sum of spousal bequests to Jill in year 2051 in 2024$: $592,103 ($2,646,437 nominal)
|
|
1006
|
-
Post-tax non-spousal bequests from Jack in year 2051 (nominal):
|
|
1007
|
-
taxable: $0 tax-def: $0 tax-free: $0
|
|
1008
|
-
Sum of post-tax non-spousal bequests from Jack in year 2051 in 2024$: $0 ($0 nominal)
|
|
1009
|
-
Total net spending in 2024$: $2,804,910 ($7,916,623 nominal)
|
|
1010
|
-
Total Roth conversions in 2024$: $311,760 ($443,005 nominal)
|
|
1011
|
-
Total ordinary income tax paid in 2024$: $236,710 ($457,922 nominal)
|
|
1012
|
-
Total dividend tax paid in 2024$: $3,437 ($3,902 nominal)
|
|
1013
|
-
Total Medicare premiums paid in 2024$: $117,817 ($346,404 nominal)
|
|
1014
|
-
Post-tax account values at the end of final plan year 2057: (nominal)
|
|
1015
|
-
taxable: $0 tax-def: $0 tax-free: $2,553,871
|
|
1016
|
-
Total estate value at the end of final plan year 2057 in 2024$: $500,000 ($2,553,871 nominal)
|
|
1017
|
-
Inflation factor from this year's start date to the end of plan final year: 5.11
|
|
1018
|
-
Case executed on: 2024-12-09 at 22:11:57
|
|
1001
|
+
Number of decision variables: 996
|
|
1002
|
+
Number of constraints: 867
|
|
1003
|
+
Case executed on: 2025-02-04 at 22:55:03
|
|
1019
1004
|
------------------------------------------------------------------------
|
|
1020
1005
|
```
|
|
1021
1006
|
And an Excel workbook can be saved with all the detailed amounts over the years by using the following command:
|
|
@@ -28,7 +28,8 @@ This is exactly where this tool fits it. Given your savings capabilities and spe
|
|
|
28
28
|
it can generate different future realizations of
|
|
29
29
|
your strategy under different market assumptions, helping to better understand your financial situation.
|
|
30
30
|
|
|
31
|
-
Disclaimers: I am not a financial planner. You make your own decisions.
|
|
31
|
+
Disclaimers: I am not a financial planner. You make your own decisions.
|
|
32
|
+
This program comes with no guarantee. Use at your own risk.
|
|
32
33
|
|
|
33
34
|
More disclaimers: While some output of the code has been verified with other approaches,
|
|
34
35
|
this code is still under development and I cannot guarantee the accuracy of the results.
|
|
@@ -272,47 +273,31 @@ The output of the last command reports that if future rates are exactly like tho
|
|
|
272
273
|
starting from 1969 and the following years, Jack and Jill could afford an annual spending of
|
|
273
274
|
\\$97k starting this year
|
|
274
275
|
(with a basis of \\$88.8k - the basis multiplies the profile which can vary over the course of the plan).
|
|
275
|
-
The summary also contains
|
|
276
|
+
The summary also contains some details:
|
|
276
277
|
```
|
|
277
278
|
SUMMARY ================================================================
|
|
279
|
+
Net yearly spending basis in 2025$: $91,812
|
|
280
|
+
Net yearly spending for year 2025: $100,448
|
|
281
|
+
Net spending remaining in year 2025: $100,448
|
|
282
|
+
Total net spending in 2025$: $2,809,453 ($7,757,092 nominal)
|
|
283
|
+
Total Roth conversions in 2025$: $320,639 ($456,454 nominal)
|
|
284
|
+
Total income tax paid on ordinary income in 2025$: $247,788 ($469,522 nominal)
|
|
285
|
+
Total tax paid on gains and dividends in 2025$: $3,313 ($3,768 nominal)
|
|
286
|
+
Total Medicare premiums paid in 2025$: $117,660 ($343,388 nominal)
|
|
287
|
+
Spousal wealth transfer from Jack to Jill in year 2051 (nominal): taxable: $0 tax-def: $57,224 tax-free: $2,102,173
|
|
288
|
+
Sum of spousal bequests to Jill in year 2051 in 2025$: $499,341 ($2,159,397 nominal)
|
|
289
|
+
Post-tax non-spousal bequests from Jack in year 2051 (nominal): taxable: $0 tax-def: $0 tax-free: $0
|
|
290
|
+
Sum of post-tax non-spousal bequests from Jack in year 2051 in 2025$: $0 ($0 nominal)
|
|
291
|
+
Post-tax account values at the end of final plan year 2057 (nominal): taxable: $0 tax-def: $0 tax-free: $2,488,808
|
|
292
|
+
Total estate value at the end of final plan year 2057 in 2025$: $500,000 ($2,488,808 nominal)
|
|
293
|
+
Plan starting date: 01-01
|
|
294
|
+
Cumulative inflation factor from start date to end of plan: 4.98
|
|
295
|
+
Jack's 27-year life horizon: 2025 -> 2051
|
|
296
|
+
Jill's 33-year life horizon: 2025 -> 2057
|
|
278
297
|
Plan name: jack & jill - tutorial
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
Initial balances [taxable, tax-deferred, tax-free]:
|
|
283
|
-
Jack's accounts: ['$90,500', '$600,500', '$70,000']
|
|
284
|
-
Jill's accounts: ['$60,200', '$150,000', '$40,000']
|
|
285
|
-
Return rates: historical
|
|
286
|
-
Rates used: from 1969 to 2002
|
|
287
|
-
This year's starting date: 01-01
|
|
288
|
-
Optimized for: maxSpending
|
|
289
|
-
Solver options: {'maxRothConversion': 100, 'bequest': 500, 'noRothConversions': 'Jill'}
|
|
290
|
-
Number of decision variables: 1026
|
|
291
|
-
Number of constraints: 894
|
|
292
|
-
Spending profile: smile
|
|
293
|
-
Surviving spouse spending needs: 60%
|
|
294
|
-
Net yearly spending in year 2024: $97,098
|
|
295
|
-
Net spending remaining in year 2024: $97,098
|
|
296
|
-
Net yearly spending profile basis in 2024$: $88,763
|
|
297
|
-
Assumed heirs tax rate: 30%
|
|
298
|
-
Spousal surplus deposit fraction: 0.5
|
|
299
|
-
Spousal beneficiary fractions to Jill: [1, 1, 1]
|
|
300
|
-
Spousal wealth transfer from Jack to Jill in year 2051 (nominal):
|
|
301
|
-
taxable: $0 tax-def: $63,134 tax-free: $2,583,303
|
|
302
|
-
Sum of spousal bequests to Jill in year 2051 in 2024$: $592,103 ($2,646,437 nominal)
|
|
303
|
-
Post-tax non-spousal bequests from Jack in year 2051 (nominal):
|
|
304
|
-
taxable: $0 tax-def: $0 tax-free: $0
|
|
305
|
-
Sum of post-tax non-spousal bequests from Jack in year 2051 in 2024$: $0 ($0 nominal)
|
|
306
|
-
Total net spending in 2024$: $2,804,910 ($7,916,623 nominal)
|
|
307
|
-
Total Roth conversions in 2024$: $311,760 ($443,005 nominal)
|
|
308
|
-
Total ordinary income tax paid in 2024$: $236,710 ($457,922 nominal)
|
|
309
|
-
Total dividend tax paid in 2024$: $3,437 ($3,902 nominal)
|
|
310
|
-
Total Medicare premiums paid in 2024$: $117,817 ($346,404 nominal)
|
|
311
|
-
Post-tax account values at the end of final plan year 2057: (nominal)
|
|
312
|
-
taxable: $0 tax-def: $0 tax-free: $2,553,871
|
|
313
|
-
Total estate value at the end of final plan year 2057 in 2024$: $500,000 ($2,553,871 nominal)
|
|
314
|
-
Inflation factor from this year's start date to the end of plan final year: 5.11
|
|
315
|
-
Case executed on: 2024-12-09 at 22:11:57
|
|
298
|
+
Number of decision variables: 996
|
|
299
|
+
Number of constraints: 867
|
|
300
|
+
Case executed on: 2025-02-04 at 22:55:03
|
|
316
301
|
------------------------------------------------------------------------
|
|
317
302
|
```
|
|
318
303
|
And an Excel workbook can be saved with all the detailed amounts over the years by using the following command:
|
|
@@ -898,12 +898,9 @@ class Plan(object):
|
|
|
898
898
|
self.myRothX_in[i, :h] = self.timeLists[iname]['Roth conv'].iloc[:h]
|
|
899
899
|
self.Lambda_in[i, :h] = self.timeLists[iname]['big-ticket items'].iloc[:h]
|
|
900
900
|
|
|
901
|
-
# In 1st year, reduce wages and
|
|
901
|
+
# In 1st year, reduce wages and contributions depending on starting date.
|
|
902
902
|
self.omega_in[:, 0] *= self.yearFracLeft
|
|
903
903
|
self.kappa_ijn[:, :, 0] *= self.yearFracLeft
|
|
904
|
-
if self.yearFracLeft != 1:
|
|
905
|
-
self.Lambda_in[:, 0] = 0
|
|
906
|
-
self.myRothX_in[:, 0] = 0
|
|
907
904
|
|
|
908
905
|
self.caseStatus = 'modified'
|
|
909
906
|
|
|
@@ -1416,11 +1413,11 @@ class Plan(object):
|
|
|
1416
1413
|
|
|
1417
1414
|
progcall.finish()
|
|
1418
1415
|
self.mylog.resetVerbose()
|
|
1419
|
-
fig,
|
|
1420
|
-
self.mylog.print(
|
|
1416
|
+
fig, description = self._showResults(objective, df, N, figure)
|
|
1417
|
+
self.mylog.print(description.getvalue())
|
|
1421
1418
|
|
|
1422
1419
|
if figure:
|
|
1423
|
-
return fig,
|
|
1420
|
+
return fig, description.getvalue()
|
|
1424
1421
|
|
|
1425
1422
|
return N, df
|
|
1426
1423
|
|
|
@@ -1472,11 +1469,11 @@ class Plan(object):
|
|
|
1472
1469
|
|
|
1473
1470
|
progcall.finish()
|
|
1474
1471
|
self.mylog.resetVerbose()
|
|
1475
|
-
fig,
|
|
1476
|
-
self.mylog.print(
|
|
1472
|
+
fig, description = self._showResults(objective, df, N, figure)
|
|
1473
|
+
self.mylog.print(description.getvalue())
|
|
1477
1474
|
|
|
1478
1475
|
if figure:
|
|
1479
|
-
return fig,
|
|
1476
|
+
return fig, description.getvalue()
|
|
1480
1477
|
|
|
1481
1478
|
return N, df
|
|
1482
1479
|
|
|
@@ -1486,9 +1483,9 @@ class Plan(object):
|
|
|
1486
1483
|
"""
|
|
1487
1484
|
import seaborn as sbn
|
|
1488
1485
|
|
|
1489
|
-
|
|
1486
|
+
description = io.StringIO()
|
|
1490
1487
|
|
|
1491
|
-
print('Success rate: %s on %d samples.' % (u.pc(len(df) / N), N), file=
|
|
1488
|
+
print('Success rate: %s on %d samples.' % (u.pc(len(df) / N), N), file=description)
|
|
1492
1489
|
title = '$N$ = %d, $P$ = %s' % (N, u.pc(len(df) / N))
|
|
1493
1490
|
means = df.mean(axis=0, numeric_only=True)
|
|
1494
1491
|
medians = df.median(axis=0, numeric_only=True)
|
|
@@ -1501,7 +1498,7 @@ class Plan(object):
|
|
|
1501
1498
|
# or if solution led to empty accounts at the end of first spouse's life.
|
|
1502
1499
|
if np.all(self.phi_j == 1) or medians.iloc[0] < 1:
|
|
1503
1500
|
if medians.iloc[0] < 1:
|
|
1504
|
-
print('Optimized solutions all have null partial bequest in year %d.' % my[0], file=
|
|
1501
|
+
print('Optimized solutions all have null partial bequest in year %d.' % my[0], file=description)
|
|
1505
1502
|
df.drop('partial', axis=1, inplace=True)
|
|
1506
1503
|
means = df.mean(axis=0, numeric_only=True)
|
|
1507
1504
|
medians = df.median(axis=0, numeric_only=True)
|
|
@@ -1553,16 +1550,16 @@ class Plan(object):
|
|
|
1553
1550
|
# plt.show()
|
|
1554
1551
|
|
|
1555
1552
|
for q in range(len(means)):
|
|
1556
|
-
print('%12s: Median (%d $): %s' % (leads[q], self.year_n[0], u.d(medians.iloc[q])), file=
|
|
1557
|
-
print('%12s: Mean (%d $): %s' % (leads[q], self.year_n[0], u.d(means.iloc[q])), file=
|
|
1553
|
+
print('%12s: Median (%d $): %s' % (leads[q], self.year_n[0], u.d(medians.iloc[q])), file=description)
|
|
1554
|
+
print('%12s: Mean (%d $): %s' % (leads[q], self.year_n[0], u.d(means.iloc[q])), file=description)
|
|
1558
1555
|
print(
|
|
1559
1556
|
'%12s: Range: %s - %s'
|
|
1560
1557
|
% (leads[q], u.d(1000 * df.iloc[:, q].min()), u.d(1000 * df.iloc[:, q].max())),
|
|
1561
|
-
file=
|
|
1558
|
+
file=description)
|
|
1562
1559
|
nzeros = len(df.iloc[:, q][df.iloc[:, q] < 0.001])
|
|
1563
|
-
print('%12s: N zero solns: %d' % (leads[q], nzeros), file=
|
|
1560
|
+
print('%12s: N zero solns: %d' % (leads[q], nzeros), file=description)
|
|
1564
1561
|
|
|
1565
|
-
return fig,
|
|
1562
|
+
return fig, description
|
|
1566
1563
|
|
|
1567
1564
|
def resolve(self):
|
|
1568
1565
|
"""
|
|
@@ -2041,141 +2038,133 @@ class Plan(object):
|
|
|
2041
2038
|
@_checkCaseStatus
|
|
2042
2039
|
def summary(self):
|
|
2043
2040
|
"""
|
|
2044
|
-
Print summary
|
|
2041
|
+
Print summary in logs.
|
|
2045
2042
|
"""
|
|
2046
2043
|
self.mylog.print('SUMMARY ================================================================')
|
|
2047
|
-
|
|
2048
|
-
for
|
|
2049
|
-
self.mylog.print(
|
|
2044
|
+
dic = self.summaryDic()
|
|
2045
|
+
for key, value in dic.items():
|
|
2046
|
+
self.mylog.print(f"{key}: {value}")
|
|
2050
2047
|
self.mylog.print('------------------------------------------------------------------------')
|
|
2051
2048
|
|
|
2052
2049
|
return None
|
|
2053
2050
|
|
|
2051
|
+
def summaryList(self):
|
|
2052
|
+
"""
|
|
2053
|
+
Return summary as a list.
|
|
2054
|
+
"""
|
|
2055
|
+
mylist = []
|
|
2056
|
+
dic = self.summaryDic()
|
|
2057
|
+
for key, value in dic.items():
|
|
2058
|
+
mylist.append(f"{key}: {value}")
|
|
2059
|
+
|
|
2060
|
+
return mylist
|
|
2061
|
+
|
|
2054
2062
|
def summaryString(self):
|
|
2055
2063
|
"""
|
|
2056
|
-
|
|
2064
|
+
Return summary as a string.
|
|
2057
2065
|
"""
|
|
2058
|
-
string = ''
|
|
2059
|
-
|
|
2060
|
-
for
|
|
2061
|
-
string +=
|
|
2066
|
+
string = 'Synopsis\n'
|
|
2067
|
+
dic = self.summaryDic()
|
|
2068
|
+
for key, value in dic.items():
|
|
2069
|
+
string += f"{key:>70}: {value}\n"
|
|
2070
|
+
# string += "%60s: %s\n" % (key, value)
|
|
2062
2071
|
|
|
2063
2072
|
return string
|
|
2064
2073
|
|
|
2065
|
-
def
|
|
2074
|
+
def summaryDic(self):
|
|
2066
2075
|
"""
|
|
2067
|
-
Return
|
|
2076
|
+
Return dictionary containing summary of values.
|
|
2068
2077
|
"""
|
|
2069
2078
|
now = self.year_n[0]
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
lines.append('Contributions file: %s' % self.timeListsFileName)
|
|
2076
|
-
lines.append('Initial balances [taxable, tax-deferred, tax-free]:')
|
|
2077
|
-
for i in range(self.N_i):
|
|
2078
|
-
lines.append("%12s's accounts: %s" % (self.inames[i], [u.d(self.beta_ij[i][j]) for j in range(self.N_j)]))
|
|
2079
|
-
|
|
2080
|
-
lines.append('Return rates: %s' % self.rateMethod)
|
|
2081
|
-
if self.rateMethod in ['historical', 'historical average', 'histochastic']:
|
|
2082
|
-
lines.append('Rates used: from %d to %d' % (self.rateFrm, self.rateTo))
|
|
2083
|
-
elif self.rateMethod == 'stochastic':
|
|
2084
|
-
lines.append(
|
|
2085
|
-
'Mean rates used (%%): %s' % (['{:.1f}'.format(100 * self.rateValues[k]) for k in range(self.N_k)])
|
|
2086
|
-
)
|
|
2087
|
-
lines.append(
|
|
2088
|
-
'Standard deviation used (%%): %s'
|
|
2089
|
-
% (['{:.1f}'.format(100 * self.rateStdev[k]) for k in range(self.N_k)])
|
|
2090
|
-
)
|
|
2091
|
-
lines.append('Correlation matrix used:')
|
|
2092
|
-
lines.append('\t\t' + str(self.rateCorr).replace('\n', '\n\t\t'))
|
|
2093
|
-
else:
|
|
2094
|
-
lines.append('Rates used (%%): %s' % (['{:.1f}'.format(100 * self.rateValues[k]) for k in range(self.N_k)]))
|
|
2095
|
-
lines.append("This year's starting date: %s" % self.startDate)
|
|
2096
|
-
lines.append('Optimized for: %s' % self.objective)
|
|
2097
|
-
lines.append('Solver options: %s' % self.solverOptions)
|
|
2098
|
-
lines.append('Number of decision variables: %d' % self.A.nvars)
|
|
2099
|
-
lines.append('Number of constraints: %d' % self.A.ncons)
|
|
2100
|
-
lines.append('Spending profile: %s' % self.spendingProfile)
|
|
2101
|
-
if self.spendingProfile == 'smile':
|
|
2102
|
-
lines.append('\twith increase: %d%%, dip: %d%%, delay: %dy'
|
|
2103
|
-
% (self.smileIncrease, self.smileDip, self.smileDelay))
|
|
2104
|
-
if self.N_i == 2:
|
|
2105
|
-
lines.append('Surviving spouse spending needs: %s' % u.pc(self.chi, f=0))
|
|
2106
|
-
|
|
2107
|
-
lines.append('Net yearly spending in year %d: %s' % (now, u.d(self.g_n[0] / self.yearFracLeft)))
|
|
2108
|
-
lines.append('Net spending remaining in year %d: %s' % (now, u.d(self.g_n[0])))
|
|
2109
|
-
lines.append('Net yearly spending profile basis in %d$: %s' % (now, u.d(self.g_n[0] / self.xi_n[0])))
|
|
2110
|
-
|
|
2111
|
-
lines.append('Assumed heirs marginal tax rate: %s' % u.pc(self.nu, f=0))
|
|
2112
|
-
|
|
2113
|
-
if self.N_i == 2 and self.n_d < self.N_n:
|
|
2114
|
-
lines.append("Spousal surplus deposit fraction in %s's taxable account: %s"
|
|
2115
|
-
% (self.inames[1], self.eta))
|
|
2116
|
-
lines.append('Spousal beneficiary fractions to %s: %s' % (self.inames[self.i_s], self.phi_j.tolist()))
|
|
2117
|
-
p_j = self.partialEstate_j * (1 - self.phi_j)
|
|
2118
|
-
p_j[1] *= 1 - self.nu
|
|
2119
|
-
nx = self.n_d - 1
|
|
2120
|
-
totOthers = np.sum(p_j)
|
|
2121
|
-
totOthersNow = totOthers / self.gamma_n[nx + 1]
|
|
2122
|
-
q_j = self.partialEstate_j * self.phi_j
|
|
2123
|
-
totSpousal = np.sum(q_j)
|
|
2124
|
-
totSpousalNow = totSpousal / self.gamma_n[nx + 1]
|
|
2125
|
-
lines.append('Spousal wealth transfer from %s to %s in year %d (nominal):'
|
|
2126
|
-
% (self.inames[self.i_d], self.inames[self.i_s], self.year_n[nx]))
|
|
2127
|
-
lines.append(' taxable: %s tax-def: %s tax-free: %s' % (u.d(q_j[0]), u.d(q_j[1]), u.d(q_j[2])))
|
|
2128
|
-
lines.append('Sum of spousal bequests to %s in year %d in %d$: %s (%s nominal)'
|
|
2129
|
-
% (self.inames[self.i_s], self.year_n[nx], now, u.d(totSpousalNow), u.d(totSpousal)))
|
|
2130
|
-
lines.append(
|
|
2131
|
-
'Post-tax non-spousal bequests from %s in year %d (nominal):' % (self.inames[self.i_d], self.year_n[nx])
|
|
2132
|
-
)
|
|
2133
|
-
lines.append(' taxable: %s tax-def: %s tax-free: %s' % (u.d(p_j[0]), u.d(p_j[1]), u.d(p_j[2])))
|
|
2134
|
-
lines.append(
|
|
2135
|
-
'Sum of post-tax non-spousal bequests from %s in year %d in %d$: %s (%s nominal)'
|
|
2136
|
-
% (self.inames[self.i_d], self.year_n[nx], now, u.d(totOthersNow), u.d(totOthers))
|
|
2137
|
-
)
|
|
2079
|
+
dic = {}
|
|
2080
|
+
# Results
|
|
2081
|
+
dic[f"Net yearly spending basis in {now}$"] = (u.d(self.g_n[0] / self.xi_n[0]))
|
|
2082
|
+
dic[f"Net yearly spending for year {now}"] = (u.d(self.g_n[0] / self.yearFracLeft))
|
|
2083
|
+
dic[f"Net spending remaining in year {now}"] = u.d(self.g_n[0])
|
|
2138
2084
|
|
|
2139
2085
|
totIncome = np.sum(self.g_n, axis=0)
|
|
2140
2086
|
totIncomeNow = np.sum(self.g_n / self.gamma_n[:-1], axis=0)
|
|
2141
|
-
|
|
2087
|
+
dic[f"Total net spending in {now}$"] = (
|
|
2088
|
+
"%s (%s nominal)" % (u.d(totIncomeNow), u.d(totIncome))
|
|
2089
|
+
)
|
|
2142
2090
|
|
|
2143
2091
|
totRoth = np.sum(self.x_in, axis=(0, 1))
|
|
2144
2092
|
totRothNow = np.sum(np.sum(self.x_in, axis=0) / self.gamma_n[:-1], axis=0)
|
|
2145
|
-
|
|
2093
|
+
dic[f"Total Roth conversions in {now}$"] = (
|
|
2094
|
+
"%s (%s nominal)" % (u.d(totRothNow), u.d(totRoth))
|
|
2095
|
+
)
|
|
2146
2096
|
|
|
2147
2097
|
taxPaid = np.sum(self.T_n, axis=0)
|
|
2148
2098
|
taxPaidNow = np.sum(self.T_n / self.gamma_n[:-1], axis=0)
|
|
2149
|
-
|
|
2150
|
-
|
|
2099
|
+
dic[f"Total income tax paid on ordinary income in {now}$"] = (
|
|
2100
|
+
"%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
|
|
2101
|
+
)
|
|
2151
2102
|
|
|
2152
2103
|
taxPaid = np.sum(self.U_n, axis=0)
|
|
2153
2104
|
taxPaidNow = np.sum(self.U_n / self.gamma_n[:-1], axis=0)
|
|
2154
|
-
|
|
2155
|
-
|
|
2105
|
+
dic[f"Total tax paid on gains and dividends in {now}$"] = (
|
|
2106
|
+
"%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
|
|
2107
|
+
)
|
|
2156
2108
|
|
|
2157
2109
|
taxPaid = np.sum(self.M_n, axis=0)
|
|
2158
2110
|
taxPaidNow = np.sum(self.M_n / self.gamma_n[:-1], axis=0)
|
|
2159
|
-
|
|
2111
|
+
dic[f"Total Medicare premiums paid in {now}$"] = (
|
|
2112
|
+
"%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
|
|
2113
|
+
)
|
|
2114
|
+
|
|
2115
|
+
if self.N_i == 2 and self.n_d < self.N_n:
|
|
2116
|
+
p_j = self.partialEstate_j * (1 - self.phi_j)
|
|
2117
|
+
p_j[1] *= 1 - self.nu
|
|
2118
|
+
nx = self.n_d - 1
|
|
2119
|
+
totOthers = np.sum(p_j)
|
|
2120
|
+
totOthersNow = totOthers / self.gamma_n[nx + 1]
|
|
2121
|
+
q_j = self.partialEstate_j * self.phi_j
|
|
2122
|
+
totSpousal = np.sum(q_j)
|
|
2123
|
+
totSpousalNow = totSpousal / self.gamma_n[nx + 1]
|
|
2124
|
+
dic["Spousal wealth transfer from %s to %s in year %d (nominal)" %
|
|
2125
|
+
(self.inames[self.i_d], self.inames[self.i_s], self.year_n[nx])] = (
|
|
2126
|
+
"taxable: %s tax-def: %s tax-free: %s" % (u.d(q_j[0]), u.d(q_j[1]), u.d(q_j[2]))
|
|
2127
|
+
)
|
|
2128
|
+
|
|
2129
|
+
dic["Sum of spousal bequests to %s in year %d in %d$" %
|
|
2130
|
+
(self.inames[self.i_s], self.year_n[nx], now)] = (
|
|
2131
|
+
"%s (%s nominal)" % (u.d(totSpousalNow), u.d(totSpousal))
|
|
2132
|
+
)
|
|
2133
|
+
dic["Post-tax non-spousal bequests from %s in year %d (nominal)" %
|
|
2134
|
+
(self.inames[self.i_d], self.year_n[nx])] = (
|
|
2135
|
+
"taxable: %s tax-def: %s tax-free: %s" % (u.d(p_j[0]), u.d(p_j[1]), u.d(p_j[2]))
|
|
2136
|
+
)
|
|
2137
|
+
dic["Sum of post-tax non-spousal bequests from %s in year %d in %d$" %
|
|
2138
|
+
(self.inames[self.i_d], self.year_n[nx], now)] = (
|
|
2139
|
+
"%s (%s nominal)" % (u.d(totOthersNow), u.d(totOthers))
|
|
2140
|
+
)
|
|
2160
2141
|
|
|
2161
2142
|
estate = np.sum(self.b_ijn[:, :, self.N_n], axis=0)
|
|
2162
2143
|
estate[1] *= 1 - self.nu
|
|
2163
|
-
|
|
2164
|
-
|
|
2144
|
+
dic["Post-tax account values at the end of final plan year %d (nominal)" % self.year_n[-1]] = (
|
|
2145
|
+
"taxable: %s tax-def: %s tax-free: %s" % (u.d(estate[0]), u.d(estate[1]), u.d(estate[2]))
|
|
2146
|
+
)
|
|
2165
2147
|
|
|
2166
2148
|
totEstate = np.sum(estate)
|
|
2167
2149
|
totEstateNow = totEstate / self.gamma_n[-1]
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
% (self.year_n[-1], now, u.d(totEstateNow), u.d(totEstate))
|
|
2150
|
+
dic["Total estate value at the end of final plan year %d in %d$" % (self.year_n[-1], now)] = (
|
|
2151
|
+
"%s (%s nominal)" % (u.d(totEstateNow), u.d(totEstate))
|
|
2171
2152
|
)
|
|
2172
|
-
|
|
2173
|
-
|
|
2153
|
+
dic["Plan starting date"] = str(self.startDate)
|
|
2154
|
+
dic["Cumulative inflation factor from start date to end of plan"] = (
|
|
2155
|
+
"%.2f" % (self.gamma_n[-1])
|
|
2174
2156
|
)
|
|
2157
|
+
for i in range(self.N_i):
|
|
2158
|
+
dic["%12s's %02d-year life horizon" % (self.inames[i], self.horizons[i])] = (
|
|
2159
|
+
"%d -> %d" % (now, now + self.horizons[i] - 1)
|
|
2160
|
+
)
|
|
2175
2161
|
|
|
2176
|
-
|
|
2162
|
+
dic["Plan name"] = self._name
|
|
2163
|
+
dic["Number of decision variables"] = str(self.A.nvars)
|
|
2164
|
+
dic["Number of constraints"] = str(self.A.ncons)
|
|
2165
|
+
dic["Case executed on"] = str(self._timestamp)
|
|
2177
2166
|
|
|
2178
|
-
return
|
|
2167
|
+
return dic
|
|
2179
2168
|
|
|
2180
2169
|
def showRatesCorrelations(self, tag='', shareRange=False, figure=False):
|
|
2181
2170
|
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2025.02.05"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import streamlit as st
|
|
2
|
+
|
|
3
|
+
import sskeys as kz
|
|
4
|
+
import owlbridge as owb
|
|
5
|
+
|
|
6
|
+
ret = kz.titleBar('summary')
|
|
7
|
+
kz.caseHeader("Case Summary")
|
|
8
|
+
|
|
9
|
+
if ret is None or kz.caseHasNoPlan():
|
|
10
|
+
st.info('Case(s) must be first created before running this page.')
|
|
11
|
+
else:
|
|
12
|
+
if kz.caseIsRunReady():
|
|
13
|
+
owb.runPlan()
|
|
14
|
+
|
|
15
|
+
if kz.caseHasNotCompletedRun():
|
|
16
|
+
st.info("Case status is currently '%s'." % kz.getKey('caseStatus'))
|
|
17
|
+
else:
|
|
18
|
+
lines = kz.getKey('summary')
|
|
19
|
+
if lines != '':
|
|
20
|
+
st.write('#### Synopsis')
|
|
21
|
+
st.code(lines, language=None)
|
|
22
|
+
st.download_button('Download synopsis',
|
|
23
|
+
data=lines,
|
|
24
|
+
file_name='Synopsis_'+kz.getKey('name')+'.txt',
|
|
25
|
+
mime='text/plain;charset=UTF-8')
|
|
26
|
+
|
|
27
|
+
lines = kz.getKey('casetoml')
|
|
28
|
+
if lines != '':
|
|
29
|
+
st.divider()
|
|
30
|
+
st.write("#### Case parameter file")
|
|
31
|
+
st.code(lines, language='toml')
|
|
32
|
+
|
|
33
|
+
st.download_button('Download case parameter file',
|
|
34
|
+
data=lines,
|
|
35
|
+
file_name='case_'+kz.getKey('name')+'.toml',
|
|
36
|
+
mime='application/toml')
|
|
37
|
+
|
|
38
|
+
st.divider()
|
|
39
|
+
st.write("#### Wages and contributions file")
|
|
40
|
+
download2 = st.download_button(
|
|
41
|
+
label="Download wages and contributions file",
|
|
42
|
+
help='Download Excel workbook.',
|
|
43
|
+
data=owb.saveContributions(),
|
|
44
|
+
file_name=kz.getKey('name')+'.xlsx',
|
|
45
|
+
disabled=kz.caseHasNotCompletedRun(),
|
|
46
|
+
mime='application/vnd.ms-excel')
|
|
@@ -72,7 +72,7 @@ An example is provided
|
|
|
72
72
|
can be found in this [directory](https://github.com/mdlacasse/Owl/blob/main/examples/).
|
|
73
73
|
Using a *case* file
|
|
74
74
|
will populate all the fields required to run a scenario. A *case* file for the case being developed
|
|
75
|
-
can be saved under the [Case
|
|
75
|
+
can be saved under the [Case Summary](#case-summary) page and made available to reload at a later time.
|
|
76
76
|
Case parameter files can have any name but when saving from the interface, their name will start with *case_*
|
|
77
77
|
followed by the case name.
|
|
78
78
|
|
|
@@ -278,8 +278,8 @@ than on January 1$^{st}$, the value of the first year will be reduced accordingl
|
|
|
278
278
|
--------------------------------------------------------------------------------------
|
|
279
279
|
### :orange[Single Scenario]
|
|
280
280
|
|
|
281
|
-
####
|
|
282
|
-
This page
|
|
281
|
+
#### Graphs
|
|
282
|
+
This page displays various plots from a single scenario based on the selections made
|
|
283
283
|
in the [Case Setup](#case-setup) section.
|
|
284
284
|
This simulation uses a single instance of a series of rates, either fixed or varying,
|
|
285
285
|
as selected in the **Case Setup** section.
|
|
@@ -292,15 +292,9 @@ in nominal value.
|
|
|
292
292
|
|
|
293
293
|
When a case has run successfully, different graphs will show the time evolution
|
|
294
294
|
of different quantities over the duration of the plan. Below
|
|
295
|
-
these graphs, two additional buttons will appear.
|
|
296
|
-
`Download case file...` button allows to save all the parameters used to generate the
|
|
297
|
-
outcome of this case to a *case* file.
|
|
298
|
-
Another button called `Download wages and contributions file...` allows
|
|
299
|
-
to save the contents of the tables on the corresponding page to an Excel workbook.
|
|
300
|
-
With both these files, the same case can be reproduced at a later time by uploading
|
|
301
|
-
them through the widgets on the `Basic Info` and `Wages and Contributions` pages.
|
|
295
|
+
these graphs, two additional buttons will appear.
|
|
302
296
|
|
|
303
|
-
####
|
|
297
|
+
#### Worksheets
|
|
304
298
|
This page shows the various worksheets containing annual transactions
|
|
305
299
|
and savings account balances in nominal \\$.
|
|
306
300
|
Each table can be downloaded separately in csv format, or all tables can be downloaded
|
|
@@ -311,10 +305,19 @@ The first line of the *Sources* worksheets are the most important
|
|
|
311
305
|
as these lines are the only ones that are actionable.
|
|
312
306
|
|
|
313
307
|
#### Case Summary
|
|
314
|
-
This page shows a
|
|
308
|
+
This page shows a synopsis of the scenario which was computed.
|
|
315
309
|
It displays informative sums of relevant income, bequest, and spending values.
|
|
316
310
|
The contents of this page can be downloaded as a plain text file by
|
|
317
|
-
clicking the button at the bottom of the
|
|
311
|
+
clicking the button at the bottom of the section.
|
|
312
|
+
|
|
313
|
+
Parameters used to generate the case are collected in *toml* format and displayed.
|
|
314
|
+
The `Download case file...` button allows to save the parameters used to generate the
|
|
315
|
+
outcome of this case to a *case* file.
|
|
316
|
+
|
|
317
|
+
Finally, another button called `Download wages and contributions file...` allows
|
|
318
|
+
to save the contents of the tables on the corresponding page to an Excel workbook.
|
|
319
|
+
With both these files, the same case can be reproduced at a later time by uploading
|
|
320
|
+
them through the widgets on the `Basic Info` and `Wages and Contributions` pages.
|
|
318
321
|
|
|
319
322
|
--------------------------------------------------------------------------------------
|
|
320
323
|
### :orange[Multiple Scenarios]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import streamlit as st
|
|
2
|
+
|
|
3
|
+
import sskeys as kz
|
|
4
|
+
import owlbridge as owb
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
ret = kz.titleBar('results')
|
|
8
|
+
kz.caseHeader("Graphs")
|
|
9
|
+
|
|
10
|
+
if ret is None or kz.caseHasNoPlan():
|
|
11
|
+
st.info('Case(s) must be first created before running this page.')
|
|
12
|
+
else:
|
|
13
|
+
if kz.caseIsRunReady():
|
|
14
|
+
owb.runPlan()
|
|
15
|
+
|
|
16
|
+
st.write("Optimize a single scenario based on the parameters selected in the **Case Setup** section.")
|
|
17
|
+
col1, col2 = st.columns(2, gap='large', vertical_alignment='bottom')
|
|
18
|
+
with col1:
|
|
19
|
+
choices = ['nominal', 'today']
|
|
20
|
+
kz.initKey('plots', choices[0])
|
|
21
|
+
ret = kz.getRadio("Dollar amounts in plots", choices, 'plots',
|
|
22
|
+
callback=owb.setDefaultPlots)
|
|
23
|
+
|
|
24
|
+
with col2:
|
|
25
|
+
st.button('Re-run single case', help='Optimize single scenario.',
|
|
26
|
+
on_click=owb.runPlan, disabled=kz.caseIsNotRunReady())
|
|
27
|
+
|
|
28
|
+
st.divider()
|
|
29
|
+
if kz.caseHasNotCompletedRun():
|
|
30
|
+
st.info("Case status is currently '%s'." % kz.getKey('caseStatus'))
|
|
31
|
+
else:
|
|
32
|
+
owb.plotSingleResults()
|
|
@@ -37,7 +37,7 @@ of Jack and Jill provided here as an example:
|
|
|
37
37
|
you just downloaded (*case_jack+jill.toml*).
|
|
38
38
|
1) Navigate to the **Wages and Contributions** page and
|
|
39
39
|
drag and drop the wages and contributions file you downloaded (*jack+jill.xlsx*).
|
|
40
|
-
1) Move to the **
|
|
40
|
+
1) Move to the **Single Scenario** section to browse results.
|
|
41
41
|
|
|
42
42
|
Congratulations! :balloon: You just ran your first case. You can now explore each page and
|
|
43
43
|
experiment with different parameters.
|
|
@@ -113,19 +113,19 @@ else:
|
|
|
113
113
|
col1, col2, col3, col4 = st.columns(4, gap='large', vertical_alignment='top')
|
|
114
114
|
with col1:
|
|
115
115
|
kz.initKey('mean0', 0)
|
|
116
|
-
kz.getNum('S&P 500', 'mean0', ro, step=1., callback=updateRates)
|
|
116
|
+
kz.getNum('S&P 500', 'mean0', ro, step=1., min_value=-9., callback=updateRates)
|
|
117
117
|
|
|
118
118
|
with col2:
|
|
119
119
|
kz.initKey('mean1', 0)
|
|
120
|
-
kz.getNum('Corporate Bonds Baa', 'mean1', ro, step=1., callback=updateRates)
|
|
120
|
+
kz.getNum('Corporate Bonds Baa', 'mean1', ro, step=1., min_value=-9., callback=updateRates)
|
|
121
121
|
|
|
122
122
|
with col3:
|
|
123
123
|
kz.initKey('mean2', 0)
|
|
124
|
-
kz.getNum('10-y Treasury Notes', 'mean2', ro, step=1., callback=updateRates)
|
|
124
|
+
kz.getNum('10-y Treasury Notes', 'mean2', ro, step=1., min_value=-9., callback=updateRates)
|
|
125
125
|
|
|
126
126
|
with col4:
|
|
127
127
|
kz.initKey('mean3', 0)
|
|
128
|
-
kz.getNum('Cash Assets/Inflation', 'mean3', ro, step=1., callback=updateRates)
|
|
128
|
+
kz.getNum('Cash Assets/Inflation', 'mean3', ro, step=1., min_value=-9., callback=updateRates)
|
|
129
129
|
|
|
130
130
|
st.write('##### Volatility (%)')
|
|
131
131
|
col1, col2, col3, col4 = st.columns(4, gap='large', vertical_alignment='top')
|
|
@@ -4,11 +4,14 @@ import sskeys as kz
|
|
|
4
4
|
import owlbridge as owb
|
|
5
5
|
|
|
6
6
|
ret = kz.titleBar('worksheets')
|
|
7
|
-
kz.caseHeader("
|
|
7
|
+
kz.caseHeader("Worksheets")
|
|
8
8
|
|
|
9
9
|
if ret is None or kz.caseHasNoPlan():
|
|
10
10
|
st.info('Case(s) must be first created before running this page.')
|
|
11
11
|
else:
|
|
12
|
+
if kz.caseIsRunReady():
|
|
13
|
+
owb.runPlan()
|
|
14
|
+
|
|
12
15
|
if kz.caseHasNotCompletedRun():
|
|
13
16
|
st.info("Case status is currently '%s'." % kz.getKey('caseStatus'))
|
|
14
17
|
else:
|
|
@@ -18,8 +18,8 @@ pages = {
|
|
|
18
18
|
st.Page('Rates_Selection.py', icon=':material/monitoring:'),
|
|
19
19
|
st.Page('Asset_Allocation.py', icon=':material/percent:'),
|
|
20
20
|
st.Page('Optimization_Parameters.py', icon=':material/tune:')],
|
|
21
|
-
'Single Scenario': [st.Page('
|
|
22
|
-
st.Page('
|
|
21
|
+
'Single Scenario': [st.Page('Graphs.py', icon=':material/directions_run:'),
|
|
22
|
+
st.Page('Worksheets.py', icon=':material/data_table:'),
|
|
23
23
|
st.Page('Case_Summary.py', icon=':material/description:')],
|
|
24
24
|
'Multiple Scenarios': [st.Page('Historical_Range.py', icon=':material/history:'),
|
|
25
25
|
st.Page('Monte_Carlo.py', icon=':material/finance:')],
|
|
@@ -125,13 +125,16 @@ def runPlan(plan):
|
|
|
125
125
|
kz.storeKey('caseStatus', plan.caseStatus)
|
|
126
126
|
if plan.caseStatus == 'solved':
|
|
127
127
|
kz.storeKey('summary', plan.summaryString())
|
|
128
|
+
kz.storeKey('casetoml', getCaseString().getvalue())
|
|
128
129
|
else:
|
|
129
130
|
kz.storeKey('summary', '')
|
|
131
|
+
kz.storeKey('casetoml', '')
|
|
130
132
|
|
|
131
133
|
|
|
132
134
|
@_checkPlan
|
|
133
135
|
def runHistorical(plan):
|
|
134
|
-
|
|
136
|
+
plan1 = owl.clone(plan)
|
|
137
|
+
prepareRun(plan1)
|
|
135
138
|
|
|
136
139
|
hyfrm = kz.getKey('hyfrm')
|
|
137
140
|
hyto = kz.getKey('hyto')
|
|
@@ -139,41 +142,33 @@ def runHistorical(plan):
|
|
|
139
142
|
objective, options = kz.getSolveParameters()
|
|
140
143
|
try:
|
|
141
144
|
mybar = progress.Progress(None)
|
|
142
|
-
fig, summary =
|
|
145
|
+
fig, summary = plan1.runHistoricalRange(objective, options, hyfrm, hyto, figure=True, progcall=mybar)
|
|
143
146
|
kz.storeKey('histoPlot', fig)
|
|
144
147
|
kz.storeKey('histoSummary', summary)
|
|
145
148
|
except Exception as e:
|
|
146
149
|
kz.storeKey('histoPlot', None)
|
|
147
150
|
kz.storeKey('histoSummary', None)
|
|
148
|
-
kz.storeKey('caseStatus', 'exception')
|
|
149
151
|
st.error('Solution failed: %s' % e)
|
|
150
|
-
setRates()
|
|
151
152
|
return
|
|
152
153
|
|
|
153
|
-
kz.storeKey('caseStatus', 'ran Historical Range')
|
|
154
|
-
setRates()
|
|
155
|
-
|
|
156
154
|
|
|
157
155
|
@_checkPlan
|
|
158
156
|
def runMC(plan):
|
|
159
|
-
|
|
157
|
+
plan1 = owl.clone(plan)
|
|
158
|
+
prepareRun(plan1)
|
|
160
159
|
|
|
161
160
|
N = kz.getKey('MC_cases')
|
|
162
161
|
|
|
163
162
|
objective, options = kz.getSolveParameters()
|
|
164
163
|
try:
|
|
165
164
|
mybar = progress.Progress(None)
|
|
166
|
-
fig, summary =
|
|
165
|
+
fig, summary = plan1.runMC(objective, options, N, figure=True, progcall=mybar)
|
|
167
166
|
kz.storeKey('monteCarloPlot', fig)
|
|
168
167
|
kz.storeKey('monteCarloSummary', summary)
|
|
169
168
|
except Exception as e:
|
|
170
169
|
kz.storeKey('monteCarloPlot', None)
|
|
171
170
|
kz.storeKey('monteCarloSummary', None)
|
|
172
|
-
kz.storeKey('caseStatus', 'exception')
|
|
173
171
|
st.error('Solution failed: %s' % e)
|
|
174
|
-
return
|
|
175
|
-
|
|
176
|
-
kz.storeKey('caseStatus', 'ran Monte Carlo')
|
|
177
172
|
|
|
178
173
|
|
|
179
174
|
@_checkPlan
|
|
@@ -499,15 +494,21 @@ def saveContributions(plan):
|
|
|
499
494
|
|
|
500
495
|
|
|
501
496
|
@_checkPlan
|
|
502
|
-
def
|
|
497
|
+
def getCaseString(plan):
|
|
503
498
|
stringBuffer = StringIO()
|
|
504
499
|
if kz.getSolveParameters() is None:
|
|
505
500
|
return ''
|
|
506
501
|
plan.saveConfig(stringBuffer)
|
|
502
|
+
|
|
503
|
+
return stringBuffer
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
@_checkPlan
|
|
507
|
+
def saveCaseFile(plan):
|
|
508
|
+
stringBuffer = getCaseString()
|
|
507
509
|
encoded_data = stringBuffer.getvalue().encode('utf-8')
|
|
508
|
-
bytesBuffer = BytesIO(encoded_data)
|
|
509
510
|
|
|
510
|
-
return
|
|
511
|
+
return BytesIO(encoded_data)
|
|
511
512
|
|
|
512
513
|
|
|
513
514
|
def createCaseFromFile(file):
|
|
@@ -531,6 +532,7 @@ def genDic(plan):
|
|
|
531
532
|
dic['plan'] = plan
|
|
532
533
|
dic['name'] = plan._name
|
|
533
534
|
dic['summary'] = ''
|
|
535
|
+
dic['casetoml'] = ''
|
|
534
536
|
dic['caseStatus'] = 'new'
|
|
535
537
|
dic['status'] = ['unknown', 'single', 'married'][plan.N_i]
|
|
536
538
|
# Prepend year if not there.
|
|
@@ -131,6 +131,10 @@ def caseHasCompletedRun():
|
|
|
131
131
|
return getKey('caseStatus') == 'solved'
|
|
132
132
|
|
|
133
133
|
|
|
134
|
+
def caseIsRunReady():
|
|
135
|
+
return not caseIsNotRunReady() and getKey('caseStatus') in ['modified', 'new']
|
|
136
|
+
|
|
137
|
+
|
|
134
138
|
def caseIsNotRunReady():
|
|
135
139
|
return (getKey('plan') is None or
|
|
136
140
|
getKey('objective') is None or
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2025.02.03"
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import streamlit as st
|
|
2
|
-
|
|
3
|
-
import sskeys as kz
|
|
4
|
-
import owlbridge as owb
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
ret = kz.titleBar('results')
|
|
8
|
-
kz.caseHeader("Case Results")
|
|
9
|
-
|
|
10
|
-
if ret is None or kz.caseHasNoPlan():
|
|
11
|
-
st.info('Case(s) must be first created before running this page.')
|
|
12
|
-
else:
|
|
13
|
-
st.write("Optimize a single scenario based on the parameters selected in the **Case setup** section.")
|
|
14
|
-
col1, col2 = st.columns(2, gap='large', vertical_alignment='bottom')
|
|
15
|
-
with col1:
|
|
16
|
-
choices = ['nominal', 'today']
|
|
17
|
-
kz.initKey('plots', choices[0])
|
|
18
|
-
ret = kz.getRadio("Dollar amounts in plots", choices, 'plots',
|
|
19
|
-
callback=owb.setDefaultPlots)
|
|
20
|
-
|
|
21
|
-
with col2:
|
|
22
|
-
st.button('Run single case', help='Optimize single scenario.',
|
|
23
|
-
on_click=owb.runPlan, disabled=kz.caseIsNotRunReady())
|
|
24
|
-
|
|
25
|
-
st.divider()
|
|
26
|
-
if kz.caseHasNotCompletedRun():
|
|
27
|
-
st.info("Case status is currently '%s'." % kz.getKey('caseStatus'))
|
|
28
|
-
else:
|
|
29
|
-
owb.plotSingleResults()
|
|
30
|
-
|
|
31
|
-
st.divider()
|
|
32
|
-
col1, col2 = st.columns(2, gap='large', vertical_alignment='bottom')
|
|
33
|
-
with col1:
|
|
34
|
-
fileName = 'case_'+kz.getKey('name')+'.toml'
|
|
35
|
-
download3 = st.download_button(
|
|
36
|
-
label="Download case file...",
|
|
37
|
-
help='Download TOML file.',
|
|
38
|
-
data=owb.saveCaseFile(),
|
|
39
|
-
file_name=fileName,
|
|
40
|
-
disabled=kz.caseHasNotCompletedRun(),
|
|
41
|
-
mime='application/toml'
|
|
42
|
-
)
|
|
43
|
-
with col2:
|
|
44
|
-
download2 = st.download_button(
|
|
45
|
-
label="Download wages and contributions file...",
|
|
46
|
-
help='Download Excel workbook.',
|
|
47
|
-
data=owb.saveContributions(),
|
|
48
|
-
file_name=kz.getKey('name')+'.xlsx',
|
|
49
|
-
disabled=kz.caseHasNotCompletedRun(),
|
|
50
|
-
mime='application/vnd.ms-excel'
|
|
51
|
-
)
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import streamlit as st
|
|
2
|
-
|
|
3
|
-
import sskeys as kz
|
|
4
|
-
|
|
5
|
-
ret = kz.titleBar('summary')
|
|
6
|
-
kz.caseHeader("Case Summary")
|
|
7
|
-
|
|
8
|
-
if ret is None or kz.caseHasNoPlan():
|
|
9
|
-
st.info('Case(s) must be first created before running this page.')
|
|
10
|
-
else:
|
|
11
|
-
if kz.caseHasNotCompletedRun():
|
|
12
|
-
st.info("Case status is currently '%s'." % kz.getKey('caseStatus'))
|
|
13
|
-
else:
|
|
14
|
-
lines = kz.getKey('summary')
|
|
15
|
-
if lines != '':
|
|
16
|
-
st.code(lines, language=None)
|
|
17
|
-
st.divider()
|
|
18
|
-
st.download_button('Download Summary',
|
|
19
|
-
data=lines,
|
|
20
|
-
file_name='Summary_'+kz.getKey('name')+'.txt',
|
|
21
|
-
mime='text/plain;charset=UTF-8')
|
|
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
|