owlplanner 2025.2.4__tar.gz → 2025.2.6__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 (92) hide show
  1. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/PKG-INFO +25 -40
  2. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/README.md +24 -39
  3. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/pyproject.toml +1 -1
  4. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/src/owlplanner/plan.py +102 -110
  5. owlplanner-2025.2.6/src/owlplanner/version.py +1 -0
  6. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/About_Owl.py +1 -1
  7. owlplanner-2025.2.4/ui/Basic_Info.py → owlplanner-2025.2.6/ui/Create_Case.py +1 -1
  8. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/Documentation.py +28 -22
  9. owlplanner-2025.2.6/ui/Graphs.py +32 -0
  10. owlplanner-2025.2.6/ui/Output_Files.py +57 -0
  11. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/Quick_Start.py +7 -7
  12. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/Rates_Selection.py +4 -4
  13. owlplanner-2025.2.6/ui/Worksheets.py +18 -0
  14. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/main.py +4 -4
  15. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/owlbridge.py +43 -23
  16. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/requirements.txt +1 -1
  17. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/sskeys.py +2 -6
  18. owlplanner-2025.2.4/src/owlplanner/version.py +0 -1
  19. owlplanner-2025.2.4/ui/Case_Results.py +0 -51
  20. owlplanner-2025.2.4/ui/Case_Summary.py +0 -21
  21. owlplanner-2025.2.4/ui/Case_Worksheets.py +0 -24
  22. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/.devcontainer/devcontainer.json +0 -0
  23. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/.flake8 +0 -0
  24. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/.github/workflows/github-actions-runtests.yml +0 -0
  25. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/.gitignore +0 -0
  26. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/INSTALL.md +0 -0
  27. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/LICENSE +0 -0
  28. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/images/AD-taxDef.png +0 -0
  29. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/images/AD-taxFree.png +0 -0
  30. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/images/AD-taxable.png +0 -0
  31. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/images/Hist_Bequest.png +0 -0
  32. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/images/Hist_Spending.png +0 -0
  33. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/images/MC-tutorial2a.png +0 -0
  34. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/images/MC-tutorial2b.png +0 -0
  35. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/images/OwlUI.png +0 -0
  36. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/images/allocations.png +0 -0
  37. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/images/owl.png +0 -0
  38. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/images/profile.png +0 -0
  39. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/images/ratesCorrelations.png +0 -0
  40. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/images/ratesPlot.png +0 -0
  41. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/images/savingsPlot.png +0 -0
  42. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/images/sourcesPlot.png +0 -0
  43. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/images/spendingPlot.png +0 -0
  44. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/images/taxIncomePlot.png +0 -0
  45. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/images/taxesPlot.png +0 -0
  46. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/owl.pdf +0 -0
  47. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/docs/owl.tex +0 -0
  48. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/examples/case_jack+jill.toml +0 -0
  49. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/examples/case_joe.toml +0 -0
  50. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/examples/case_john+sally.toml +0 -0
  51. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/examples/case_kim+sam-bequest.toml +0 -0
  52. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/examples/case_kim+sam-spending.toml +0 -0
  53. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/examples/jack+jill.xlsx +0 -0
  54. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/examples/joe.xlsx +0 -0
  55. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/examples/john+sally.xlsx +0 -0
  56. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/examples/template.xlsx +0 -0
  57. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/notebooks/john+sally.ipynb +0 -0
  58. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/notebooks/kim+sam.ipynb +0 -0
  59. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/notebooks/template.ipynb +0 -0
  60. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/notebooks/tutorial_1.ipynb +0 -0
  61. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/notebooks/tutorial_2.ipynb +0 -0
  62. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/notebooks/tutorial_3.ipynb +0 -0
  63. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/owlplanner.cmd +0 -0
  64. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/requirements.txt +0 -0
  65. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/src/owlplanner/__init__.py +0 -0
  66. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/src/owlplanner/abcapi.py +0 -0
  67. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/src/owlplanner/config.py +0 -0
  68. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/src/owlplanner/data/__init__.py +0 -0
  69. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/src/owlplanner/data/rates.csv +0 -0
  70. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/src/owlplanner/logging.py +0 -0
  71. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/src/owlplanner/progress.py +0 -0
  72. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/src/owlplanner/rates.py +0 -0
  73. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/src/owlplanner/tax2025.py +0 -0
  74. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/src/owlplanner/timelists.py +0 -0
  75. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/src/owlplanner/utils.py +0 -0
  76. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/tests/test_logger.py +0 -0
  77. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/tests/test_regressions.py +0 -0
  78. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/tests/test_repro.py +0 -0
  79. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/tests/test_toml_cases.py +0 -0
  80. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/tests/test_units.py +0 -0
  81. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/Asset_Allocation.py +0 -0
  82. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/Assets.py +0 -0
  83. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/Fixed_Income.py +0 -0
  84. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/Historical_Range.py +0 -0
  85. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/Logs.py +0 -0
  86. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/Monte_Carlo.py +0 -0
  87. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/Optimization_Parameters.py +0 -0
  88. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/README.md +0 -0
  89. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/Settings.py +0 -0
  90. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/Wages_And_Contributions.py +0 -0
  91. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/plots.py +0 -0
  92. {owlplanner-2025.2.4 → owlplanner-2025.2.6}/ui/progress.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owlplanner
