owlplanner 2025.1.29__tar.gz → 2025.2.1__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 (93) hide show
  1. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/PKG-INFO +1 -1
  2. owlplanner-2025.2.1/examples/jack+jill.xlsx +0 -0
  3. owlplanner-2025.2.1/examples/joe.xlsx +0 -0
  4. owlplanner-2025.2.1/examples/john+sally.xlsx +0 -0
  5. owlplanner-2025.2.1/examples/template.xlsx +0 -0
  6. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/pyproject.toml +1 -1
  7. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/plan.py +43 -40
  8. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/timelists.py +8 -8
  9. owlplanner-2025.2.1/src/owlplanner/version.py +1 -0
  10. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/About_Owl.py +3 -3
  11. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Case_Results.py +1 -1
  12. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Documentation.py +14 -8
  13. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Optimization_Parameters.py +13 -8
  14. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Quick_Start.py +13 -11
  15. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/owlbridge.py +9 -11
  16. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/requirements.txt +1 -1
  17. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/sskeys.py +12 -9
  18. owlplanner-2025.1.29/examples/jack+jill.xlsx +0 -0
  19. owlplanner-2025.1.29/examples/joe.xlsx +0 -0
  20. owlplanner-2025.1.29/examples/john+sally.xlsx +0 -0
  21. owlplanner-2025.1.29/examples/template.xlsx +0 -0
  22. owlplanner-2025.1.29/src/owlplanner/version.py +0 -1
  23. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/.devcontainer/devcontainer.json +0 -0
  24. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/.flake8 +0 -0
  25. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/.github/workflows/github-actions-runtests.yml +0 -0
  26. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/.gitignore +0 -0
  27. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/INSTALL.md +0 -0
  28. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/LICENSE +0 -0
  29. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/README.md +0 -0
  30. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/AD-taxDef.png +0 -0
  31. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/AD-taxFree.png +0 -0
  32. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/AD-taxable.png +0 -0
  33. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/Hist_Bequest.png +0 -0
  34. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/Hist_Spending.png +0 -0
  35. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/MC-tutorial2a.png +0 -0
  36. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/MC-tutorial2b.png +0 -0
  37. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/OwlUI.png +0 -0
  38. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/allocations.png +0 -0
  39. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/owl.png +0 -0
  40. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/profile.png +0 -0
  41. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/ratesCorrelations.png +0 -0
  42. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/ratesPlot.png +0 -0
  43. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/savingsPlot.png +0 -0
  44. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/sourcesPlot.png +0 -0
  45. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/spendingPlot.png +0 -0
  46. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/taxIncomePlot.png +0 -0
  47. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/taxesPlot.png +0 -0
  48. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/owl.pdf +0 -0
  49. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/owl.tex +0 -0
  50. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/examples/case_jack+jill.toml +0 -0
  51. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/examples/case_joe.toml +0 -0
  52. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/examples/case_john+sally.toml +0 -0
  53. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/examples/case_kim+sam-bequest.toml +0 -0
  54. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/examples/case_kim+sam-spending.toml +0 -0
  55. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/notebooks/john+sally.ipynb +0 -0
  56. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/notebooks/kim+sam.ipynb +0 -0
  57. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/notebooks/template.ipynb +0 -0
  58. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/notebooks/tutorial_1.ipynb +0 -0
  59. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/notebooks/tutorial_2.ipynb +0 -0
  60. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/notebooks/tutorial_3.ipynb +0 -0
  61. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/owlplanner.cmd +0 -0
  62. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/requirements.txt +0 -0
  63. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/__init__.py +0 -0
  64. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/abcapi.py +0 -0
  65. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/config.py +0 -0
  66. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/data/__init__.py +0 -0
  67. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/data/rates.csv +0 -0
  68. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/logging.py +0 -0
  69. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/progress.py +0 -0
  70. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/rates.py +0 -0
  71. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/tax2025.py +0 -0
  72. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/utils.py +0 -0
  73. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/tests/test_logger.py +0 -0
  74. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/tests/test_regressions.py +0 -0
  75. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/tests/test_repro.py +0 -0
  76. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/tests/test_toml_cases.py +0 -0
  77. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/tests/test_units.py +0 -0
  78. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Asset_Allocation.py +0 -0
  79. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Assets.py +0 -0
  80. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Basic_Info.py +0 -0
  81. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Case_Summary.py +0 -0
  82. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Case_Worksheets.py +0 -0
  83. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Fixed_Income.py +0 -0
  84. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Historical_Range.py +0 -0
  85. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Logs.py +0 -0
  86. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Monte_Carlo.py +0 -0
  87. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/README.md +0 -0
  88. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Rates_Selection.py +0 -0
  89. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Settings.py +0 -0
  90. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Wages_And_Contributions.py +0 -0
  91. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/main.py +0 -0
  92. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/plots.py +0 -0
  93. {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/progress.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owlplanner
3
- Version: 2025.1.29
3
+ Version: 2025.2.1
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
Binary file
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "owlplanner"
7
- version = "2025.01.29"
7
+ version = "2025.02.01"
8
8
  authors = [
9
9
  { name="Martin-D. Lacasse", email="martin.d.lacasse@gmail.com" },
10
10
  ]
@@ -397,16 +397,6 @@ class Plan(object):
397
397
 
398
398
  return None
399
399
 
400
- def setPreviousMAGI(self, magi, units='k'):
401
- """
402
- Set MAGI for two previous years to the plan. Values are in nominal $k.
403
- """
404
- assert len(magi) == 2, "MAGI must have two values."
405
- fac = u.getUnits(units)
406
- u.rescale(magi, fac)
407
- self.mylog.vprint('Setting previous years MAGI to:', [u.d(magi[i]) for i in range(2)])
408
- self.prevMAGI = np.array(magi)
409
-
410
400
  def rename(self, newname):
411
401
  """
412
402
  Override name of the plan. Plan name is used
@@ -869,12 +859,12 @@ class Plan(object):
869
859
 
870
860
  'year',
871
861
  'anticipated wages',
872
- 'ctrb taxable',
873
- 'ctrb 401k',
874
- 'ctrb Roth 401k',
875
- 'ctrb IRA',
876
- 'ctrb Roth IRA',
877
- 'Roth X',
862
+ 'taxable ctrb',
863
+ '401k ctrb',
864
+ 'Roth 401k ctrb',
865
+ 'IRA ctrb',
866
+ 'Roth IRA ctrb',
867
+ 'Roth conv',
878
868
  'big-ticket items'
879
869
 
880
870
  in any order. A template is provided as an example.
@@ -900,12 +890,12 @@ class Plan(object):
900
890
  for i, iname in enumerate(self.inames):
901
891
  h = self.horizons[i]
902
892
  self.omega_in[i, :h] = self.timeLists[iname]['anticipated wages'].iloc[:h]
903
- self.kappa_ijn[i, 0, :h] = self.timeLists[iname]['ctrb taxable'].iloc[:h]
904
- self.kappa_ijn[i, 1, :h] = self.timeLists[iname]['ctrb 401k'].iloc[:h]
905
- self.kappa_ijn[i, 2, :h] = self.timeLists[iname]['ctrb Roth 401k'].iloc[:h]
906
- self.kappa_ijn[i, 1, :h] += self.timeLists[iname]['ctrb IRA'].iloc[:h]
907
- self.kappa_ijn[i, 2, :h] += self.timeLists[iname]['ctrb Roth IRA'].iloc[:h]
908
- self.myRothX_in[i, :h] = self.timeLists[iname]['Roth X'].iloc[:h]
893
+ self.kappa_ijn[i, 0, :h] = self.timeLists[iname]['taxable ctrb'].iloc[:h]
894
+ self.kappa_ijn[i, 1, :h] = self.timeLists[iname]['401k ctrb'].iloc[:h]
895
+ self.kappa_ijn[i, 2, :h] = self.timeLists[iname]['Roth 401k ctrb'].iloc[:h]
896
+ self.kappa_ijn[i, 1, :h] += self.timeLists[iname]['IRA ctrb'].iloc[:h]
897
+ self.kappa_ijn[i, 2, :h] += self.timeLists[iname]['Roth IRA ctrb'].iloc[:h]
898
+ self.myRothX_in[i, :h] = self.timeLists[iname]['Roth conv'].iloc[:h]
909
899
  self.Lambda_in[i, :h] = self.timeLists[iname]['big-ticket items'].iloc[:h]
910
900
 
911
901
  # In 1st year, reduce wages and contribution depending on starting date.
@@ -957,8 +947,8 @@ class Plan(object):
957
947
  self.myRothX_in[:, :] = 0.
958
948
  self.kappa_ijn[:, :, :] = 0.
959
949
 
960
- cols = ['year', 'anticipated wages', 'ctrb taxable', 'ctrb 401k',
961
- 'ctrb Roth 401k', 'ctrb IRA', 'ctrb Roth IRA', 'Roth X', 'big-ticket items']
950
+ cols = ['year', 'anticipated wages', 'taxable ctrb', '401k ctrb',
951
+ 'Roth 401k ctrb', 'IRA ctrb', 'Roth IRA ctrb', 'Roth conv', 'big-ticket items']
962
952
  for i, iname in enumerate(self.inames):
963
953
  h = self.horizons[i]
964
954
  df = pd.DataFrame(0, index=np.arange(h), columns=cols)
@@ -1619,6 +1609,7 @@ class Plan(object):
1619
1609
  'noRothConversions',
1620
1610
  'withMedicare',
1621
1611
  'solver',
1612
+ 'previousMAGIs',
1622
1613
  ]
1623
1614
  # We will modify options if required.
1624
1615
  if options is None:
@@ -1647,6 +1638,17 @@ class Plan(object):
1647
1638
  if objective == 'maxSpending' and 'bequest' not in myoptions:
1648
1639
  self.mylog.vprint('Using bequest of $1.')
1649
1640
 
1641
+ if 'previousMAGIs' in myoptions:
1642
+ magi = myoptions['previousMAGIs']
1643
+ if len(magi) != 2:
1644
+ raise ValueError("previousMAGIs must have two values.")
1645
+
1646
+ if 'units' in options:
1647
+ units = u.getUnits(options['units'])
1648
+ else:
1649
+ units = 1000
1650
+ self.prevMAGI = units * np.array(magi)
1651
+
1650
1652
  self._adjustParameters()
1651
1653
 
1652
1654
  if 'solver' in options:
@@ -1991,8 +1993,8 @@ class Plan(object):
1991
1993
  'wages',
1992
1994
  'ssec',
1993
1995
  'pension',
1994
- 'dist',
1995
- 'rmd',
1996
+ '+dist',
1997
+ 'RMD',
1996
1998
  'RothX',
1997
1999
  'wdrwl taxable',
1998
2000
  'wdrwl tax-free',
@@ -2002,12 +2004,12 @@ class Plan(object):
2002
2004
  sources['wages'] = self.omega_in
2003
2005
  sources['ssec'] = self.zetaBar_in
2004
2006
  sources['pension'] = self.pi_in
2005
- sources['wdrwl taxable'] = self.w_ijn[:, 0, :]
2006
- sources['rmd'] = self.rmd_in
2007
- sources['dist'] = self.dist_in
2007
+ sources['taxable wdrwl'] = self.w_ijn[:, 0, :]
2008
+ sources['RMD'] = self.rmd_in
2009
+ sources['+dist'] = self.dist_in
2008
2010
  sources['RothX'] = self.x_in
2009
- sources['wdrwl tax-free'] = self.w_ijn[:, 2, :]
2010
- sources['bti'] = self.Lambda_in
2011
+ sources['tax-free wdrwl'] = self.w_ijn[:, 2, :]
2012
+ sources['BTI'] = self.Lambda_in
2011
2013
 
2012
2014
  savings = {}
2013
2015
  savings['taxable'] = self.b_ijn[:, 0, :]
@@ -2717,7 +2719,7 @@ class Plan(object):
2717
2719
  'all wages': np.sum(self.omega_in, axis=0),
2718
2720
  'all pensions': np.sum(self.pi_in, axis=0),
2719
2721
  'all soc sec': np.sum(self.zetaBar_in, axis=0),
2720
- "all bti's": np.sum(self.Lambda_in, axis=0),
2722
+ "all BTI's": np.sum(self.Lambda_in, axis=0),
2721
2723
  'all wdrwls': np.sum(self.w_ijn, axis=(0, 1)),
2722
2724
  'all deposits': -np.sum(self.d_in, axis=0),
2723
2725
  'ord taxes': -self.T_n,
@@ -2733,12 +2735,12 @@ class Plan(object):
2733
2735
  'wages': self.sources_in['wages'],
2734
2736
  'social sec': self.sources_in['ssec'],
2735
2737
  'pension': self.sources_in['pension'],
2736
- 'txbl acc wdrwl': self.sources_in['wdrwl taxable'],
2737
- 'RMDs': self.sources_in['rmd'],
2738
- '+distributions': self.sources_in['dist'],
2739
- 'Roth conversion': self.sources_in['RothX'],
2740
- 'tax-free wdrwl': self.sources_in['wdrwl tax-free'],
2741
- 'big-ticket items': self.sources_in['bti'],
2738
+ 'txbl acc wdrwl': self.sources_in['txbl acc wdrwl'],
2739
+ 'RMDs': self.sources_in['RMD'],
2740
+ '+distributions': self.sources_in['+dist'],
2741
+ 'Roth conv': self.sources_in['RothX'],
2742
+ 'tax-free wdrwl': self.sources_in['tax-free wdrwl'],
2743
+ 'big-ticket items': self.sources_in['BTI'],
2742
2744
  }
2743
2745
 
2744
2746
  for i in range(self.N_i):
@@ -2749,13 +2751,14 @@ class Plan(object):
2749
2751
  # Account balances except final year.
2750
2752
  accDic = {
2751
2753
  'taxable bal': self.b_ijn[:, 0, :-1],
2754
+ 'taxable ctrb': self.kappa_ijn[:, 0, :],
2752
2755
  'taxable dep': self.d_in,
2753
2756
  'taxable wdrwl': self.w_ijn[:, 0, :],
2754
2757
  'tax-deferred bal': self.b_ijn[:, 1, :-1],
2755
2758
  'tax-deferred ctrb': self.kappa_ijn[:, 1, :],
2756
2759
  'tax-deferred wdrwl': self.w_ijn[:, 1, :],
2757
2760
  '(included RMDs)': self.rmd_in[:, :],
2758
- 'Roth conversion': self.x_in,
2761
+ 'Roth conv': self.x_in,
2759
2762
  'tax-free bal': self.b_ijn[:, 2, :-1],
2760
2763
  'tax-free ctrb': self.kappa_ijn[:, 2, :],
2761
2764
  'tax-free wdrwl': self.w_ijn[:, 2, :],
@@ -2854,7 +2857,7 @@ class Plan(object):
2854
2857
  planData[self.inames[i] + ' tx-def ctrb'] = self.kappa_ijn[i, 1, :]
2855
2858
  planData[self.inames[i] + ' tx-def wdrl'] = self.w_ijn[i, 1, :]
2856
2859
  planData[self.inames[i] + ' (RMD)'] = self.rmd_in[i, :]
2857
- planData[self.inames[i] + ' Roth conversion'] = self.x_in[i, :]
2860
+ planData[self.inames[i] + ' Roth conv'] = self.x_in[i, :]
2858
2861
  planData[self.inames[i] + ' tx-free bal'] = self.b_ijn[i, 2, :-1]
2859
2862
  planData[self.inames[i] + ' tx-free ctrb'] = self.kappa_ijn[i, 2, :]
2860
2863
  planData[self.inames[i] + ' tax-free wdrwl'] = self.w_ijn[i, 2, :]
@@ -23,12 +23,12 @@ import pandas as pd
23
23
  timeHorizonItems = [
24
24
  'year',
25
25
  'anticipated wages',
26
- 'ctrb taxable',
27
- 'ctrb 401k',
28
- 'ctrb Roth 401k',
29
- 'ctrb IRA',
30
- 'ctrb Roth IRA',
31
- 'Roth X',
26
+ 'taxable ctrb',
27
+ '401k ctrb',
28
+ 'Roth 401k ctrb',
29
+ 'IRA ctrb',
30
+ 'Roth IRA ctrb',
31
+ 'Roth conv',
32
32
  'big-ticket items',
33
33
  ]
34
34
 
@@ -38,8 +38,8 @@ def read(finput, inames, horizons, mylog):
38
38
  Read listed parameters from an excel spreadsheet or through
39
39
  a dictionary of dataframes through Pandas.
40
40
  Use one sheet for each individual with the following 9 columns:
41
- year, anticipated wages, ctrb taxable, ctrb 401k, ctrb Roth 401k,
42
- ctrb IRA, ctrb Roth IRA, Roth X, and big-ticket items.
41
+ year, anticipated wages, taxable ctrb, 401k ctrb, Roth 401k ctrb,
42
+ IRA ctrb, Roth IRA ctrb, Roth conv, and big-ticket items.
43
43
  Supports xls, xlsx, xlsm, xlsb, odf, ods, and odt file extensions.
44
44
  Returs a dictionary of dataframes by individual's names.
45
45
  """
@@ -0,0 +1 @@
1
+ __version__ = "2025.02.01"
@@ -7,8 +7,7 @@ import owlbridge as owb
7
7
  st.write("## About Owl 🦉")
8
8
  kz.orangeDivider()
9
9
 
10
- st.write("This version of Owl was released in January 2025 (version %s). "
11
- "Running on Streamlit %s." % (owb.version(), st.__version__))
10
+ st.write("This is Owl version %s running on Streamlit %s." % (owb.version(), st.__version__))
12
11
  st.snow()
13
12
 
14
13
  st.write('''
@@ -36,7 +35,8 @@ Copyright © 2024 - Martin-D. Lacasse
36
35
 
37
36
  #### :orange[Privacy]
38
37
  - This app does not store or forward any information. All data entered is lost
39
- after a session is closed.
38
+ after a session is closed. However, you can choose to download selected data to your own
39
+ computer before closing the session.
40
40
  Source code is publicly available and can be inspected in the repository.
41
41
 
42
42
  #### :orange[Disclaimers]
@@ -38,7 +38,7 @@ else:
38
38
  data=owb.saveCaseFile(),
39
39
  file_name=fileName,
40
40
  disabled=kz.caseHasNotCompletedRun(),
41
- mime='txt/plain'
41
+ mime='application/toml'
42
42
  )
43
43
  with col2:
44
44
  download2 = st.download_button(
@@ -105,7 +105,11 @@ how to split potential surplus budget moneys between the taxable accounts of the
105
105
  When the `Beneficiary fractions` are not all 1, it is recommended to deposit all
106
106
  surplus moneys in the taxable account of the first individual to pass. Otherwise,
107
107
  the optimizer will find creative solutions that can generate surpluses in order
108
- to maximize the final bequest.
108
+ to maximize the final bequest. Finally, when fractions are not all equal,
109
+ it can take longer to solve (minutes) as these cases trigger the use
110
+ of binary variables which involve more complex algorithms.
111
+ In some situations, transfers from tax-deferred savings accounts to taxable
112
+ savings accounts, through surpluses and deposits, can be part of the optimal solution.
109
113
 
110
114
  Setting a surplus fraction that deposits all surpluses in the survivor's account
111
115
  can lead to slow convergence. This is especially noticeable when solving with
@@ -124,7 +128,7 @@ The wages and contributions data contains 9 columns titled as follows:
124
128
 
125
129
  # <span style="font-size: 10px;"> </span>
126
130
  st.write('''
127
- |year|anticipated wages|ctrb taxable|ctrb 401k|ctrb Roth 401k|ctrb IRA|ctrb Roth IRA|Roth X|big-ticket items|
131
+ |year|anticipated wages|ctrb taxable|ctrb 401k|ctrb Roth 401k|ctrb IRA|ctrb Roth IRA|Roth conv|big-ticket items|
128
132
  |--|--|--|--|--|--|--|--|--|
129
133
  |2025 | | | | | | | | |
130
134
  |2026 | | | | | | | | |
@@ -158,7 +162,7 @@ of the anticipated wages for contributions as this can sometimes be easier. For
158
162
  purpose, additional columns (on the right) can be used for storing the anticipated total salary and
159
163
  to derive relevant numbers from there. These columns will be ignored when the file is processed.
160
164
 
161
- Roth conversion can be specified in the column marked *Roth X*.
165
+ Roth conversion can be specified in the column marked *Roth conv*.
162
166
  This column is provided to override the Roth conversion optimization in Owl. When the option
163
167
  `Convert as in contribution file` is toggled in the [Optimization Parameters](#optimization-parameters) page,
164
168
  values from the contributions file will be used and no optimization over Roth conversions
@@ -252,7 +256,6 @@ the calculations by a factor of 2 to 3, which can be useful when running Monte C
252
256
  If the age of individuals makes them eligible for Medicare within the next two years,
253
257
  additional cells will appear for entering the Modified Adjusted Gross Income (MAGI) for past years.
254
258
  These numbers are needed to calculate the Income-Related Monthly Adjusted Amounts (IRMAA).
255
- Note that these values are not included in the *case* parameter file when saving.
256
259
 
257
260
  The time profile of the net spending amount
258
261
  can be selected to either be *flat* or follow a *smile* shape.
@@ -304,6 +307,8 @@ Each table can be downloaded separately in csv format, or all tables can be down
304
307
  together as an Excel workbook by clicking the button at the bottom
305
308
  of the page.
306
309
  Note that all values here (worksheets and workbook) are in \\$, not in thousands.
310
+ The first line of the *Sources* worksheets are the most important
311
+ as these lines are the only ones that are actionable.
307
312
 
308
313
  #### Case Summary
309
314
  This page shows a summary of the scenario which was computed.
@@ -348,15 +353,16 @@ when considering Monte Carlo simulations, consider:
348
353
  --------------------------------------------------------------------------------------
349
354
  ### :orange[Resources]
350
355
  #### Logs
351
- Messages coming from the underlying Owl calculation engine are displayed under this page.
356
+ Messages coming from the underlying Owl calculation engine are displayed on this page.
352
357
 
353
358
  #### Settings
354
- This page contains a global setting for choosing the display style used for the graphs. Some color
359
+ This page contains global settings. At the current time, there is only a single
360
+ option for choosing the style used for the graphs. Some color
355
361
  schemes are best suited for colorblind individuals. The *classic* offers good contrast, while
356
- *petroff10* has more distinguishing colors.
362
+ *petroff10* presents other distinguishing colors.
357
363
 
358
364
  #### Documentation
359
- These pages.
365
+ These very pages.
360
366
 
361
367
  #### About Owl
362
368
  Credits and disclaimers.
@@ -24,6 +24,7 @@ if ret is None or kz.caseHasNoPlan():
24
24
  else:
25
25
  kz.runOncePerCase(initProfile)
26
26
 
27
+ st.write('##### Objective')
27
28
  col1, col2 = st.columns(2, gap='large', vertical_alignment='top')
28
29
  with col1:
29
30
  choices = ['Net spending', 'Bequest']
@@ -41,6 +42,7 @@ else:
41
42
  ret = kz.getNum("Desired annual net spending (\\$k)", 'netSpending', help=helpmsg)
42
43
 
43
44
  st.divider()
45
+ st.write('##### Roth Conversions')
44
46
  col1, col2 = st.columns(2, gap='large', vertical_alignment='top')
45
47
  with col1:
46
48
  iname0 = kz.getKey('iname0')
@@ -49,7 +51,6 @@ else:
49
51
  fromFile = kz.getKey('readRothX')
50
52
  kz.initKey('maxRothConversion', 50)
51
53
  ret = kz.getNum("Maximum Roth conversion (\\$k)", 'maxRothConversion', disabled=fromFile, help=helpmsg)
52
- # caseHasNoContributions = (kz.getKey('stTimeLists') is None)
53
54
  ret = kz.getToggle('Convert as in wages and contributions tables', 'readRothX')
54
55
 
55
56
  with col2:
@@ -62,8 +63,9 @@ else:
62
63
  "noRothConversions", help=helpmsg)
63
64
 
64
65
  st.divider()
66
+ st.write('##### Medicare')
65
67
  kz.initKey('withMedicare', True)
66
- col1, col2, col3 = st.columns(3, gap='large', vertical_alignment='top')
68
+ col1, col2 = st.columns(2, gap='large', vertical_alignment='top')
67
69
  with col1:
68
70
  helpmsg = "Do or do not perform additional Medicare and IRMAA calculations."
69
71
  ret = kz.getToggle('Medicare and IRMAA calculations', 'withMedicare', help=helpmsg)
@@ -75,16 +77,19 @@ else:
75
77
  kz.initKey('MAGI'+str(ii), 0)
76
78
  if years[ii] > 0:
77
79
  ret = kz.getNum(f"MAGI for year {years[ii]} ($k)", 'MAGI'+str(ii), help=helpmsg)
78
- with col3:
79
- if owb.hasMOSEK():
80
- choices = ['HiGHS', 'MOSEK']
81
- kz.initKey('solver', choices[0])
82
- ret = kz.getRadio('Solver', choices, 'solver')
80
+
81
+ if owb.hasMOSEK():
82
+ st.divider()
83
+ st.write('##### Solver')
84
+ choices = ['HiGHS', 'MOSEK']
85
+ kz.initKey('solver', choices[0])
86
+ ret = kz.getRadio('Linear programming solver', choices, 'solver')
83
87
 
84
88
  st.divider()
89
+ st.write('##### Spending Profile')
85
90
  col1, col2, col3 = st.columns(3, gap='medium', vertical_alignment='top')
86
91
  with col1:
87
- ret = kz.getRadio("Spending profile", profileChoices, 'spendingProfile', callback=owb.setProfile)
92
+ ret = kz.getRadio("Type of profile", profileChoices, 'spendingProfile', callback=owb.setProfile)
88
93
  with col2:
89
94
  if kz.getKey('status') == 'married':
90
95
  helpmsg = 'Percentage of spending required for the surviving spouse.'
@@ -11,36 +11,38 @@ with col1:
11
11
  kz.orangeDivider()
12
12
  st.write('### Quick Start')
13
13
  st.markdown('''
14
- Owl uses two files to store the specifications of a case so that it can be recalled at a later time.
14
+ Owl does not store any information related to a case:
15
+ all is lost after a session is closed. For that reason,
16
+ two files can be used to store the specifications of a case so that it can be recalled at a later time.
15
17
  - A *case* parameter file
16
- specifies account balances, asset allocation, social security and pension, rates,
18
+ specifying account balances, asset allocation, social security and pension, rates,
17
19
  optimization parameters and related assumptions.
18
20
  This file is in *toml* format which is editable with a simple text editor.
19
- - A *wages and contributions* file contains a
20
- time table describing anticipated wages, future contributions
21
+ - A *wages and contributions* file containing a
22
+ time table with anticipated wages, future contributions
21
23
  to savings accounts, and anticipated big-ticket items, which can be either expenses or income.
22
24
  This file is in Excel or LibreOffice format, and has one tab per individual in the plan.
23
25
 
24
- With these two files, a scenario can be solved in only a few steps. We will use the case
26
+ With these two files, a scenario can be created and solved in only a few steps. We will use the case
25
27
  of Jack and Jill provided here as an example:
26
- 1) Download these two files from the repository:
28
+ 1) Download these two files from the GitHub repository:
27
29
  - Case parameter file named
28
30
  [case_jack+jill.toml](https://raw.github.com/mdlacasse/Owl/main/examples/case_jack+jill.toml)
29
31
  in editable *toml* format.
30
32
  - Wages and contributions file named
31
33
  [jack+jill.xlsx](https://raw.github.com/mdlacasse/Owl/main/examples/jack+jill.xlsx)
32
34
  in Excel format.
33
- 1) Navigate to the ***Basic Info*** page and drag and drop the case parameter file
35
+ 1) Navigate to the **Basic Info** page and drag and drop the case parameter file
34
36
  you just downloaded (*case_jack+jill.toml*).
35
- 1) Navigate to the ***Wages and Contributions*** page and
37
+ 1) Navigate to the **Wages and Contributions** page and
36
38
  drag and drop the wages and contributions file you downloaded (*jack+jill.xlsx*).
37
- 1) Move to the ***Case Results*** page and click on the `Run single case` button.
39
+ 1) Move to the **Case Results** page and click on the `Run single case` button.
38
40
 
39
41
  Congratulations! :balloon: You just ran your first case. You can now explore each page and
40
42
  experiment with different parameters.
41
43
 
42
44
  For creating your own cases, you can start
43
- from scratch by selecting `New Case...` in the selection box while on the ***Basic Info*** page,
45
+ from scratch by selecting `New Case...` in the selection box while on the **Basic Info** page,
44
46
  and fill in the information needed on each page of the `Case Setup` section.
45
47
  Once a case has been fully parameterized and successfully optimized,
46
48
  it can then be saved as a parameter file
@@ -52,5 +54,5 @@ the `Duplicate case` button, and then edit its values to fit your situation.
52
54
  Multiple cases can coexist and can be called using the `Select case` box
53
55
  at the bottom of the margin.
54
56
 
55
- More information can be found on the :material/help: ***Documentation*** page located in the **Resources** section.
57
+ More information can be found on the :material/help: **Documentation** page located in the **Resources** section.
56
58
  ''')
@@ -84,14 +84,6 @@ def prepareRun(plan):
84
84
  st.error('Failed setting social security: %s' % e)
85
85
  return
86
86
 
87
- previousMAGI = kz.getPreviousMAGI()
88
- if previousMAGI[0] > 0 or previousMAGI[1] > 0:
89
- try:
90
- plan.setPreviousMAGI(previousMAGI)
91
- except Exception as e:
92
- st.error('Failed setting previous MAGI: %s' % e)
93
- return
94
-
95
87
  if ni == 2:
96
88
  benfrac = [kz.getKey('benf0'), kz.getKey('benf1'), kz.getKey('benf2')]
97
89
  try:
@@ -111,6 +103,8 @@ def prepareRun(plan):
111
103
  plan.setLongTermCapitalTaxRate(kz.getKey('gainTx'))
112
104
  plan.setDividendRate(kz.getKey('divRate'))
113
105
 
106
+ setInterpolationMethod()
107
+ setAllocationRatios()
114
108
  setRates()
115
109
  setContributions()
116
110
 
@@ -605,6 +599,10 @@ def genDic(plan):
605
599
  if key in optionKeys:
606
600
  dic[key] = plan.solverOptions[key]
607
601
 
602
+ if 'previousMAGIs' in optionKeys:
603
+ dic['MAGI0'] = plan.solverOptions['previousMAGIs'][0]
604
+ dic['MAGI1'] = plan.solverOptions['previousMAGIs'][1]
605
+
608
606
  if plan.objective == 'maxSpending':
609
607
  dic['objective'] = 'Net spending'
610
608
  else:
@@ -619,7 +617,7 @@ def genDic(plan):
619
617
 
620
618
  # Initialize in both cases.
621
619
  for k1 in range(plan.N_k):
622
- dic['fxRate'+str(k1)] = 100*plan.rateValues[k1]
620
+ dic['fxRate'+str(k1)] = 100 * plan.rateValues[k1]
623
621
 
624
622
  if plan.rateMethod in ['historical average', 'histochastic', 'historical']:
625
623
  dic['yfrm'] = plan.rateFrm
@@ -632,8 +630,8 @@ def genDic(plan):
632
630
  if plan.rateMethod in ['stochastic', 'histochastic']:
633
631
  qq = 1
634
632
  for k1 in range(plan.N_k):
635
- dic['mean'+str(k1)] = 100*plan.rateValues[k1]
636
- dic['stdev'+str(k1)] = 100*plan.rateStdev[k1]
633
+ dic['mean'+str(k1)] = 100 * plan.rateValues[k1]
634
+ dic['stdev'+str(k1)] = 100 * plan.rateStdev[k1]
637
635
  for k2 in range(k1+1, plan.N_k):
638
636
  dic['corr'+str(qq)] = plan.rateCorr[k1, k2]
639
637
  qq += 1
@@ -7,4 +7,4 @@ scipy
7
7
  streamlit
8
8
  toml
9
9
  # --extra-index-url https://test.pypi.org/simple
10
- owlplanner >= 2025.1.29
10
+ owlplanner >= 2025.02.01
@@ -186,8 +186,6 @@ def duplicateCase():
186
186
  ss.cases[dupname]['duplicate'] = True
187
187
  refreshCase(ss.cases[dupname])
188
188
  ss.currentCase = dupname
189
- # resetTimeLists()
190
- # print(dupname, '->', ss.cases[dupname])
191
189
 
192
190
 
193
191
  def createCaseFromFile(confile):
@@ -336,6 +334,10 @@ def getSolveParameters():
336
334
  if getKey('readRothX'):
337
335
  options['maxRothConversion'] = 'file'
338
336
 
337
+ previousMAGIs = getPreviousMAGIs()
338
+ if previousMAGIs[0] > 0 or previousMAGIs[1] > 0:
339
+ options['previousMAGIs'] = previousMAGIs
340
+
339
341
  return objective, options
340
342
 
341
343
 
@@ -385,14 +387,14 @@ def getAccountAllocationRatios():
385
387
  return accounts
386
388
 
387
389
 
388
- def getPreviousMAGI():
389
- backMAGI = [0, 0]
390
+ def getPreviousMAGIs():
391
+ backMAGIs = [0, 0]
390
392
  for ii in range(2):
391
393
  val = getKey('MAGI'+str(ii))
392
394
  if val:
393
- backMAGI[ii] = val
395
+ backMAGIs[ii] = val
394
396
 
395
- return backMAGI
397
+ return backMAGIs
396
398
 
397
399
 
398
400
  def getFixedIncome(ni, what):
@@ -437,11 +439,11 @@ def getText(text, nkey, disabled=False, callback=setpull, placeholder=None):
437
439
  placeholder=placeholder)
438
440
 
439
441
 
440
- def getRadio(text, choices, nkey, callback=setpull, help=None):
442
+ def getRadio(text, choices, nkey, callback=setpull, disabled=False, help=None):
441
443
  return st.radio(text, choices,
442
444
  index=choices.index(getKey(nkey)),
443
445
  on_change=callback, args=[nkey], key='_'+nkey,
444
- horizontal=True, help=help)
446
+ disabled=disabled, horizontal=True, help=help)
445
447
 
446
448
 
447
449
  def getToggle(text, nkey, callback=setpull, disabled=False, help=None):
@@ -454,6 +456,7 @@ def orangeDivider():
454
456
 
455
457
 
456
458
  def caseHeader(txt):
457
- st.html('<div style="text-align: right;color: orange;font-style: italic;">%s</div>' % currentCaseName())
459
+ # st.html('<div style="text-align: right;color: orange;font-style: italic;">%s</div>' % currentCaseName())
460
+ st.html('<div style="text-align: left;color: orange;font-style: italic;">%s</div>' % currentCaseName())
458
461
  st.write('## ' + txt)
459
462
  orangeDivider()
Binary file
Binary file
@@ -1 +0,0 @@
1
- __version__ = "2025.1.29"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes