owlplanner 2025.1.27__tar.gz → 2025.1.29__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 (89) hide show
  1. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/PKG-INFO +1 -1
  2. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/pyproject.toml +1 -1
  3. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/src/owlplanner/plan.py +14 -31
  4. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/src/owlplanner/rates.py +5 -5
  5. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/src/owlplanner/tax2025.py +5 -5
  6. owlplanner-2025.1.29/src/owlplanner/version.py +1 -0
  7. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/Case_Worksheets.py +1 -2
  8. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/Documentation.py +4 -0
  9. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/Optimization_Parameters.py +9 -1
  10. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/Rates_Selection.py +3 -3
  11. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/owlbridge.py +39 -148
  12. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/requirements.txt +1 -1
  13. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/sskeys.py +107 -3
  14. owlplanner-2025.1.27/src/owlplanner/version.py +0 -1
  15. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/.devcontainer/devcontainer.json +0 -0
  16. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/.flake8 +0 -0
  17. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/.github/workflows/github-actions-runtests.yml +0 -0
  18. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/.gitignore +0 -0
  19. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/INSTALL.md +0 -0
  20. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/LICENSE +0 -0
  21. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/README.md +0 -0
  22. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/images/AD-taxDef.png +0 -0
  23. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/images/AD-taxFree.png +0 -0
  24. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/images/AD-taxable.png +0 -0
  25. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/images/Hist_Bequest.png +0 -0
  26. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/images/Hist_Spending.png +0 -0
  27. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/images/MC-tutorial2a.png +0 -0
  28. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/images/MC-tutorial2b.png +0 -0
  29. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/images/OwlUI.png +0 -0
  30. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/images/allocations.png +0 -0
  31. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/images/owl.png +0 -0
  32. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/images/profile.png +0 -0
  33. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/images/ratesCorrelations.png +0 -0
  34. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/images/ratesPlot.png +0 -0
  35. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/images/savingsPlot.png +0 -0
  36. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/images/sourcesPlot.png +0 -0
  37. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/images/spendingPlot.png +0 -0
  38. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/images/taxIncomePlot.png +0 -0
  39. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/images/taxesPlot.png +0 -0
  40. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/owl.pdf +0 -0
  41. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/docs/owl.tex +0 -0
  42. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/examples/case_jack+jill.toml +0 -0
  43. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/examples/case_joe.toml +0 -0
  44. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/examples/case_john+sally.toml +0 -0
  45. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/examples/case_kim+sam-bequest.toml +0 -0
  46. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/examples/case_kim+sam-spending.toml +0 -0
  47. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/examples/jack+jill.xlsx +0 -0
  48. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/examples/joe.xlsx +0 -0
  49. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/examples/john+sally.xlsx +0 -0
  50. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/examples/template.xlsx +0 -0
  51. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/notebooks/john+sally.ipynb +0 -0
  52. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/notebooks/kim+sam.ipynb +0 -0
  53. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/notebooks/template.ipynb +0 -0
  54. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/notebooks/tutorial_1.ipynb +0 -0
  55. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/notebooks/tutorial_2.ipynb +0 -0
  56. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/notebooks/tutorial_3.ipynb +0 -0
  57. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/owlplanner.cmd +0 -0
  58. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/requirements.txt +0 -0
  59. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/src/owlplanner/__init__.py +0 -0
  60. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/src/owlplanner/abcapi.py +0 -0
  61. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/src/owlplanner/config.py +0 -0
  62. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/src/owlplanner/data/__init__.py +0 -0
  63. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/src/owlplanner/data/rates.csv +0 -0
  64. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/src/owlplanner/logging.py +0 -0
  65. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/src/owlplanner/progress.py +0 -0
  66. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/src/owlplanner/timelists.py +0 -0
  67. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/src/owlplanner/utils.py +0 -0
  68. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/tests/test_logger.py +0 -0
  69. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/tests/test_regressions.py +0 -0
  70. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/tests/test_repro.py +0 -0
  71. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/tests/test_toml_cases.py +0 -0
  72. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/tests/test_units.py +0 -0
  73. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/About_Owl.py +0 -0
  74. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/Asset_Allocation.py +0 -0
  75. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/Assets.py +0 -0
  76. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/Basic_Info.py +0 -0
  77. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/Case_Results.py +0 -0
  78. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/Case_Summary.py +0 -0
  79. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/Fixed_Income.py +0 -0
  80. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/Historical_Range.py +0 -0
  81. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/Logs.py +0 -0
  82. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/Monte_Carlo.py +0 -0
  83. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/Quick_Start.py +0 -0
  84. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/README.md +0 -0
  85. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/Settings.py +0 -0
  86. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/Wages_And_Contributions.py +0 -0
  87. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/main.py +0 -0
  88. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/plots.py +0 -0
  89. {owlplanner-2025.1.27 → owlplanner-2025.1.29}/ui/progress.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owlplanner
3
- Version: 2025.1.27
3
+ Version: 2025.1.29
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "owlplanner"
7
- version = "2025.01.27"
7
+ version = "2025.01.29"
8
8
  authors = [
9
9
  { name="Martin-D. Lacasse", email="martin.d.lacasse@gmail.com" },
10
10
  ]
@@ -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]))
@@ -394,6 +397,16 @@ class Plan(object):
394
397
 
395
398
  return None
396
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
+
397
410
  def rename(self, newname):
398
411
  """
399
412
  Override name of the plan. Plan name is used
@@ -403,8 +416,6 @@ class Plan(object):
403
416
  self.mylog.vprint('Renaming plan %s -> %s.' % (self._name, newname))
404
417
  self._name = newname
405
418
 
406
- return None
407
-
408
419
  def setSpousalDepositFraction(self, eta):
409
420
  """
410
421
  Set spousal deposit and withdrawal fraction. Default 0.5.
@@ -424,8 +435,6 @@ class Plan(object):
424
435
  self.mylog.vprint('\t%s: %.1f, %s: %.1f' % (self.inames[0], (1 - eta), self.inames[1], eta))
425
436
  self.eta = eta
426
437
 
427
- return None
428
-
429
438
  def setDefaultPlots(self, value):
430
439
  """
431
440
  Set plots between nominal values or today's $.
@@ -434,8 +443,6 @@ class Plan(object):
434
443
  self.defaultPlots = self._checkValue(value)
435
444
  self.mylog.vprint('Setting plots default value to %s.' % value)
436
445
 
437
- return None
438
-
439
446
  def setDividendRate(self, mu):
440
447
  """
441
448
  Set dividend rate on equities. Rate is in percent. Default 2%.
@@ -446,8 +453,6 @@ class Plan(object):
446
453
  self.mu = mu
447
454
  self.caseStatus = 'modified'
448
455
 
449
- return None
450
-
451
456
  def setLongTermCapitalTaxRate(self, psi):
452
457
  """
453
458
  Set long-term income tax rate. Rate is in percent. Default 15%.
@@ -458,8 +463,6 @@ class Plan(object):
458
463
  self.psi = psi
459
464
  self.caseStatus = 'modified'
460
465
 
461
- return None
462
-
463
466
  def setBeneficiaryFractions(self, phi):
464
467
  """
465
468
  Set fractions of savings accounts that is left to surviving spouse.
@@ -477,8 +480,6 @@ class Plan(object):
477
480
  self.mylog.vprint('Consider changing spousal deposit fraction for better convergence.')
478
481
  self.mylog.vprint('\tRecommended: setSpousalDepositFraction(%d)' % self.i_d)
479
482
 
480
- return None
481
-
482
483
  def setHeirsTaxRate(self, nu):