3
- Version: 2025.2.4
3
+ Version: 2025.2.6
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. This program comes with no guarantee. Use at your own risk.
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 many more details:
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
- Jack's life horizon: 2024 -> 2051
983
- Jill's life horizon: 2024 -> 2057
984
- Contributions file: examples/jack+jill.xlsx
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. This program comes with no guarantee. Use at your own risk.
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 many more details:
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
- Jack's life horizon: 2024 -> 2051
280
- Jill's life horizon: 2024 -> 2057
281
- Contributions file: examples/jack+jill.xlsx
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:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "owlplanner"
7
- version = "2025.02.04"
7
+ version = "2025.02.06"
8
8
  authors = [
9
9
  { name="Martin-D. Lacasse", email="martin.d.lacasse@gmail.com" },
10
10
  ]
@@ -540,7 +540,7 @@ class Plan(object):
540
540
 
541
541
  if self.N_i == 2:
542
542
  # Approximate calculation for spousal benefit (only valid at FRA).
543
- self.zeta_in[self.i_s, self.n_d:] = max(amounts[self.i_s], amounts[self.i_d] / 2)
543
+ self.zeta_in[self.i_s, self.n_d:] = max(amounts[self.i_s], amounts[self.i_d])
544
544
 
545
545
  self.ssecAmounts = np.array(amounts)
546
546
  self.ssecAges = np.array(ages, dtype=np.int32)
@@ -1413,11 +1413,11 @@ class Plan(object):
1413
1413
 
1414
1414
  progcall.finish()
1415
1415
  self.mylog.resetVerbose()
1416
- fig, summary = self._showResults(objective, df, N, figure)
1417
- self.mylog.print(summary.getvalue())
1416
+ fig, description = self._showResults(objective, df, N, figure)
1417
+ self.mylog.print(description.getvalue())
1418
1418
 
1419
1419
  if figure:
1420
- return fig, summary.getvalue()
1420
+ return fig, description.getvalue()
1421
1421
 
1422
1422
  return N, df
1423
1423
 
@@ -1469,11 +1469,11 @@ class Plan(object):
1469
1469
 
1470
1470
  progcall.finish()
1471
1471
  self.mylog.resetVerbose()
1472
- fig, summary = self._showResults(objective, df, N, figure)
1473
- self.mylog.print(summary.getvalue())
1472
+ fig, description = self._showResults(objective, df, N, figure)
1473
+ self.mylog.print(description.getvalue())
1474
1474
 
1475
1475
  if figure:
1476
- return fig, summary.getvalue()
1476
+ return fig, description.getvalue()
1477
1477
 
1478
1478
  return N, df
1479
1479
 
