owlplanner 2025.1.28__tar.gz → 2025.2__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.28 → owlplanner-2025.2}/PKG-INFO +1 -1
- {owlplanner-2025.1.28 → owlplanner-2025.2}/pyproject.toml +1 -1
- {owlplanner-2025.1.28 → owlplanner-2025.2}/src/owlplanner/plan.py +16 -31
- {owlplanner-2025.1.28 → owlplanner-2025.2}/src/owlplanner/tax2025.py +5 -5
- owlplanner-2025.2/src/owlplanner/version.py +1 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/Case_Worksheets.py +1 -2
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/Documentation.py +3 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/Optimization_Parameters.py +19 -6
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/Rates_Selection.py +3 -3
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/owlbridge.py +35 -151
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/requirements.txt +1 -1
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/sskeys.py +115 -6
- owlplanner-2025.1.28/src/owlplanner/version.py +0 -1
- {owlplanner-2025.1.28 → owlplanner-2025.2}/.devcontainer/devcontainer.json +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/.flake8 +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/.github/workflows/github-actions-runtests.yml +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/.gitignore +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/INSTALL.md +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/LICENSE +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/README.md +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/images/AD-taxDef.png +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/images/AD-taxFree.png +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/images/AD-taxable.png +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/images/Hist_Bequest.png +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/images/Hist_Spending.png +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/images/MC-tutorial2a.png +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/images/MC-tutorial2b.png +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/images/OwlUI.png +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/images/allocations.png +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/images/owl.png +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/images/profile.png +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/images/ratesCorrelations.png +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/images/ratesPlot.png +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/images/savingsPlot.png +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/images/sourcesPlot.png +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/images/spendingPlot.png +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/images/taxIncomePlot.png +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/images/taxesPlot.png +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/owl.pdf +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/docs/owl.tex +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/examples/case_jack+jill.toml +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/examples/case_joe.toml +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/examples/case_john+sally.toml +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/examples/case_kim+sam-bequest.toml +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/examples/case_kim+sam-spending.toml +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/examples/jack+jill.xlsx +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/examples/joe.xlsx +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/examples/john+sally.xlsx +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/examples/template.xlsx +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/notebooks/john+sally.ipynb +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/notebooks/kim+sam.ipynb +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/notebooks/template.ipynb +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/notebooks/tutorial_1.ipynb +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/notebooks/tutorial_2.ipynb +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/notebooks/tutorial_3.ipynb +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/owlplanner.cmd +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/requirements.txt +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/src/owlplanner/__init__.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/src/owlplanner/abcapi.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/src/owlplanner/config.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/src/owlplanner/data/__init__.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/src/owlplanner/data/rates.csv +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/src/owlplanner/logging.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/src/owlplanner/progress.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/src/owlplanner/rates.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/src/owlplanner/timelists.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/src/owlplanner/utils.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/tests/test_logger.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/tests/test_regressions.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/tests/test_repro.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/tests/test_toml_cases.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/tests/test_units.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/About_Owl.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/Asset_Allocation.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/Assets.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/Basic_Info.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/Case_Results.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/Case_Summary.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/Fixed_Income.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/Historical_Range.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/Logs.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/Monte_Carlo.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/Quick_Start.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/README.md +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/Settings.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/Wages_And_Contributions.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/main.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/plots.py +0 -0
- {owlplanner-2025.1.28 → owlplanner-2025.2}/ui/progress.py +0 -0
|
@@ -295,6 +295,9 @@ class Plan(object):
|
|
|
295
295
|
self.myRothX_in = np.zeros((self.N_i, self.N_n))
|
|
296
296
|
self.kappa_ijn = np.zeros((self.N_i, self.N_j, self.N_n))
|
|
297
297
|
|
|
298
|
+
# Previous 2 years for Medicare.
|
|
299
|
+
self.prevMAGI = np.zeros((2))
|
|
300
|
+
|
|
298
301
|
# Scenario starts at the beginning of this year and ends at the end of the last year.
|
|
299
302
|
self.mylog.vprint('Preparing scenario of %d years for %d individual%s.'
|
|
300
303
|
% (self.N_n, self.N_i, ['', 's'][self.N_i - 1]))
|
|
@@ -403,8 +406,6 @@ class Plan(object):
|
|
|
403
406
|
self.mylog.vprint('Renaming plan %s -> %s.' % (self._name, newname))
|
|
404
407
|
self._name = newname
|
|
405
408
|
|
|
406
|
-
return None
|
|
407
|
-
|
|
408
409
|
def setSpousalDepositFraction(self, eta):
|
|
409
410
|
"""
|
|
410
411
|
Set spousal deposit and withdrawal fraction. Default 0.5.
|
|
@@ -424,8 +425,6 @@ class Plan(object):
|
|
|
424
425
|
self.mylog.vprint('\t%s: %.1f, %s: %.1f' % (self.inames[0], (1 - eta), self.inames[1], eta))
|
|
425
426
|
self.eta = eta
|
|
426
427
|
|
|
427
|
-
return None
|
|
428
|
-
|
|
429
428
|
def setDefaultPlots(self, value):
|
|
430
429
|
"""
|
|
431
430
|
Set plots between nominal values or today's $.
|
|
@@ -434,8 +433,6 @@ class Plan(object):
|
|
|
434
433
|
self.defaultPlots = self._checkValue(value)
|
|
435
434
|
self.mylog.vprint('Setting plots default value to %s.' % value)
|
|
436
435
|
|
|
437
|
-
return None
|
|
438
|
-
|
|
439
436
|
def setDividendRate(self, mu):
|
|
440
437
|
"""
|
|
441
438
|
Set dividend rate on equities. Rate is in percent. Default 2%.
|
|
@@ -446,8 +443,6 @@ class Plan(object):
|
|
|
446
443
|
self.mu = mu
|
|
447
444
|
self.caseStatus = 'modified'
|
|
448
445
|
|
|
449
|
-
return None
|
|
450
|
-
|
|
451
446
|
def setLongTermCapitalTaxRate(self, psi):
|
|
452
447
|
"""
|
|
453
448
|
Set long-term income tax rate. Rate is in percent. Default 15%.
|
|
@@ -458,8 +453,6 @@ class Plan(object):
|
|
|
458
453
|
self.psi = psi
|
|
459
454
|
self.caseStatus = 'modified'
|
|
460
455
|
|
|
461
|
-
return None
|
|
462
|
-
|
|
463
456
|
def setBeneficiaryFractions(self, phi):
|
|
464
457
|
"""
|
|
465
458
|
Set fractions of savings accounts that is left to surviving spouse.
|
|
@@ -477,8 +470,6 @@ class Plan(object):
|
|
|
477
470
|
self.mylog.vprint('Consider changing spousal deposit fraction for better convergence.')
|
|
478
471
|
self.mylog.vprint('\tRecommended: setSpousalDepositFraction(%d)' % self.i_d)
|
|
479
472
|
|
|
480
|
-
return None
|
|
481
|
-
|
|
482
473
|
def setHeirsTaxRate(self, nu):
|
|
483
474
|
"""
|
|
484
475
|
Set the heirs tax rate on the tax-deferred portion of the estate.
|
|
@@ -490,8 +481,6 @@ class Plan(object):
|
|
|
490
481
|
self.nu = nu
|
|
491
482
|
self.caseStatus = 'modified'
|
|
492
483
|
|
|
493
|
-
return None
|
|
494
|
-
|
|
495
484
|
def setPension(self, amounts, ages, units='k'):
|
|
496
485
|
"""
|
|
497
486
|
Set value of pension for each individual and commencement age.
|
|
@@ -521,8 +510,6 @@ class Plan(object):
|
|
|
521
510
|
self.pensionAges = np.array(ages, dtype=np.int32)
|
|
522
511
|
self.caseStatus = 'modified'
|
|
523
512
|
|
|
524
|
-
return None
|
|
525
|
-
|
|
526
513
|
def setSocialSecurity(self, amounts, ages, units='k'):
|
|
527
514
|
"""
|
|
528
515
|
Set value of social security for each individual and commencement age.
|
|
@@ -560,8 +547,6 @@ class Plan(object):
|
|
|
560
547
|
self.caseStatus = 'modified'
|
|
561
548
|
self._adjustedParameters = False
|
|
562
549
|
|
|
563
|
-
return None
|
|
564
|
-
|
|
565
550
|
def setSpendingProfile(self, profile, percent=60, dip=15, increase=12, delay=0):
|
|
566
551
|
"""
|
|
567
552
|
Generate time series for spending profile. Surviving spouse fraction can be specified
|
|
@@ -589,8 +574,6 @@ class Plan(object):
|
|
|
589
574
|
self.smileDelay = delay
|
|
590
575
|
self.caseStatus = 'modified'
|
|
591
576
|
|
|
592
|
-
return None
|
|
593
|
-
|
|
594
577
|
def setRates(self, method, frm=None, to=None, values=None, stdev=None, corr=None):
|
|
595
578
|
"""
|
|
596
579
|
Generate rates for return and inflation based on the method and
|
|
@@ -632,8 +615,6 @@ class Plan(object):
|
|
|
632
615
|
self._adjustedParameters = False
|
|
633
616
|
self.caseStatus = 'modified'
|
|
634
617
|
|
|
635
|
-
return None
|
|
636
|
-
|
|
637
618
|
def regenRates(self):
|
|
638
619
|
"""
|
|
639
620
|
Regenerate the rates using the arguments specified during last setRates() call.
|
|
@@ -648,8 +629,6 @@ class Plan(object):
|
|
|
648
629
|
corr=self.rateCorr,
|
|
649
630
|
)
|
|
650
631
|
|
|
651
|
-
return None
|
|
652
|
-
|
|
653
632
|
def value(self, amount, year):
|
|
654
633
|
"""
|
|
655
634
|
Return value of amount deflated or inflated at the beginning
|
|
@@ -711,8 +690,6 @@ class Plan(object):
|
|
|
711
690
|
u.d(np.sum(taxable) + 0.7 * np.sum(taxDeferred) + np.sum(taxFree)),
|
|
712
691
|
)
|
|
713
692
|
|
|
714
|
-
return None
|
|
715
|
-
|
|
716
693
|
def setInterpolationMethod(self, method, center=15, width=5):
|
|
717
694
|
"""
|
|
718
695
|
Interpolate assets allocation ratios from initial value (today) to
|
|
@@ -739,8 +716,6 @@ class Plan(object):
|
|
|
739
716
|
|
|
740
717
|
self.mylog.vprint('Asset allocation interpolation method set to %s.' % method)
|
|
741
718
|
|
|
742
|
-
return None
|
|
743
|
-
|
|
744
719
|
def setAllocationRatios(self, allocType, taxable=None, taxDeferred=None, taxFree=None, generic=None):
|
|
745
720
|
"""
|
|
746
721
|
Single function for setting all types of asset allocations.
|
|
@@ -874,8 +849,6 @@ class Plan(object):
|
|
|
874
849
|
|
|
875
850
|
self.mylog.vprint('Interpolating assets allocation ratios using', self.interpMethod, 'method.')
|
|
876
851
|
|
|
877
|
-
return None
|
|
878
|
-
|
|
879
852
|
def readContributions(self, filename):
|
|
880
853
|
"""
|
|
881
854
|
Provide the name of the file containing the financial events
|
|
@@ -1636,6 +1609,7 @@ class Plan(object):
|
|
|
1636
1609
|
'noRothConversions',
|
|
1637
1610
|
'withMedicare',
|
|
1638
1611
|
'solver',
|
|
1612
|
+
'previousMAGIs',
|
|
1639
1613
|
]
|
|
1640
1614
|
# We will modify options if required.
|
|
1641
1615
|
if options is None:
|
|
@@ -1664,6 +1638,17 @@ class Plan(object):
|
|
|
1664
1638
|
if objective == 'maxSpending' and 'bequest' not in myoptions:
|
|
1665
1639
|
self.mylog.vprint('Using bequest of $1.')
|
|
1666
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
|
+
|
|
1667
1652
|
self._adjustParameters()
|
|
1668
1653
|
|
|
1669
1654
|
if 'solver' in options:
|
|
@@ -1897,7 +1882,7 @@ class Plan(object):
|
|
|
1897
1882
|
self.F_tn = self.F_tn.reshape((self.N_t, self.N_n))
|
|
1898
1883
|
MAGI_n = np.sum(self.F_tn, axis=0) + np.array(x[self.C['e']:self.C['F']])
|
|
1899
1884
|
|
|
1900
|
-
self.M_n = tx.mediCosts(self.yobs, self.horizons, MAGI_n, self.gamma_n[:-1], self.N_n)
|
|
1885
|
+
self.M_n = tx.mediCosts(self.yobs, self.horizons, MAGI_n, self.prevMAGI, self.gamma_n[:-1], self.N_n)
|
|
1901
1886
|
|
|
1902
1887
|
return None
|
|
1903
1888
|
|
|
@@ -69,7 +69,7 @@ extra65Deduction_2025 = np.array([2000, 1600])
|
|
|
69
69
|
##############################################################################
|
|
70
70
|
|
|
71
71
|
|
|
72
|
-
def mediCosts(yobs, horizons, magi, gamma_n, Nn):
|
|
72
|
+
def mediCosts(yobs, horizons, magi, prevmagi, gamma_n, Nn):
|
|
73
73
|
"""
|
|
74
74
|
Compute Medicare costs directly.
|
|
75
75
|
"""
|
|
@@ -79,14 +79,14 @@ def mediCosts(yobs, horizons, magi, gamma_n, Nn):
|
|
|
79
79
|
for n in range(Nn):
|
|
80
80
|
for i in range(Ni):
|
|
81
81
|
if thisyear + n - yobs[i] >= 65 and n < horizons[i]:
|
|
82
|
-
#
|
|
82
|
+
# Start with the (indexed) basic Medicare part B premium.
|
|
83
83
|
costs[n] += gamma_n[n] * irmaaFees_2025[0]
|
|
84
84
|
if n < 2:
|
|
85
|
-
|
|
85
|
+
mymagi = prevmagi[n]
|
|
86
86
|
else:
|
|
87
|
-
|
|
87
|
+
mymagi = magi[n - 2]
|
|
88
88
|
for q in range(1, 6):
|
|
89
|
-
if
|
|
89
|
+
if mymagi > gamma_n[n] * irmaaBrackets_2025[Ni - 1][q]:
|
|
90
90
|
costs[n] += gamma_n[n] * irmaaFees_2025[q]
|
|
91
91
|
|
|
92
92
|
return costs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2025.02"
|
|
@@ -14,12 +14,11 @@ else:
|
|
|
14
14
|
else:
|
|
15
15
|
owb.showWorkbook()
|
|
16
16
|
st.divider()
|
|
17
|
-
# if not owb.isCaseUnsolved():
|
|
18
17
|
if kz.caseHasPlan():
|
|
19
18
|
download2 = st.download_button(
|
|
20
19
|
label="Download data as an Excel workbook...",
|
|
21
20
|
data=owb.saveWorkbook(),
|
|
22
21
|
file_name='Workbook_'+kz.getKey('name')+'.xlsx',
|
|
23
22
|
mime='application/vnd.ms-excel',
|
|
24
|
-
disabled=
|
|
23
|
+
disabled=kz.isCaseUnsolved()
|
|
25
24
|
)
|
|
@@ -249,6 +249,9 @@ the column `RothX` found in the
|
|
|
249
249
|
|
|
250
250
|
Calculations of Medicare and IRMAA can be turned on or off. This will typically speed up
|
|
251
251
|
the calculations by a factor of 2 to 3, which can be useful when running Monte Carlo simulations.
|
|
252
|
+
If the age of individuals makes them eligible for Medicare within the next two years,
|
|
253
|
+
additional cells will appear for entering the Modified Adjusted Gross Income (MAGI) for past years.
|
|
254
|
+
These numbers are needed to calculate the Income-Related Monthly Adjusted Amounts (IRMAA).
|
|
252
255
|
|
|
253
256
|
The time profile of the net spending amount
|
|
254
257
|
can be selected to either be *flat* or follow a *smile* shape.
|
|
@@ -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,21 +63,33 @@ else:
|
|
|
62
63
|
"noRothConversions", help=helpmsg)
|
|
63
64
|
|
|
64
65
|
st.divider()
|
|
66
|
+
st.write('##### Medicare')
|
|
65
67
|
kz.initKey('withMedicare', True)
|
|
66
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)
|
|
70
72
|
with col2:
|
|
71
|
-
if
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
if kz.getKey('withMedicare'):
|
|
74
|
+
helpmsg = "MAGI in nominal $k for that previous year."
|
|
75
|
+
years = owb.backYearsMAGI()
|
|
76
|
+
for ii in range(2):
|
|
77
|
+
kz.initKey('MAGI'+str(ii), 0)
|
|
78
|
+
if years[ii] > 0:
|
|
79
|
+
ret = kz.getNum(f"MAGI for year {years[ii]} ($k)", 'MAGI'+str(ii), help=helpmsg)
|
|
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')
|
|
75
87
|
|
|
76
88
|
st.divider()
|
|
89
|
+
st.write('##### Spending Profile')
|
|
77
90
|
col1, col2, col3 = st.columns(3, gap='medium', vertical_alignment='top')
|
|
78
91
|
with col1:
|
|
79
|
-
ret = kz.getRadio("
|
|
92
|
+
ret = kz.getRadio("Type of profile", profileChoices, 'spendingProfile', callback=owb.setProfile)
|
|
80
93
|
with col2:
|
|
81
94
|
if kz.getKey('status') == 'married':
|
|
82
95
|
helpmsg = 'Percentage of spending required for the surviving spouse.'
|
|
@@ -198,17 +198,17 @@ else:
|
|
|
198
198
|
kz.initKey('divRate', 2)
|
|
199
199
|
helpmsg = 'Average annual dividend return rate on stock portfolio.'
|
|
200
200
|
ret = kz.getNum('Dividends return rate (%)', 'divRate', max_value=100., format='%.2f',
|
|
201
|
-
help=helpmsg,
|
|
201
|
+
help=helpmsg, step=1.)
|
|
202
202
|
|
|
203
203
|
st.write('#### Income taxes')
|
|
204
204
|
col1, col2 = st.columns(2, gap='large', vertical_alignment='top')
|
|
205
205
|
with col1:
|
|
206
206
|
kz.initKey('gainTx', 15)
|
|
207
207
|
ret = kz.getNum('Long-term capital gains tax rate (%)', 'gainTx', max_value=100.,
|
|
208
|
-
|
|
208
|
+
step=1.)
|
|
209
209
|
|
|
210
210
|
with col2:
|
|
211
211
|
kz.initKey('heirsTx', 30)
|
|
212
212
|
helpmsg = 'Marginal tax rate that heirs would have to pay on inherited tax-deferred balance.'
|
|
213
213
|
ret = kz.getNum('Heirs marginal tax rate (%)', 'heirsTx', max_value=100., help=helpmsg,
|
|
214
|
-
|
|
214
|
+
step=1.)
|
|
@@ -59,67 +59,25 @@ def _checkPlan(func):
|
|
|
59
59
|
return wrapper
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
amounts = []
|
|
64
|
-
ages = []
|
|
65
|
-
for i in range(ni):
|
|
66
|
-
amounts.append(kz.getKey(what+'Amt'+str(i)))
|
|
67
|
-
ages.append(kz.getKey(what+'Age'+str(i)))
|
|
68
|
-
|
|
69
|
-
return amounts, ages
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def getAccountBalances(ni):
|
|
73
|
-
bal = [[], [], []]
|
|
74
|
-
accounts = ['txbl', 'txDef', 'txFree']
|
|
75
|
-
for j, acc in enumerate(accounts):
|
|
76
|
-
for i in range(ni):
|
|
77
|
-
bal[j].append(kz.getKey(acc+str(i)))
|
|
78
|
-
|
|
79
|
-
return bal
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def getSolveParameters():
|
|
83
|
-
maximize = kz.getKey('objective')
|
|
84
|
-
if maximize is None:
|
|
85
|
-
return None
|
|
86
|
-
if 'spending' in maximize:
|
|
87
|
-
objective = 'maxSpending'
|
|
88
|
-
else:
|
|
89
|
-
objective = 'maxBequest'
|
|
90
|
-
|
|
91
|
-
options = {}
|
|
92
|
-
optList = ['netSpending', 'maxRothConversion', 'noRothConversions',
|
|
93
|
-
'withMedicare', 'bequest', 'solver']
|
|
94
|
-
for opt in optList:
|
|
95
|
-
val = kz.getKey(opt)
|
|
96
|
-
if val is not None:
|
|
97
|
-
options[opt] = val
|
|
98
|
-
|
|
99
|
-
if kz.getKey('readRothX'):
|
|
100
|
-
options['maxRothConversion'] = 'file'
|
|
101
|
-
|
|
102
|
-
return objective, options
|
|
103
|
-
|
|
104
|
-
|
|
62
|
+
# _checkPlan
|
|
105
63
|
def prepareRun(plan):
|
|
106
64
|
ni = 2 if kz.getKey('status') == 'married' else 1
|
|
107
65
|
|
|
108
|
-
bal = getAccountBalances(ni)
|
|
66
|
+
bal = kz.getAccountBalances(ni)
|
|
109
67
|
try:
|
|
110
68
|
plan.setAccountBalances(taxable=bal[0], taxDeferred=bal[1], taxFree=bal[2])
|
|
111
69
|
except Exception as e:
|
|
112
70
|
st.error('Setting account balances failed: %s' % e)
|
|
113
71
|
return
|
|
114
72
|
|
|
115
|
-
amounts, ages = getFixedIncome(ni, 'p')
|
|
73
|
+
amounts, ages = kz.getFixedIncome(ni, 'p')
|
|
116
74
|
try:
|
|
117
75
|
plan.setPension(amounts, ages)
|
|
118
76
|
except Exception as e:
|
|
119
77
|
st.error('Failed setting pensions: %s' % e)
|
|
120
78
|
return
|
|
121
79
|
|
|
122
|
-
amounts, ages = getFixedIncome(ni, 'ss')
|
|
80
|
+
amounts, ages = kz.getFixedIncome(ni, 'ss')
|
|
123
81
|
try:
|
|
124
82
|
plan.setSocialSecurity(amounts, ages)
|
|
125
83
|
except Exception as e:
|
|
@@ -141,26 +99,19 @@ def prepareRun(plan):
|
|
|
141
99
|
st.error('Failed setting beneficiary fractions: %s' % e)
|
|
142
100
|
return
|
|
143
101
|
|
|
102
|
+
plan.setHeirsTaxRate(kz.getKey('heirsTx'))
|
|
103
|
+
plan.setLongTermCapitalTaxRate(kz.getKey('gainTx'))
|
|
104
|
+
plan.setDividendRate(kz.getKey('divRate'))
|
|
105
|
+
|
|
144
106
|
setRates()
|
|
145
107
|
setContributions()
|
|
146
108
|
|
|
147
109
|
|
|
148
|
-
def isCaseUnsolved():
|
|
149
|
-
if kz.getKey('plan') is None:
|
|
150
|
-
return True
|
|
151
|
-
return kz.getKey('caseStatus') != 'solved'
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
@_checkPlan
|
|
155
|
-
def caseStatus(plan):
|
|
156
|
-
return plan.caseStatus
|
|
157
|
-
|
|
158
|
-
|
|
159
110
|
@_checkPlan
|
|
160
111
|
def runPlan(plan):
|
|
161
112
|
prepareRun(plan)
|
|
162
113
|
|
|
163
|
-
objective, options = getSolveParameters()
|
|
114
|
+
objective, options = kz.getSolveParameters()
|
|
164
115
|
try:
|
|
165
116
|
plan.solve(objective, options=options)
|
|
166
117
|
except Exception as e:
|
|
@@ -183,7 +134,7 @@ def runHistorical(plan):
|
|
|
183
134
|
hyfrm = kz.getKey('hyfrm')
|
|
184
135
|
hyto = kz.getKey('hyto')
|
|
185
136
|
|
|
186
|
-
objective, options = getSolveParameters()
|
|
137
|
+
objective, options = kz.getSolveParameters()
|
|
187
138
|
try:
|
|
188
139
|
mybar = progress.Progress(None)
|
|
189
140
|
fig, summary = plan.runHistoricalRange(objective, options, hyfrm, hyto, figure=True, progcall=mybar)
|
|
@@ -207,7 +158,7 @@ def runMC(plan):
|
|
|
207
158
|
|
|
208
159
|
N = kz.getKey('MC_cases')
|
|
209
160
|
|
|
210
|
-
objective, options = getSolveParameters()
|
|
161
|
+
objective, options = kz.getSolveParameters()
|
|
211
162
|
try:
|
|
212
163
|
mybar = progress.Progress(None)
|
|
213
164
|
fig, summary = plan.runMC(objective, options, N, figure=True, progcall=mybar)
|
|
@@ -386,66 +337,20 @@ def resetContributions(plan):
|
|
|
386
337
|
def setAllocationRatios(plan):
|
|
387
338
|
if kz.getKey('allocType') == 'individual':
|
|
388
339
|
try:
|
|
389
|
-
generic = getIndividualAllocationRatios()
|
|
340
|
+
generic = kz.getIndividualAllocationRatios()
|
|
390
341
|
plan.setAllocationRatios('individual', generic=generic)
|
|
391
342
|
except Exception as e:
|
|
392
343
|
st.error('Setting asset allocations failed: %s' % e)
|
|
393
344
|
return
|
|
394
345
|
elif kz.getKey('allocType') == 'account':
|
|
395
346
|
try:
|
|
396
|
-
acc = getAccountAllocationRatios()
|
|
347
|
+
acc = kz.getAccountAllocationRatios()
|
|
397
348
|
plan.setAllocationRatios('account', taxable=acc[0], taxDeferred=acc[1], taxFree=acc[2])
|
|
398
349
|
except Exception as e:
|
|
399
350
|
st.error('Setting asset allocations failed: %s' % e)
|
|
400
351
|
return
|
|
401
352
|
|
|
402
353
|
|
|
403
|
-
def getIndividualAllocationRatios():
|
|
404
|
-
generic = []
|
|
405
|
-
initial = []
|
|
406
|
-
final = []
|
|
407
|
-
for k1 in range(4):
|
|
408
|
-
initial.append(int(kz.getKey('j3_init%'+str(k1)+'_0')))
|
|
409
|
-
final.append(int(kz.getKey('j3_fin%'+str(k1)+'_0')))
|
|
410
|
-
gen0 = [initial, final]
|
|
411
|
-
generic = [gen0]
|
|
412
|
-
|
|
413
|
-
if kz.getKey('status') == 'married':
|
|
414
|
-
initial = []
|
|
415
|
-
final = []
|
|
416
|
-
for k1 in range(4):
|
|
417
|
-
initial.append(int(kz.getKey('j3_init%'+str(k1)+'_1')))
|
|
418
|
-
final.append(int(kz.getKey('j3_fin%'+str(k1)+'_1')))
|
|
419
|
-
gen1 = [initial, final]
|
|
420
|
-
generic.append(gen1)
|
|
421
|
-
|
|
422
|
-
return generic
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
def getAccountAllocationRatios():
|
|
426
|
-
accounts = [[], [], []]
|
|
427
|
-
for j1 in range(3):
|
|
428
|
-
initial = []
|
|
429
|
-
final = []
|
|
430
|
-
for k1 in range(4):
|
|
431
|
-
initial.append(int(kz.getKey(f'j{j1}_init%'+str(k1)+'_0')))
|
|
432
|
-
final.append(int(kz.getKey(f'j{j1}_fin%'+str(k1)+'_0')))
|
|
433
|
-
tmp = [initial, final]
|
|
434
|
-
accounts[j1].append(tmp)
|
|
435
|
-
|
|
436
|
-
if kz.getKey('status') == 'married':
|
|
437
|
-
for j1 in range(3):
|
|
438
|
-
initial = []
|
|
439
|
-
final = []
|
|
440
|
-
for k1 in range(4):
|
|
441
|
-
initial.append(int(kz.getKey(f'j{j1}_init%'+str(k1)+'_1')))
|
|
442
|
-
final.append(int(kz.getKey(f'j{j1}_fin%'+str(k1)+'_1')))
|
|
443
|
-
tmp = [initial, final]
|
|
444
|
-
accounts[j1].append(tmp)
|
|
445
|
-
|
|
446
|
-
return accounts
|
|
447
|
-
|
|
448
|
-
|
|
449
354
|
@_checkPlan
|
|
450
355
|
def plotSingleResults(plan):
|
|
451
356
|
c = 0
|
|
@@ -510,24 +415,6 @@ def setProfile(plan, key, pull=True):
|
|
|
510
415
|
plan.setSpendingProfile(profile, survivor, dip, increase, delay)
|
|
511
416
|
|
|
512
417
|
|
|
513
|
-
@_checkPlan
|
|
514
|
-
def setHeirsTaxRate(plan, key):
|
|
515
|
-
val = kz.setpull(key)
|
|
516
|
-
plan.setHeirsTaxRate(val)
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
@_checkPlan
|
|
520
|
-
def setLongTermCapitalTaxRate(plan, key):
|
|
521
|
-
val = kz.setpull(key)
|
|
522
|
-
plan.setLongTermCapitalTaxRate(val)
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
@_checkPlan
|
|
526
|
-
def setDividendRate(plan, key):
|
|
527
|
-
val = kz.setpull(key)
|
|
528
|
-
plan.setDividendRate(val)
|
|
529
|
-
|
|
530
|
-
|
|
531
418
|
@_checkPlan
|
|
532
419
|
def setDefaultPlots(plan, key):
|
|
533
420
|
val = kz.storepull(key)
|
|
@@ -612,7 +499,7 @@ def saveContributions(plan):
|
|
|
612
499
|
@_checkPlan
|
|
613
500
|
def saveCaseFile(plan):
|
|
614
501
|
stringBuffer = StringIO()
|
|
615
|
-
if getSolveParameters() is None:
|
|
502
|
+
if kz.getSolveParameters() is None:
|
|
616
503
|
return ''
|
|
617
504
|
plan.saveConfig(stringBuffer)
|
|
618
505
|
encoded_data = stringBuffer.getvalue().encode('utf-8')
|
|
@@ -635,23 +522,7 @@ def createCaseFromFile(file):
|
|
|
635
522
|
|
|
636
523
|
return name, mydic
|
|
637
524
|
|
|
638
|
-
|
|
639
|
-
# 'timeList', 'plots', 'interpMethod', 'interpCenter', 'interpWidth',
|
|
640
|
-
# 'objective', 'withMedicare', 'bequest', 'netSpending',
|
|
641
|
-
# 'noRothConversions', 'maxRothConversion',
|
|
642
|
-
# 'rateType', 'fixedType', 'varyingType', 'yfrm', 'yto',
|
|
643
|
-
# 'divRate', 'heirsTx', 'gainTx', 'spendingProfile', 'survivor',
|
|
644
|
-
# 'surplusFraction', ]
|
|
645
|
-
# keynamesJ = ['benf', ]
|
|
646
|
-
# keynamesK = ['fxRate', 'mean', 'stdev']
|
|
647
|
-
# keynamesI = ['iname', 'yob', 'life', 'txbl', 'txDef', 'txFree',
|
|
648
|
-
# 'ssAge', 'ssAmt', 'pAge', 'pAmt', 'df',
|
|
649
|
-
# 'jX_init%0_', 'jX_init%1_', 'jX_init%2_', 'jX_init%3_',
|
|
650
|
-
# 'jX_fin%0_', 'jX_fin%1_', 'jX_fin%2_', 'jX_fin%3_']
|
|
651
|
-
# keynames6 = ['corr']
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
# @_checkPlan
|
|
525
|
+
|
|
655
526
|
def genDic(plan):
|
|
656
527
|
accName = ['txbl', 'txDef', 'txFree']
|
|
657
528
|
dic = {}
|
|
@@ -726,6 +597,10 @@ def genDic(plan):
|
|
|
726
597
|
if key in optionKeys:
|
|
727
598
|
dic[key] = plan.solverOptions[key]
|
|
728
599
|
|
|
600
|
+
if 'previousMAGIs' in optionKeys:
|
|
601
|
+
dic['MAGI0'] = plan.solverOptions['previousMAGIs'][0]
|
|
602
|
+
dic['MAGI1'] = plan.solverOptions['previousMAGIs'][1]
|
|
603
|
+
|
|
729
604
|
if plan.objective == 'maxSpending':
|
|
730
605
|
dic['objective'] = 'Net spending'
|
|
731
606
|
else:
|
|
@@ -740,21 +615,21 @@ def genDic(plan):
|
|
|
740
615
|
|
|
741
616
|
# Initialize in both cases.
|
|
742
617
|
for k1 in range(plan.N_k):
|
|
743
|
-
dic['fxRate'+str(k1)] = 100*plan.rateValues[k1]
|
|
618
|
+
dic['fxRate'+str(k1)] = 100 * plan.rateValues[k1]
|
|
744
619
|
|
|
745
620
|
if plan.rateMethod in ['historical average', 'histochastic', 'historical']:
|
|
746
621
|
dic['yfrm'] = plan.rateFrm
|
|
747
622
|
dic['yto'] = plan.rateTo
|
|
748
623
|
else:
|
|
749
624
|
dic['yfrm'] = FROM
|
|
750
|
-
# Rates
|
|
751
|
-
dic['yto'] = date.today().year -
|
|
625
|
+
# Rates availability are trailing by 1 year.
|
|
626
|
+
dic['yto'] = date.today().year - 1
|
|
752
627
|
|
|
753
628
|
if plan.rateMethod in ['stochastic', 'histochastic']:
|
|
754
629
|
qq = 1
|
|
755
630
|
for k1 in range(plan.N_k):
|
|
756
|
-
dic['mean'+str(k1)] = 100*plan.rateValues[k1]
|
|
757
|
-
dic['stdev'+str(k1)] = 100*plan.rateStdev[k1]
|
|
631
|
+
dic['mean'+str(k1)] = 100 * plan.rateValues[k1]
|
|
632
|
+
dic['stdev'+str(k1)] = 100 * plan.rateStdev[k1]
|
|
758
633
|
for k2 in range(k1+1, plan.N_k):
|
|
759
634
|
dic['corr'+str(qq)] = plan.rateCorr[k1, k2]
|
|
760
635
|
qq += 1
|
|
@@ -762,8 +637,17 @@ def genDic(plan):
|
|
|
762
637
|
return plan._name, dic
|
|
763
638
|
|
|
764
639
|
|
|
765
|
-
|
|
766
|
-
|
|
640
|
+
@_checkPlan
|
|
641
|
+
def backYearsMAGI(plan):
|
|
642
|
+
thisyear = date.today().year
|
|
643
|
+
backyears = [0, 0]
|
|
644
|
+
for i in range(plan.N_i):
|
|
645
|
+
if thisyear - plan.yobs[i] >= 65:
|
|
646
|
+
backyears[0] = thisyear - 2
|
|
647
|
+
elif thisyear - plan.yobs[i] >= 64:
|
|
648
|
+
backyears[1] = thisyear - 1
|
|
649
|
+
|
|
650
|
+
return backyears
|
|
767
651
|
|
|
768
652
|
|
|
769
653
|
def version():
|
|
@@ -98,10 +98,9 @@ def updateContributions():
|
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
def switchToCase(key):
|
|
101
|
-
import owlbridge as owb
|
|
102
101
|
# Catch case where switch happens while editing W&W tables.
|
|
103
102
|
if getGlobalKey('currentPageName') == 'Wages And Contributions':
|
|
104
|
-
|
|
103
|
+
updateContributions()
|
|
105
104
|
ss.currentCase = ss['_'+key]
|
|
106
105
|
|
|
107
106
|
|
|
@@ -110,6 +109,12 @@ def isIncomplete():
|
|
|
110
109
|
or (getKey('status') == 'married' and getKey('iname1') in [None, '']))
|
|
111
110
|
|
|
112
111
|
|
|
112
|
+
def isCaseUnsolved():
|
|
113
|
+
if caseHasNoPlan():
|
|
114
|
+
return True
|
|
115
|
+
return getKey('caseStatus') != 'solved'
|
|
116
|
+
|
|
117
|
+
|
|
113
118
|
def caseHasNoPlan():
|
|
114
119
|
return getKey('plan') is None
|
|
115
120
|
|
|
@@ -214,7 +219,7 @@ def createNewCase(case):
|
|
|
214
219
|
st.error("Case name '%s' already exists." % casename)
|
|
215
220
|
return
|
|
216
221
|
|
|
217
|
-
ss.cases[casename] = {'name': casename, 'caseStatus': '', 'summary': '', 'logs': None}
|
|
222
|
+
ss.cases[casename] = {'name': casename, 'caseStatus': 'unknown', 'summary': '', 'logs': None}
|
|
218
223
|
setCurrentCase(ss._newcase)
|
|
219
224
|
|
|
220
225
|
|
|
@@ -301,6 +306,109 @@ def getDict(key=ss.currentCase):
|
|
|
301
306
|
return ss.cases[key]
|
|
302
307
|
|
|
303
308
|
|
|
309
|
+
def getAccountBalances(ni):
|
|
310
|
+
bal = [[], [], []]
|
|
311
|
+
accounts = ['txbl', 'txDef', 'txFree']
|
|
312
|
+
for j, acc in enumerate(accounts):
|
|
313
|
+
for i in range(ni):
|
|
314
|
+
bal[j].append(getKey(acc+str(i)))
|
|
315
|
+
|
|
316
|
+
return bal
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def getSolveParameters():
|
|
320
|
+
maximize = getKey('objective')
|
|
321
|
+
if maximize is None:
|
|
322
|
+
return None
|
|
323
|
+
if 'spending' in maximize:
|
|
324
|
+
objective = 'maxSpending'
|
|
325
|
+
else:
|
|
326
|
+
objective = 'maxBequest'
|
|
327
|
+
|
|
328
|
+
options = {}
|
|
329
|
+
optList = ['netSpending', 'maxRothConversion', 'noRothConversions',
|
|
330
|
+
'withMedicare', 'bequest', 'solver']
|
|
331
|
+
for opt in optList:
|
|
332
|
+
val = getKey(opt)
|
|
333
|
+
if val is not None:
|
|
334
|
+
options[opt] = val
|
|
335
|
+
|
|
336
|
+
if getKey('readRothX'):
|
|
337
|
+
options['maxRothConversion'] = 'file'
|
|
338
|
+
|
|
339
|
+
previousMAGIs = getPreviousMAGIs()
|
|
340
|
+
if previousMAGIs[0] > 0 or previousMAGIs[1] > 0:
|
|
341
|
+
options['previousMAGIs'] = previousMAGIs
|
|
342
|
+
|
|
343
|
+
return objective, options
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def getIndividualAllocationRatios():
|
|
347
|
+
generic = []
|
|
348
|
+
initial = []
|
|
349
|
+
final = []
|
|
350
|
+
for k1 in range(4):
|
|
351
|
+
initial.append(int(getKey('j3_init%'+str(k1)+'_0')))
|
|
352
|
+
final.append(int(getKey('j3_fin%'+str(k1)+'_0')))
|
|
353
|
+
gen0 = [initial, final]
|
|
354
|
+
generic = [gen0]
|
|
355
|
+
|
|
356
|
+
if getKey('status') == 'married':
|
|
357
|
+
initial = []
|
|
358
|
+
final = []
|
|
359
|
+
for k1 in range(4):
|
|
360
|
+
initial.append(int(getKey('j3_init%'+str(k1)+'_1')))
|
|
361
|
+
final.append(int(getKey('j3_fin%'+str(k1)+'_1')))
|
|
362
|
+
gen1 = [initial, final]
|
|
363
|
+
generic.append(gen1)
|
|
364
|
+
|
|
365
|
+
return generic
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def getAccountAllocationRatios():
|
|
369
|
+
accounts = [[], [], []]
|
|
370
|
+
for j1 in range(3):
|
|
371
|
+
initial = []
|
|
372
|
+
final = []
|
|
373
|
+
for k1 in range(4):
|
|
374
|
+
initial.append(int(getKey(f'j{j1}_init%'+str(k1)+'_0')))
|
|
375
|
+
final.append(int(getKey(f'j{j1}_fin%'+str(k1)+'_0')))
|
|
376
|
+
tmp = [initial, final]
|
|
377
|
+
accounts[j1].append(tmp)
|
|
378
|
+
|
|
379
|
+
if getKey('status') == 'married':
|
|
380
|
+
for j1 in range(3):
|
|
381
|
+
initial = []
|
|
382
|
+
final = []
|
|
383
|
+
for k1 in range(4):
|
|
384
|
+
initial.append(int(getKey(f'j{j1}_init%'+str(k1)+'_1')))
|
|
385
|
+
final.append(int(getKey(f'j{j1}_fin%'+str(k1)+'_1')))
|
|
386
|
+
tmp = [initial, final]
|
|
387
|
+
accounts[j1].append(tmp)
|
|
388
|
+
|
|
389
|
+
return accounts
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def getPreviousMAGIs():
|
|
393
|
+
backMAGIs = [0, 0]
|
|
394
|
+
for ii in range(2):
|
|
395
|
+
val = getKey('MAGI'+str(ii))
|
|
396
|
+
if val:
|
|
397
|
+
backMAGIs[ii] = val
|
|
398
|
+
|
|
399
|
+
return backMAGIs
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def getFixedIncome(ni, what):
|
|
403
|
+
amounts = []
|
|
404
|
+
ages = []
|
|
405
|
+
for i in range(ni):
|
|
406
|
+
amounts.append(getKey(what+'Amt'+str(i)))
|
|
407
|
+
ages.append(getKey(what+'Age'+str(i)))
|
|
408
|
+
|
|
409
|
+
return amounts, ages
|
|
410
|
+
|
|
411
|
+
|
|
304
412
|
def getIntNum(text, nkey, disabled=False, callback=setpull, step=1, help=None, min_value=0, max_value=None):
|
|
305
413
|
return st.number_input(text,
|
|
306
414
|
value=int(getKey(nkey)),
|
|
@@ -333,11 +441,11 @@ def getText(text, nkey, disabled=False, callback=setpull, placeholder=None):
|
|
|
333
441
|
placeholder=placeholder)
|
|
334
442
|
|
|
335
443
|
|
|
336
|
-
def getRadio(text, choices, nkey, callback=setpull, help=None):
|
|
444
|
+
def getRadio(text, choices, nkey, callback=setpull, disabled=False, help=None):
|
|
337
445
|
return st.radio(text, choices,
|
|
338
446
|
index=choices.index(getKey(nkey)),
|
|
339
447
|
on_change=callback, args=[nkey], key='_'+nkey,
|
|
340
|
-
horizontal=True, help=help)
|
|
448
|
+
disabled=disabled, horizontal=True, help=help)
|
|
341
449
|
|
|
342
450
|
|
|
343
451
|
def getToggle(text, nkey, callback=setpull, disabled=False, help=None):
|
|
@@ -350,6 +458,7 @@ def orangeDivider():
|
|
|
350
458
|
|
|
351
459
|
|
|
352
460
|
def caseHeader(txt):
|
|
353
|
-
st.html('<div style="text-align: right;color: orange;font-style: italic;">%s</div>' % currentCaseName())
|
|
461
|
+
# st.html('<div style="text-align: right;color: orange;font-style: italic;">%s</div>' % currentCaseName())
|
|
462
|
+
st.html('<div style="text-align: left;color: orange;font-style: italic;">%s</div>' % currentCaseName())
|
|
354
463
|
st.write('## ' + txt)
|
|
355
464
|
orangeDivider()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2025.1.28"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|