483
484
  """
484
485
  Set the heirs tax rate on the tax-deferred portion of the estate.
@@ -490,8 +491,6 @@ class Plan(object):
490
491
  self.nu = nu
491
492
  self.caseStatus = 'modified'
492
493
 
493
- return None
494
-
495
494
  def setPension(self, amounts, ages, units='k'):
496
495
  """
497
496
  Set value of pension for each individual and commencement age.
@@ -521,8 +520,6 @@ class Plan(object):
521
520
  self.pensionAges = np.array(ages, dtype=np.int32)
522
521
  self.caseStatus = 'modified'
523
522
 
524
- return None
525
-
526
523
  def setSocialSecurity(self, amounts, ages, units='k'):
527
524
  """
528
525
  Set value of social security for each individual and commencement age.
@@ -560,8 +557,6 @@ class Plan(object):
560
557
  self.caseStatus = 'modified'
561
558
  self._adjustedParameters = False
562
559
 
563
- return None
564
-
565
560
  def setSpendingProfile(self, profile, percent=60, dip=15, increase=12, delay=0):
566
561
  """
567
562
  Generate time series for spending profile. Surviving spouse fraction can be specified
@@ -589,8 +584,6 @@ class Plan(object):
589
584
  self.smileDelay = delay
590
585
  self.caseStatus = 'modified'
591
586
 
592
- return None
593
-
594
587
  def setRates(self, method, frm=None, to=None, values=None, stdev=None, corr=None):
595
588
  """
596
589
  Generate rates for return and inflation based on the method and
@@ -632,8 +625,6 @@ class Plan(object):
632
625
  self._adjustedParameters = False
633
626
  self.caseStatus = 'modified'
634
627
 
635
- return None
636
-
637
628
  def regenRates(self):
638
629
  """
639
630
  Regenerate the rates using the arguments specified during last setRates() call.
@@ -648,8 +639,6 @@ class Plan(object):
648
639
  corr=self.rateCorr,
649
640
  )
650
641
 
651
- return None
652
-
653
642
  def value(self, amount, year):
654
643
  """
655
644
  Return value of amount deflated or inflated at the beginning
@@ -711,8 +700,6 @@ class Plan(object):
711
700
  u.d(np.sum(taxable) + 0.7 * np.sum(taxDeferred) + np.sum(taxFree)),
712
701
  )
713
702
 
714
- return None
715
-
716
703
  def setInterpolationMethod(self, method, center=15, width=5):
717
704
  """
718
705
  Interpolate assets allocation ratios from initial value (today) to
@@ -739,8 +726,6 @@ class Plan(object):
739
726
 
740
727
  self.mylog.vprint('Asset allocation interpolation method set to %s.' % method)
741
728
 
742
- return None
743
-
744
729
  def setAllocationRatios(self, allocType, taxable=None, taxDeferred=None, taxFree=None, generic=None):
745
730
  """
746
731
  Single function for setting all types of asset allocations.
@@ -874,8 +859,6 @@ class Plan(object):
874
859
 
875
860
  self.mylog.vprint('Interpolating assets allocation ratios using', self.interpMethod, 'method.')
876
861
 
877
- return None
878
-
879
862
  def readContributions(self, filename):
880
863
  """
881
864
  Provide the name of the file containing the financial events
@@ -1897,7 +1880,7 @@ class Plan(object):
1897
1880
  self.F_tn = self.F_tn.reshape((self.N_t, self.N_n))
1898
1881
  MAGI_n = np.sum(self.F_tn, axis=0) + np.array(x[self.C['e']:self.C['F']])
1899
1882
 
1900
- self.M_n = tx.mediCosts(self.yobs, self.horizons, MAGI_n, self.gamma_n[:-1], self.N_n)
1883
+ self.M_n = tx.mediCosts(self.yobs, self.horizons, MAGI_n, self.prevMAGI, self.gamma_n[:-1], self.N_n)
1901
1884
 
1902
1885
  return None
1903
1886
 
@@ -31,8 +31,8 @@ Disclaimer: This program comes with no guarantee. Use at your own risk.
31
31
  ###################################################################