@@ -1483,9 +1483,9 @@ class Plan(object):
1483
1483
  """
1484
1484
  import seaborn as sbn
1485
1485
 
1486
- summary = io.StringIO()
1486
+ description = io.StringIO()
1487
1487
 
1488
- print('Success rate: %s on %d samples.' % (u.pc(len(df) / N), N), file=summary)
1488
+ print('Success rate: %s on %d samples.' % (u.pc(len(df) / N), N), file=description)
1489
1489
  title = '$N$ = %d, $P$ = %s' % (N, u.pc(len(df) / N))
1490
1490
  means = df.mean(axis=0, numeric_only=True)
1491
1491
  medians = df.median(axis=0, numeric_only=True)
@@ -1498,7 +1498,7 @@ class Plan(object):
1498
1498
  # or if solution led to empty accounts at the end of first spouse's life.
1499
1499
  if np.all(self.phi_j == 1) or medians.iloc[0] < 1:
1500
1500
  if medians.iloc[0] < 1:
1501
- print('Optimized solutions all have null partial bequest in year %d.' % my[0], file=summary)
1501
+ print('Optimized solutions all have null partial bequest in year %d.' % my[0], file=description)
1502
1502
  df.drop('partial', axis=1, inplace=True)
1503
1503
  means = df.mean(axis=0, numeric_only=True)
1504
1504
  medians = df.median(axis=0, numeric_only=True)
@@ -1550,16 +1550,16 @@ class Plan(object):
1550
1550
  # plt.show()
1551
1551
 
1552
1552
  for q in range(len(means)):
1553
- print('%12s: Median (%d $): %s' % (leads[q], self.year_n[0], u.d(medians.iloc[q])), file=summary)
1554
- print('%12s: Mean (%d $): %s' % (leads[q], self.year_n[0], u.d(means.iloc[q])), file=summary)
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)
1555
1555
  print(
1556
1556
  '%12s: Range: %s - %s'
1557
1557
  % (leads[q], u.d(1000 * df.iloc[:, q].min()), u.d(1000 * df.iloc[:, q].max())),
1558
- file=summary)
1558
+ file=description)
1559
1559
  nzeros = len(df.iloc[:, q][df.iloc[:, q] < 0.001])
1560
- print('%12s: N zero solns: %d' % (leads[q], nzeros), file=summary)
1560
+ print('%12s: N zero solns: %d' % (leads[q], nzeros), file=description)
1561
1561
 
1562
- return fig, summary
1562
+ return fig, description
1563
1563
 
1564
1564
  def resolve(self):
1565
1565
  """
@@ -2038,141 +2038,133 @@ class Plan(object):
2038
2038
  @_checkCaseStatus
2039
2039
  def summary(self):
2040
2040
  """
2041
- Print summary of values.
2041
+ Print summary in logs.
2042
2042
  """
2043
2043
  self.mylog.print('SUMMARY ================================================================')
2044
- lines = self.summaryList()
2045
- for line in lines:
2046
- self.mylog.print(line)
2044
+ dic = self.summaryDic()
2045
+ for key, value in dic.items():
2046
+ self.mylog.print(f"{key}: {value}")
2047
2047
  self.mylog.print('------------------------------------------------------------------------')
2048
2048
 
2049
2049
  return None
2050
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
+
2051
2062
  def summaryString(self):
2052
2063
  """
2053
- Print summary of values in a string.
2064
+ Return summary as a string.
2054
2065
  """
2055
- string = ''
2056
- lines = self.summaryList()
2057
- for line in lines:
2058
- string += line + '\n'
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)
2059
2071
 
2060
2072
  return string
2061
2073
 
2062
- def summaryList(self):
2074
+ def summaryDic(self):
2063
2075
  """
2064
- Return string with summary of values.
2076
+ Return dictionary containing summary of values.
2065
2077
  """
2066
2078
  now = self.year_n[0]
