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.
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/PKG-INFO +1 -1
- owlplanner-2025.2.1/examples/jack+jill.xlsx +0 -0
- owlplanner-2025.2.1/examples/joe.xlsx +0 -0
- owlplanner-2025.2.1/examples/john+sally.xlsx +0 -0
- owlplanner-2025.2.1/examples/template.xlsx +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/pyproject.toml +1 -1
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/plan.py +43 -40
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/timelists.py +8 -8
- owlplanner-2025.2.1/src/owlplanner/version.py +1 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/About_Owl.py +3 -3
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Case_Results.py +1 -1
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Documentation.py +14 -8
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Optimization_Parameters.py +13 -8
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Quick_Start.py +13 -11
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/owlbridge.py +9 -11
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/requirements.txt +1 -1
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/sskeys.py +12 -9
- owlplanner-2025.1.29/examples/jack+jill.xlsx +0 -0
- owlplanner-2025.1.29/examples/joe.xlsx +0 -0
- owlplanner-2025.1.29/examples/john+sally.xlsx +0 -0
- owlplanner-2025.1.29/examples/template.xlsx +0 -0
- owlplanner-2025.1.29/src/owlplanner/version.py +0 -1
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/.devcontainer/devcontainer.json +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/.flake8 +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/.github/workflows/github-actions-runtests.yml +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/.gitignore +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/INSTALL.md +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/LICENSE +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/README.md +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/AD-taxDef.png +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/AD-taxFree.png +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/AD-taxable.png +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/Hist_Bequest.png +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/Hist_Spending.png +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/MC-tutorial2a.png +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/MC-tutorial2b.png +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/OwlUI.png +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/allocations.png +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/owl.png +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/profile.png +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/ratesCorrelations.png +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/ratesPlot.png +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/savingsPlot.png +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/sourcesPlot.png +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/spendingPlot.png +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/taxIncomePlot.png +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/images/taxesPlot.png +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/owl.pdf +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/docs/owl.tex +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/examples/case_jack+jill.toml +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/examples/case_joe.toml +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/examples/case_john+sally.toml +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/examples/case_kim+sam-bequest.toml +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/examples/case_kim+sam-spending.toml +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/notebooks/john+sally.ipynb +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/notebooks/kim+sam.ipynb +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/notebooks/template.ipynb +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/notebooks/tutorial_1.ipynb +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/notebooks/tutorial_2.ipynb +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/notebooks/tutorial_3.ipynb +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/owlplanner.cmd +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/requirements.txt +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/__init__.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/abcapi.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/config.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/data/__init__.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/data/rates.csv +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/logging.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/progress.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/rates.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/tax2025.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/src/owlplanner/utils.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/tests/test_logger.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/tests/test_regressions.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/tests/test_repro.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/tests/test_toml_cases.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/tests/test_units.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Asset_Allocation.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Assets.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Basic_Info.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Case_Summary.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Case_Worksheets.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Fixed_Income.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Historical_Range.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Logs.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Monte_Carlo.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/README.md +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Rates_Selection.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Settings.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/Wages_And_Contributions.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/main.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/plots.py +0 -0
- {owlplanner-2025.1.29 → owlplanner-2025.2.1}/ui/progress.py +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
873
|
-
'ctrb
|
|
874
|
-
'
|
|
875
|
-
'ctrb
|
|
876
|
-
'
|
|
877
|
-
'Roth
|
|
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
|
|
904
|
-
self.kappa_ijn[i, 1, :h] = self.timeLists[iname]['ctrb
|
|
905
|
-
self.kappa_ijn[i, 2, :h] = self.timeLists[iname]['
|
|
906
|
-
self.kappa_ijn[i, 1, :h] += self.timeLists[iname]['ctrb
|
|
907
|
-
self.kappa_ijn[i, 2, :h] += self.timeLists[iname]['
|
|
908
|
-
self.myRothX_in[i, :h] = self.timeLists[iname]['Roth
|
|
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
|
|
961
|
-
'
|
|
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
|
-
'
|
|
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
|
|
2006
|
-
sources['
|
|
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['
|
|
2010
|
-
sources['
|
|
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
|
|
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
|
|
2737
|
-
'RMDs': self.sources_in['
|
|
2738
|
-
'+distributions': self.sources_in['dist'],
|
|
2739
|
-
'Roth
|
|
2740
|
-
'tax-free wdrwl': self.sources_in['
|
|
2741
|
-
'big-ticket items': self.sources_in['
|
|
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
|
|
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
|
|
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
|
|
27
|
-
'ctrb
|
|
28
|
-
'
|
|
29
|
-
'ctrb
|
|
30
|
-
'
|
|
31
|
-
'Roth
|
|
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,
|
|
42
|
-
|
|
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
|
|
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]
|
|
@@ -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
|
|
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
|
|
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
|
|
356
|
+
Messages coming from the underlying Owl calculation engine are displayed on this page.
|
|
352
357
|
|
|
353
358
|
#### Settings
|
|
354
|
-
This page contains
|
|
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*
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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("
|
|
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
|
|
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
|
-
|
|
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
|
|
20
|
-
time table
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
@@ -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
|
|
389
|
-
|
|
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
|
-
|
|
395
|
+
backMAGIs[ii] = val
|
|
394
396
|
|
|
395
|
-
return
|
|
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
|
|
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
|
|
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
|