32
32
  import numpy as np
33
33
  import pandas as pd
34
- import pkgutil
35
- import io
34
+ import os
35
+ import sys
36
36
  from datetime import date
37
37
 
38
38
  from owlplanner import logging
@@ -43,10 +43,10 @@ from owlplanner import utils as u
43
43
  FROM = 1928
44
44
  TO = 2024
45
45
 
46
+ where = os.path.dirname(sys.modules['owlplanner'].__file__)
47
+ file = os.path.join(where, 'data/rates.csv')
46
48
  try:
47
- csvBytes = pkgutil.get_data('owlplanner', 'data/rates.csv')
48
- csvFile = io.BytesIO(csvBytes)
49
- df = pd.read_csv(csvFile)
49
+ df = pd.read_csv(file)
50
50
  except Exception as e:
51
51
  raise RuntimeError(f'Could not find rates data file: {e}')
52
52
 
@@ -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
- # The standard Medicare part B premium
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
- nn = n
85
+ mymagi = prevmagi[n]
86
86
  else:
87
- nn = 2
87
+ mymagi = magi[n - 2]
88
88
  for q in range(1, 6):
89
- if magi[n - nn] > gamma_n[n] * irmaaBrackets_2025[Ni - 1][q]:
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.1.29"
@@ -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=owb.isCaseUnsolved()
23
+ disabled=kz.isCaseUnsolved()
25
24
  )
@@ -249,6 +249,10 @@ 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).
255
+ Note that these values are not included in the *case* parameter file when saving.
252
256
 
253
257
  The time profile of the net spending amount
254
258
  can be selected to either be *flat* or follow a *smile* shape.
@@ -63,11 +63,19 @@ else:
63
63
 
64
64
  st.divider()
65
65
  kz.initKey('withMedicare', True)
66
- col1, col2 = st.columns(2, gap='large', vertical_alignment='top')
66
+ col1, col2, col3 = st.columns(3, gap='large', vertical_alignment='top')
67
67
  with col1:
68
68
  helpmsg = "Do or do not perform additional Medicare and IRMAA calculations."
69
69
  ret = kz.getToggle('Medicare and IRMAA calculations', 'withMedicare', help=helpmsg)
70
70
  with col2:
71
+ if kz.getKey('withMedicare'):
72
+ helpmsg = "MAGI in nominal $k for that previous year."
73
+ years = owb.backYearsMAGI()
74
+ for ii in range(2):
75
+ kz.initKey('MAGI'+str(ii), 0)
76
+ if years[ii] > 0:
77
+ ret = kz.getNum(f"MAGI for year {years[ii]} ($k)", 'MAGI'+str(ii), help=helpmsg)
78
+ with col3:
71
79
  if owb.hasMOSEK():
72
80
  choices = ['HiGHS', 'MOSEK']
73
81
  kz.initKey('solver', choices[0])
@@ -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, callback=owb.setDividendRate, step=1.)
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
- callback=owb.setLongTermCapitalTaxRate, step=1.)
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
- callback=owb.setHeirsTaxRate, step=1.)
214
+ step=1.)
@@ -59,73 +59,39 @@ def _checkPlan(func):
59
59
  return wrapper
60
60
 
61
61
 
62
- def getFixedIncome(ni, what):
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:
126
84
  st.error('Failed setting social security: %s' % e)
127
85
  return
128
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
+
129
95
  if ni == 2:
130
96
  benfrac = [kz.getKey('benf0'), kz.getKey('benf1'), kz.getKey('benf2')]
131
97
  try:
@@ -141,26 +107,19 @@ def prepareRun(plan):
141
107
  st.error('Failed setting beneficiary fractions: %s' % e)
142
108
  return
143
109
 
110
+ plan.setHeirsTaxRate(kz.getKey('heirsTx'))
111
+ plan.setLongTermCapitalTaxRate(kz.getKey('gainTx'))
112
+ plan.setDividendRate(kz.getKey('divRate'))
113
+
144
114
  setRates()