2067
- lines = []
2068
- lines.append('Plan name: %s' % self._name)
2069
- for i in range(self.N_i):
2070
- lines.append("%12s's %02d-year life horizon: %d -> %d"
2071
- % (self.inames[i], self.horizons[i], now, now + self.horizons[i] - 1))
2072
- lines.append('Contributions file: %s' % self.timeListsFileName)
2073
- lines.append('Initial balances [taxable, tax-deferred, tax-free]:')
2074
- for i in range(self.N_i):
2075
- lines.append("%12s's accounts: %s" % (self.inames[i], [u.d(self.beta_ij[i][j]) for j in range(self.N_j)]))
2076
-
2077
- lines.append('Return rates: %s' % self.rateMethod)
2078
- if self.rateMethod in ['historical', 'historical average', 'histochastic']:
2079
- lines.append('Rates used: from %d to %d' % (self.rateFrm, self.rateTo))
2080
- elif self.rateMethod == 'stochastic':
2081
- lines.append(
2082
- 'Mean rates used (%%): %s' % (['{:.1f}'.format(100 * self.rateValues[k]) for k in range(self.N_k)])
2083
- )
2084
- lines.append(
2085
- 'Standard deviation used (%%): %s'
2086
- % (['{:.1f}'.format(100 * self.rateStdev[k]) for k in range(self.N_k)])
2087
- )
2088
- lines.append('Correlation matrix used:')
2089
- lines.append('\t\t' + str(self.rateCorr).replace('\n', '\n\t\t'))
2090
- else:
2091
- lines.append('Rates used (%%): %s' % (['{:.1f}'.format(100 * self.rateValues[k]) for k in range(self.N_k)]))
2092
- lines.append("This year's starting date: %s" % self.startDate)
2093
- lines.append('Optimized for: %s' % self.objective)
2094
- lines.append('Solver options: %s' % self.solverOptions)
2095
- lines.append('Number of decision variables: %d' % self.A.nvars)
2096
- lines.append('Number of constraints: %d' % self.A.ncons)
2097
- lines.append('Spending profile: %s' % self.spendingProfile)
2098
- if self.spendingProfile == 'smile':
2099
- lines.append('\twith increase: %d%%, dip: %d%%, delay: %dy'
2100
- % (self.smileIncrease, self.smileDip, self.smileDelay))
2101
- if self.N_i == 2:
2102
- lines.append('Surviving spouse spending needs: %s' % u.pc(self.chi, f=0))
2103
-
2104
- lines.append('Net yearly spending in year %d: %s' % (now, u.d(self.g_n[0] / self.yearFracLeft)))
2105
- lines.append('Net spending remaining in year %d: %s' % (now, u.d(self.g_n[0])))
2106
- lines.append('Net yearly spending profile basis in %d$: %s' % (now, u.d(self.g_n[0] / self.xi_n[0])))
2107
-
2108
- lines.append('Assumed heirs marginal tax rate: %s' % u.pc(self.nu, f=0))
2109
-
2110
- if self.N_i == 2 and self.n_d < self.N_n:
2111
- lines.append("Spousal surplus deposit fraction in %s's taxable account: %s"
2112
- % (self.inames[1], self.eta))
2113
- lines.append('Spousal beneficiary fractions to %s: %s' % (self.inames[self.i_s], self.phi_j.tolist()))
2114
- p_j = self.partialEstate_j * (1 - self.phi_j)
2115
- p_j[1] *= 1 - self.nu
2116
- nx = self.n_d - 1
2117
- totOthers = np.sum(p_j)
2118
- totOthersNow = totOthers / self.gamma_n[nx + 1]
2119
- q_j = self.partialEstate_j * self.phi_j
2120
- totSpousal = np.sum(q_j)
2121
- totSpousalNow = totSpousal / self.gamma_n[nx + 1]
2122
- lines.append('Spousal wealth transfer from %s to %s in year %d (nominal):'
2123
- % (self.inames[self.i_d], self.inames[self.i_s], self.year_n[nx]))
2124
- 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])))
2125
- lines.append('Sum of spousal bequests to %s in year %d in %d$: %s (%s nominal)'
2126
- % (self.inames[self.i_s], self.year_n[nx], now, u.d(totSpousalNow), u.d(totSpousal)))
2127
- lines.append(
2128
- 'Post-tax non-spousal bequests from %s in year %d (nominal):' % (self.inames[self.i_d], self.year_n[nx])
2129
- )
2130
- 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])))
2131
- lines.append(
2132
- 'Sum of post-tax non-spousal bequests from %s in year %d in %d$: %s (%s nominal)'
2133
- % (self.inames[self.i_d], self.year_n[nx], now, u.d(totOthersNow), u.d(totOthers))
2134
- )
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])
2135
2084
 
2136
2085
  totIncome = np.sum(self.g_n, axis=0)
2137
2086
  totIncomeNow = np.sum(self.g_n / self.gamma_n[:-1], axis=0)
2138
- lines.append('Total net spending in %d$: %s (%s nominal)' % (now, u.d(totIncomeNow), u.d(totIncome)))
2087
+ dic[f"Total net spending in {now}$"] = (
2088
+ "%s (%s nominal)" % (u.d(totIncomeNow), u.d(totIncome))
2089
+ )
2139
2090
 
2140
2091
  totRoth = np.sum(self.x_in, axis=(0, 1))
2141
2092
  totRothNow = np.sum(np.sum(self.x_in, axis=0) / self.gamma_n[:-1], axis=0)
2142
- lines.append('Total Roth conversions in %d$: %s (%s nominal)' % (now, u.d(totRothNow), u.d(totRoth)))
2093
+ dic[f"Total Roth conversions in {now}$"] = (
2094
+ "%s (%s nominal)" % (u.d(totRothNow), u.d(totRoth))
2095
+ )
2143
2096
 
2144
2097
  taxPaid = np.sum(self.T_n, axis=0)
2145
2098
  taxPaidNow = np.sum(self.T_n / self.gamma_n[:-1], axis=0)
2146
- lines.append('Total income tax paid on ordinary income in %d$: %s (%s nominal)'
2147
- % (now, u.d(taxPaidNow), u.d(taxPaid)))
2099
+ dic[f"Total income tax paid on ordinary income in {now}$"] = (
2100
+ "%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
2101
+ )
2148
2102
 
2149
2103
  taxPaid = np.sum(self.U_n, axis=0)
2150
2104
  taxPaidNow = np.sum(self.U_n / self.gamma_n[:-1], axis=0)
2151
- lines.append('Total tax paid on gains and dividends in %d$: %s (%s nominal)'
2152
- % (now, u.d(taxPaidNow), u.d(taxPaid)))
2105
+ dic[f"Total tax paid on gains and dividends in {now}$"] = (
2106
+ "%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
2107
+ )
2153
2108
 
2154
2109
  taxPaid = np.sum(self.M_n, axis=0)
2155
2110
  taxPaidNow = np.sum(self.M_n / self.gamma_n[:-1], axis=0)
2156
- lines.append('Total Medicare premiums paid in %d$: %s (%s nominal)' % (now, u.d(taxPaidNow), u.d(taxPaid)))
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
+ )
2157
2141
 
2158
2142
  estate = np.sum(self.b_ijn[:, :, self.N_n], axis=0)
2159
2143
  estate[1] *= 1 - self.nu
2160
- lines.append('Post-tax account values at the end of final plan year %d: (nominal)' % self.year_n[-1])
2161
- lines.append(' taxable: %s tax-def: %s tax-free: %s' % (u.d(estate[0]), u.d(estate[1]), u.d(estate[2])))
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
+ )
2162
2147
 
2163
2148
  totEstate = np.sum(estate)
2164
2149
  totEstateNow = totEstate / self.gamma_n[-1]
2165
- lines.append(
2166
- 'Total estate value at the end of final plan year %d in %d$: %s (%s nominal)'
2167
- % (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))
2168
2152
  )
2169
- lines.append(
2170
- "Inflation factor from this year's start date to the end of plan final year: %.2f" % self.gamma_n[-1]
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])
2171
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
+ )
2172
2161
 
2173
- lines.append('Case executed on: %s' % self._timestamp)
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)
2174
2166
 
2175
- return lines
2167
+ return dic
2176
2168
 
2177
2169
  def showRatesCorrelations(self, tag='', shareRange=False, figure=False):