145
115
  setContributions()
146
116
 
147
117
 
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
118
  @_checkPlan
160
119
  def runPlan(plan):
161
120
  prepareRun(plan)
162
121
 
163
- objective, options = getSolveParameters()
122
+ objective, options = kz.getSolveParameters()
164
123
  try:
165
124
  plan.solve(objective, options=options)
166
125
  except Exception as e:
@@ -183,7 +142,7 @@ def runHistorical(plan):
183
142
  hyfrm = kz.getKey('hyfrm')
184
143
  hyto = kz.getKey('hyto')
185
144
 
186
- objective, options = getSolveParameters()
145
+ objective, options = kz.getSolveParameters()
187
146
  try:
188
147
  mybar = progress.Progress(None)
189
148
  fig, summary = plan.runHistoricalRange(objective, options, hyfrm, hyto, figure=True, progcall=mybar)
@@ -207,7 +166,7 @@ def runMC(plan):
207
166
 
208
167
  N = kz.getKey('MC_cases')
209
168
 
210
- objective, options = getSolveParameters()
169
+ objective, options = kz.getSolveParameters()
211
170
  try:
212
171
  mybar = progress.Progress(None)
213
172
  fig, summary = plan.runMC(objective, options, N, figure=True, progcall=mybar)
@@ -337,6 +296,9 @@ def setContributions(plan):
337
296
  """
338
297
  Set from UI -> Plan.
339
298
  """
299
+ if kz.getKey('timeList0') is None:
300
+ return
301
+
340
302
  dicDf = {kz.getKey('iname0'): kz.getKey('timeList0')}
341
303
  if kz.getKey('status') == 'married':
342
304
  dicDf[kz.getKey('iname1')] = kz.getKey('timeList1')
@@ -383,66 +345,20 @@ def resetContributions(plan):
383
345
  def setAllocationRatios(plan):
384
346
  if kz.getKey('allocType') == 'individual':
385
347
  try:
386
- generic = getIndividualAllocationRatios()
348
+ generic = kz.getIndividualAllocationRatios()
387
349
  plan.setAllocationRatios('individual', generic=generic)
388
350
  except Exception as e:
389
351
  st.error('Setting asset allocations failed: %s' % e)
390
352
  return
391
353
  elif kz.getKey('allocType') == 'account':
392
354
  try:
393
- acc = getAccountAllocationRatios()
355
+ acc = kz.getAccountAllocationRatios()
394
356
  plan.setAllocationRatios('account', taxable=acc[0], taxDeferred=acc[1], taxFree=acc[2])
395
357
  except Exception as e:
396
358
  st.error('Setting asset allocations failed: %s' % e)
397
359
  return
398
360
 
399
361
 
400
- def getIndividualAllocationRatios():
401
- generic = []
402
- initial = []
403
- final = []
404
- for k1 in range(4):
405
- initial.append(int(kz.getKey('j3_init%'+str(k1)+'_0')))
406
- final.append(int(kz.getKey('j3_fin%'+str(k1)+'_0')))
407
- gen0 = [initial, final]
408
- generic = [gen0]
409
-
410
- if kz.getKey('status') == 'married':
411
- initial = []
412
- final = []
413
- for k1 in range(4):
414
- initial.append(int(kz.getKey('j3_init%'+str(k1)+'_1')))
415
- final.append(int(kz.getKey('j3_fin%'+str(k1)+'_1')))
416
- gen1 = [initial, final]
417
- generic.append(gen1)
418
-
419
- return generic
420
-
421
-
422
- def getAccountAllocationRatios():
423
- accounts = [[], [], []]
424
- for j1 in range(3):
425
- initial = []
426
- final = []
427
- for k1 in range(4):
428
- initial.append(int(kz.getKey(f'j{j1}_init%'+str(k1)+'_0')))
429
- final.append(int(kz.getKey(f'j{j1}_fin%'+str(k1)+'_0')))
430
- tmp = [initial, final]
431
- accounts[j1].append(tmp)
432
-
433
- if kz.getKey('status') == 'married':
434
- for j1 in range(3):
435
- initial = []
436
- final = []
437
- for k1 in range(4):
438
- initial.append(int(kz.getKey(f'j{j1}_init%'+str(k1)+'_1')))
439
- final.append(int(kz.getKey(f'j{j1}_fin%'+str(k1)+'_1')))
440
- tmp = [initial, final]
441
- accounts[j1].append(tmp)
442
-
443
- return accounts
444
-
445
-
446
362
  @_checkPlan
447
363
  def plotSingleResults(plan):
448
364
  c = 0
@@ -507,24 +423,6 @@ def setProfile(plan, key, pull=True):
507
423
  plan.setSpendingProfile(profile, survivor, dip, increase, delay)
508
424
 
509
425
 
510
- @_checkPlan
511
- def setHeirsTaxRate(plan, key):
512
- val = kz.setpull(key)
513
- plan.setHeirsTaxRate(val)
514
-
515
-
516
- @_checkPlan
517
- def setLongTermCapitalTaxRate(plan, key):
518
- val = kz.setpull(key)
519
- plan.setLongTermCapitalTaxRate(val)
520
-
521
-
522
- @_checkPlan
523
- def setDividendRate(plan, key):
524
- val = kz.setpull(key)
525
- plan.setDividendRate(val)
526
-
527
-
528
426
  @_checkPlan
529
427
  def setDefaultPlots(plan, key):
530
428
  val = kz.storepull(key)
@@ -609,7 +507,7 @@ def saveContributions(plan):
609
507
  @_checkPlan
610
508
  def saveCaseFile(plan):
611
509
  stringBuffer = StringIO()
612
- if getSolveParameters() is None:
510
+ if kz.getSolveParameters() is None:
613
511
  return ''
614
512
  plan.saveConfig(stringBuffer)
615
513
  encoded_data = stringBuffer.getvalue().encode('utf-8')
@@ -632,23 +530,7 @@ def createCaseFromFile(file):
632
530
 
633
531
  return name, mydic
634
532
 
635
- # keynames = ['name', 'status', 'plan', 'summary', 'logs', 'startDate',
636
- # 'timeList', 'plots', 'interpMethod', 'interpCenter', 'interpWidth',
637
- # 'objective', 'withMedicare', 'bequest', 'netSpending',
638
- # 'noRothConversions', 'maxRothConversion',
639
- # 'rateType', 'fixedType', 'varyingType', 'yfrm', 'yto',
640
- # 'divRate', 'heirsTx', 'gainTx', 'spendingProfile', 'survivor',
641
- # 'surplusFraction', ]
642
- # keynamesJ = ['benf', ]
643
- # keynamesK = ['fxRate', 'mean', 'stdev']
644
- # keynamesI = ['iname', 'yob', 'life', 'txbl', 'txDef', 'txFree',
645
- # 'ssAge', 'ssAmt', 'pAge', 'pAmt', 'df',
646
- # 'jX_init%0_', 'jX_init%1_', 'jX_init%2_', 'jX_init%3_',
647
- # 'jX_fin%0_', 'jX_fin%1_', 'jX_fin%2_', 'jX_fin%3_']
648
- # keynames6 = ['corr']
649
-
650
-
651
- # @_checkPlan
533
+
652
534
  def genDic(plan):
653
535
  accName = ['txbl', 'txDef', 'txFree']
654
536
  dic = {}
@@ -744,8 +626,8 @@ def genDic(plan):
744
626
  dic['yto'] = plan.rateTo
745
627
  else:
746
628
  dic['yfrm'] = FROM