2178
2170
  """
@@ -0,0 +1 @@
1
+ __version__ = "2025.02.06"
@@ -12,7 +12,7 @@ st.snow()
12
12
 
13
13
  st.write('''
14
14
  - Owl is released under GPL Licence through a publicly available
15
- repository on [github](https://github.com/mdlacasse/owl).
15
+ repository on [GitHub](https://github.com/mdlacasse/owl).
16
16
 
17
17
  - Mathematical formulation of the linear optimization problem can be
18
18
  found [here](https://raw.github.com/mdlacasse/Owl/main/docs/owl.pdf).
@@ -7,7 +7,7 @@ import owlbridge as owb
7
7
 
8
8
  caseChoices = kz.allCaseNames()
9
9
  ret = kz.titleBar('setup', caseChoices)
10
- kz.caseHeader("Basic Info")
10
+ kz.caseHeader("Create Case")
11
11
 
12
12
  if ret == kz.newCase:
13
13
  st.info('#### Starting a new case from scratch.\n\n'
@@ -46,7 +46,7 @@ There are four sections in the user interface:
46
46
  ### :orange[Case Setup]
47
47
  This section contains the steps for creating and configuring case scenarios.
48
48
 
49
- #### Basic Info
49
+ #### Create Case
50
50
  This page is where every new scenario begins.
51
51
  It controls the creation of scenarios as the `Select case` drop-down menu contains
52
52
  two additional items when this page is open:
@@ -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 Results](#case-results) page and made available to reload at a later time.
75
+ can be saved under the [Output Files](#output-files) 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
 
@@ -246,8 +246,10 @@ As one of the two choices (net spending or bequest) is selected as the value to
246
246
  the other becomes a constraint to obey.
247
247
 
248
248
  The maximum amount for Roth conversions and who can execute them is configurable.
249
- If a *Wages and Contributions* file has been uploaded and the `Convert from contributions file`
250
- button is toggled, Roth conversions will not be optimized, but will rather be performed according to
249
+ Roth conversions are optimized for reducing taxes and maximizing the selected objective function,
250
+ unless the `Convert from contributions file`
251
+ button is toggled, in which case Roth conversions will not be optimized,
252
+ but will rather be performed according to
251
253
  the `Roth conv` column on the
252
254
  [Wages and Contributions](#wages-and-contributions) page.
253
255
 
@@ -278,43 +280,47 @@ than on January 1$^{st}$, the value of the first year will be reduced accordingl
278
280
  --------------------------------------------------------------------------------------
279
281
  ### :orange[Single Scenario]
280
282
 
281
- #### Case Results
282
- This page allows to run a single scenario based on the selections made
283
+ #### Graphs
284
+ This page displays various plots from a single scenario based on the selections made
283
285
  in the [Case Setup](#case-setup) section.
284
286
  This simulation uses a single instance of a series of rates, either fixed or varying,
285
287
  as selected in the **Case Setup** section.
286
288
  The outcome is optimized according to the chosen parameters: either maximize the
287
289
  net spending, of maximize the bequest under the constraint of a net spending amount.
288
- If `Convert from contributions file` is not toggled on,
289
- Roth conversions are optimized for reducing taxes and maximizing the selected objective function.
290
290
  Various plots show the results, which can be displayed in today's \\$ or
291
291
  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. The
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
- #### Case Worksheets
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
307
- together as an Excel workbook by clicking the button at the bottom
308
- of the page.
301
+ together as an Excel workbook by clicking the associated button on the
302
+ [Output Files](#output-files) page.
309
303
  Note that all values here (worksheets and workbook) are in \\$, not in thousands.
310
304
  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
- #### Case Summary
314
- This page shows a summary of the scenario which was computed.
315
- It displays informative sums of relevant income, bequest, and spending values.
307
+ #### Output Files
308
+ This page allow to save many files for future use.
309
+ First it shows a synopsis of the computed scenario by
310
+ displaying sums of income, bequest, and spending values over the duration of the plan.
316
311
  The contents of this page can be downloaded as a plain text file by
317
- clicking the button at the bottom of the page.
312
+ clicking the button at the bottom of the section.
313
+
314
+ Finally, parameters used to generate the case are collected in *toml* format and displayed.
315
+ The `Download case file...` button allows to save the parameters used to generate the
316
+ outcome of this case to a *case* file.
317
+
318
+ Another section called `Excel workbooks` allows
319
+ to save the contents of the tables on the corresponding page to an Excel workbook.
320
+
321
+ With the case parameter and the wages and contributions files,
322
+ the same case can be reproduced at a later time by uploading
323
+ them through the widgets on the `Create Case` and `Wages and Contributions` pages.
318
324
 
319
325
  --------------------------------------------------------------------------------------
320
326
  ### :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.isCaseUnsolved():
30
+ st.info("Case status is currently '%s'." % kz.getKey('caseStatus'))
31
+ else:
32
+ owb.plotSingleResults()