747
- # Rates avalability are trailing by 1 or 2 years.
748
- dic['yto'] = date.today().year - 2
629
+ # Rates availability are trailing by 1 year.
630
+ dic['yto'] = date.today().year - 1
749
631
 
750
632
  if plan.rateMethod in ['stochastic', 'histochastic']:
751
633
  qq = 1
@@ -759,8 +641,17 @@ def genDic(plan):
759
641
  return plan._name, dic
760
642
 
761
643
 
762
- def clone(plan, newname, logstreams=None):
763
- return owl.clone(plan, newname, logstreams=logstreams)
644
+ @_checkPlan
645
+ def backYearsMAGI(plan):
646
+ thisyear = date.today().year
647
+ backyears = [0, 0]
648
+ for i in range(plan.N_i):
649
+ if thisyear - plan.yobs[i] >= 65:
650
+ backyears[0] = thisyear - 2
651
+ elif thisyear - plan.yobs[i] >= 64:
652
+ backyears[1] = thisyear - 1
653
+
654
+ return backyears
764
655
 
765
656
 
766
657
  def version():
@@ -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.25
10
+ owlplanner >= 2025.1.29
@@ -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
- owb.updateContributions()
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,105 @@ 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
+ return objective, options
340
+
341
+
342
+ def getIndividualAllocationRatios():
343
+ generic = []
344
+ initial = []
345
+ final = []
346
+ for k1 in range(4):
347
+ initial.append(int(getKey('j3_init%'+str(k1)+'_0')))
348
+ final.append(int(getKey('j3_fin%'+str(k1)+'_0')))
349
+ gen0 = [initial, final]
350
+ generic = [gen0]
351
+
352
+ if getKey('status') == 'married':
353
+ initial = []
354
+ final = []
355
+ for k1 in range(4):
356
+ initial.append(int(getKey('j3_init%'+str(k1)+'_1')))
357
+ final.append(int(getKey('j3_fin%'+str(k1)+'_1')))
358
+ gen1 = [initial, final]
359
+ generic.append(gen1)
360
+
361
+ return generic
362
+
363
+
364
+ def getAccountAllocationRatios():
365
+ accounts = [[], [], []]
366
+ for j1 in range(3):
367
+ initial = []
368
+ final = []
369
+ for k1 in range(4):
370
+ initial.append(int(getKey(f'j{j1}_init%'+str(k1)+'_0')))
371
+ final.append(int(getKey(f'j{j1}_fin%'+str(k1)+'_0')))
372
+ tmp = [initial, final]
373
+ accounts[j1].append(tmp)
374
+
375
+ if getKey('status') == 'married':
376
+ for j1 in range(3):
377
+ initial = []
378
+ final = []
379
+ for k1 in range(4):
380
+ initial.append(int(getKey(f'j{j1}_init%'+str(k1)+'_1')))
381
+ final.append(int(getKey(f'j{j1}_fin%'+str(k1)+'_1')))
382
+ tmp = [initial, final]
383
+ accounts[j1].append(tmp)
384
+
385
+ return accounts
386
+
387
+
388
+ def getPreviousMAGI():
389
+ backMAGI = [0, 0]
390
+ for ii in range(2):
391
+ val = getKey('MAGI'+str(ii))
392
+ if val:
393
+ backMAGI[ii] = val
394
+
395
+ return backMAGI
396
+
397
+
398
+ def getFixedIncome(ni, what):
399
+ amounts = []
400
+ ages = []
401
+ for i in range(ni):
402
+ amounts.append(getKey(what+'Amt'+str(i)))
403
+ ages.append(getKey(what+'Age'+str(i)))
404
+
405
+ return amounts, ages
406
+
407
+
304
408
  def getIntNum(text, nkey, disabled=False, callback=setpull, step=1, help=None, min_value=0, max_value=None):
305
409
  return st.number_input(text,
306
410
  value=int(getKey(nkey)),
@@ -1 +0,0 @@
1
- __version__ = "2025.1.27"
File without changes
File without changes
File without changes