owlplanner 2025.2.11__py3-none-any.whl → 2025.2.15__py3-none-any.whl

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/config.py CHANGED
@@ -64,6 +64,7 @@ def saveConfig(plan, file, mylog):
64
64
  "Heirs rate on tax-deferred estate": float(100 * plan.nu),
65
65
  "Long-term capital gain tax rate": float(100 * plan.psi),
66
66
  "Dividend tax rate": float(100 * plan.mu),
67
+ "TCJA expiration year": plan.yTCJA,
67
68
  "Method": plan.rateMethod,
68
69
  }
69
70
  if plan.rateMethod in ["user", "stochastic"]:
@@ -226,6 +227,7 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
226
227
  p.setDividendRate(float(diconf["Rates Selection"]["Dividend tax rate"]))
227
228
  p.setLongTermCapitalTaxRate(float(diconf["Rates Selection"]["Long-term capital gain tax rate"]))
228
229
  p.setHeirsTaxRate(float(diconf["Rates Selection"]["Heirs rate on tax-deferred estate"]))
230
+ p.yTCJA = int(diconf["Rates Selection"]["TCJA expiration year"])
229
231
 
230
232
  frm = None
231
233
  to = None
owlplanner/plan.py CHANGED
@@ -249,7 +249,8 @@ class Plan(object):
249
249
  assert inames[0] != "" or (self.N_i == 2 and inames[1] == ""), "Name for each individual must be provided."
250
250
 
251
251
  self.filingStatus = ["single", "married"][self.N_i - 1]
252
-
252
+ # Default year TCJA is speculated to expire.
253
+ self.yTCJA = 2026
253
254
  self.inames = inames
254
255
  self.yobs = np.array(yobs, dtype=np.int32)
255
256
  self.expectancy = np.array(expectancy, dtype=np.int32)
@@ -307,7 +308,8 @@ class Plan(object):
307
308
 
308
309
  # Prepare income tax and RMD time series.
309
310
  self.rho_in = tx.rho_in(self.yobs, self.N_n)
310
- self.sigma_n, self.theta_tn, self.Delta_tn = tx.taxParams(self.yobs, self.i_d, self.n_d, self.N_n)
311
+ self.sigma_n, self.theta_tn, self.Delta_tn = tx.taxParams(self.yobs, self.i_d, self.n_d,
312
+ self.N_n, self.yTCJA)
311
313
 
312
314
  # If none was given, default is to begin plan on today's date.
313
315
  self._setStartingDate(startDate)
@@ -443,6 +445,14 @@ class Plan(object):
443
445
  self.mu = mu
444
446
  self.caseStatus = "modified"
445
447
 
448
+ def setExpirationYearTCJA(self, yTCJA):
449
+ """
450
+ Set year at which TCJA is speculated to expire.
451
+ """
452
+ self.mylog.vprint(f"Setting TCJA expiration year to {yTCJA}.")
453
+ self.yTCJA = yTCJA
454
+ self.caseStatus = "modified"
455
+
446
456
  def setLongTermCapitalTaxRate(self, psi):
447
457
  """
448
458
  Set long-term income tax rate. Rate is in percent. Default 15%.
@@ -2262,7 +2272,7 @@ class Plan(object):
2262
2272
  # style = {'net': '-', 'target': ':'}
2263
2273
  style = {"profile": "-"}
2264
2274
  series = {"profile": self.xi_n}
2265
- fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat="xi")
2275
+ fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat="$\\xi$")
2266
2276
 
2267
2277
  if figure:
2268
2278
  return fig
@@ -2572,7 +2582,7 @@ class Plan(object):
2572
2582
 
2573
2583
  fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat)
2574
2584
 
2575
- data = tx.taxBrackets(self.N_i, self.n_d, self.N_n)
2585
+ data = tx.taxBrackets(self.N_i, self.n_d, self.N_n, self.yTCJA)
2576
2586
  for key in data:
2577
2587
  data_adj = data[key] * infladjust
2578
2588
  ax.plot(self.year_n, data_adj, label=key, ls=":")
owlplanner/tax2025.py CHANGED
@@ -24,49 +24,62 @@ from datetime import date
24
24
 
25
25
  taxBracketNames = ["10%", "12/15%", "22/25%", "24/28%", "32/33%", "35%", "37/40%"]
26
26
 
27
- rates_2025 = np.array([0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.370])
28
- rates_2026 = np.array([0.10, 0.15, 0.25, 0.28, 0.33, 0.35, 0.396])
27
+ rates_TCJA = np.array([0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.370])
28
+ rates_nonTCJA = np.array([0.10, 0.15, 0.25, 0.28, 0.33, 0.35, 0.396])
29
29
 
30
+ ###############################################################################
31
+ # Start of section where rates need to be actualized every year.
32
+ ###############################################################################
30
33
  # Single [0] and married filing jointly [1].
31
- taxBrackets_2025 = np.array(
34
+
35
+ # These are current.
36
+ taxBrackets_TCJA = np.array(
32
37
  [
33
38
  [11925, 48475, 103350, 197300, 250525, 626350, 9999999],
34
39
  [23850, 96950, 206700, 394600, 501050, 751700, 9999999],
35
40
  ]
36
41
  )
37
42
 
38
- irmaaBrackets_2025 = np.array(
43
+ irmaaBrackets = np.array(
39
44
  [
40
45
  [0, 106000, 133000, 167000, 200000, 500000],
41
46
  [0, 212000, 266000, 334000, 400000, 750000],
42
47
  ]
43
48
  )
44
49
 
45
- # Use index [0] to store the standard Medicare part B premium.
50
+ # Index [0] stores the standard Medicare part B premium.
46
51
  # Following values are incremental IRMAA part B monthly fees.
47
- # 2024 total monthly fees: [174.70, 244.60, 349.40, 454.20, 559.00, 594.00]
48
- # irmaaFees_2024 = 12 * np.array([174.70, 69.90, 104.80, 104.80, 104.80, 35.00])
49
- irmaaFees_2025 = 12 * np.array([185.00, 74.00, 111.00, 110.90, 111.00, 37.00])
52
+ irmaaFees = 12 * np.array([185.00, 74.00, 111.00, 110.90, 111.00, 37.00])
50
53
 
51
- # Compute 2026 from 2017 with 27% increase.
54
+ # Make projection for non-TCJA using 2017 to current year.
52
55
  # taxBrackets_2017 = np.array(
53
56
  # [ [9325, 37950, 91900, 191650, 416700, 418400, 9999999],
54
- # [18650, 75900, 153100, 233350, 416700, 470000, 9999999],
57
+ # [18650, 75900, 153100, 233350, 416700, 470700, 9999999],
55
58
  # ])
56
-
57
- taxBrackets_2026 = np.array(
59
+ #
60
+ # stdDeduction_2017 = [6350, 12700]
61
+ #
62
+ # For 2025, I used a 30.5% adjustment from 2017, rounded to closest 50.
63
+ #
64
+ # These are speculated.
65
+ taxBrackets_nonTCJA = np.array(
58
66
  [
59
- [11850, 48200, 116700, 243400, 529200, 531400, 9999999],
60
- [23700, 96400, 194400, 296350, 529200, 596900, 9999999],
67
+ [12150, 49550, 119950, 250200, 544000, 546200, 9999999],
68
+ [24350, 99100, 199850, 304600, 543950, 614450, 9999999],
61
69
  ]
62
70
  )
63
71
 
64
- stdDeduction_2025 = np.array([15000, 30000])
65
- stdDeduction_2026 = np.array([8300, 16600])
66
- extra65Deduction_2025 = np.array([2000, 1600])
72
+ # These are current.
73
+ stdDeduction_TCJA = np.array([15000, 30000])
74
+ # These are speculated.
75
+ stdDeduction_nonTCJA = np.array([8300, 16600])
67
76
 
77
+ # These are current.
78
+ extra65Deduction = np.array([2000, 1600])
68
79
 
69
- ##############################################################################
80
+ ###############################################################################
81
+ # End of section where rates need to be actualized every year.
82
+ ###############################################################################
70
83
 
71
84
 
72
85
  def mediCosts(yobs, horizons, magi, prevmagi, gamma_n, Nn):
@@ -80,21 +93,25 @@ def mediCosts(yobs, horizons, magi, prevmagi, gamma_n, Nn):
80
93
  for i in range(Ni):
81
94
  if thisyear + n - yobs[i] >= 65 and n < horizons[i]:
82
95
  # Start with the (indexed) basic Medicare part B premium.
83
- costs[n] += gamma_n[n] * irmaaFees_2025[0]
96
+ costs[n] += gamma_n[n] * irmaaFees[0]
84
97
  if n < 2:
85
98
  mymagi = prevmagi[n]
86
99
  else:
87
100
  mymagi = magi[n - 2]
88
101
  for q in range(1, 6):
89
- if mymagi > gamma_n[n] * irmaaBrackets_2025[Ni - 1][q]:
90
- costs[n] += gamma_n[n] * irmaaFees_2025[q]
102
+ if mymagi > gamma_n[n] * irmaaBrackets[Ni - 1][q]:
103
+ costs[n] += gamma_n[n] * irmaaFees[q]
91
104
 
92
105
  return costs
93
106
 
94
107
 
95
- def taxParams(yobs, i_d, n_d, N_n):
108
+ def taxParams(yobs, i_d, n_d, N_n, y_TCJA=2026):
96
109
  """
97
- Return 3 time series:
110
+ Input is year of birth, index of shortest-lived individual,
111
+ lifespan of shortest-lived individual, total number of years
112
+ in the plan, and the year that TCJA might expire.
113
+
114
+ It returns 3 time series:
98
115
  1) Standard deductions at year n (sigma_n).
99
116
  2) Tax rate in year n (theta_tn)
100
117
  3) Delta from top to bottom of tax brackets (Delta_tn)
@@ -102,12 +119,12 @@ def taxParams(yobs, i_d, n_d, N_n):
102
119
  Returned values are not indexed for inflation.
103
120
  """
104
121
  # Compute the deltas in-place between brackets, starting from the end.
105
- deltaBrackets_2025 = np.array(taxBrackets_2025)
106
- deltaBrackets_2026 = np.array(taxBrackets_2026)
122
+ deltaBrackets_TCJA = np.array(taxBrackets_TCJA)
123
+ deltaBrackets_nonTCJA = np.array(taxBrackets_nonTCJA)
107
124
  for t in range(6, 0, -1):
108
125
  for i in range(2):
109
- deltaBrackets_2025[i, t] -= deltaBrackets_2025[i, t - 1]
110
- deltaBrackets_2026[i, t] -= deltaBrackets_2026[i, t - 1]
126
+ deltaBrackets_TCJA[i, t] -= deltaBrackets_TCJA[i, t - 1]
127
+ deltaBrackets_nonTCJA[i, t] -= deltaBrackets_nonTCJA[i, t - 1]
111
128
 
112
129
  # Prepare the 3 arrays to return - use transpose for easy slicing.
113
130
  sigma = np.zeros((N_n))
@@ -124,23 +141,23 @@ def taxParams(yobs, i_d, n_d, N_n):
124
141
  souls.remove(i_d)
125
142
  filingStatus -= 1
126
143
 
127
- if thisyear + n < 2026:
128
- sigma[n] = stdDeduction_2025[filingStatus]
129
- Delta[n, :] = deltaBrackets_2025[filingStatus, :]
144
+ if thisyear + n < y_TCJA:
145
+ sigma[n] = stdDeduction_TCJA[filingStatus]
146
+ Delta[n, :] = deltaBrackets_TCJA[filingStatus, :]
130
147
  else:
131
- sigma[n] = stdDeduction_2026[filingStatus]
132
- Delta[n, :] = deltaBrackets_2026[filingStatus, :]
148
+ sigma[n] = stdDeduction_nonTCJA[filingStatus]
149
+ Delta[n, :] = deltaBrackets_nonTCJA[filingStatus, :]
133
150
 
134
151
  # Add 65+ additional exemption(s).
135
152
  for i in souls:
136
153
  if thisyear + n - yobs[i] >= 65:
137
- sigma[n] += extra65Deduction_2025[filingStatus]
154
+ sigma[n] += extra65Deduction[filingStatus]
138
155
 
139
156
  # Fill in future tax rates for year n.
140
- if thisyear + n < 2026:
141
- theta[n, :] = rates_2025[:]
157
+ if thisyear + n < y_TCJA:
158
+ theta[n, :] = rates_TCJA[:]
142
159
  else:
143
- theta[n, :] = rates_2026[:]
160
+ theta[n, :] = rates_nonTCJA[:]
144
161
 
145
162
  Delta = Delta.transpose()
146
163
  theta = theta.transpose()
@@ -149,23 +166,26 @@ def taxParams(yobs, i_d, n_d, N_n):
149
166
  return sigma, theta, Delta
150
167
 
151
168
 
152
- def taxBrackets(N_i, n_d, N_n):
169
+ def taxBrackets(N_i, n_d, N_n, y_TCJA):
153
170
  """
154
171
  Return dictionary containing future tax brackets
155
172
  unadjusted for inflation for plotting.
156
173
  """
157
174
  assert 0 < N_i and N_i <= 2, f"Cannot process {N_i} individuals."
158
- # This 1 is the number of years left in TCJA from 2025.
159
- ytc = 1
160
- status = N_i - 1
161
175
  n_d = min(n_d, N_n)
176
+ status = N_i - 1
177
+
178
+ # Number of years left in TCJA from this year.
179
+ thisyear = date.today().year
180
+ ytc = y_TCJA - thisyear
162
181
 
163
182
  data = {}
164
183
  for t in range(len(taxBracketNames) - 1):
165
184
  array = np.zeros(N_n)
166
- array[0:ytc] = taxBrackets_2025[status][t]
167
- array[ytc:n_d] = taxBrackets_2026[status][t]
168
- array[n_d:N_n] = taxBrackets_2026[0][t]
185
+ for n in range(N_n):
186
+ stat = status if n < n_d else 0
187
+ array[n] = taxBrackets_TCJA[stat][t] if n < ytc else taxBrackets_nonTCJA[stat][t]
188
+
169
189
  data[taxBracketNames[t]] = array
170
190
 
171
191
  return data
owlplanner/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2025.02.11"
1
+ __version__ = "2025.02.15"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owlplanner
3
- Version: 2025.2.11
3
+ Version: 2025.2.15
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
@@ -708,15 +708,21 @@ Description-Content-Type: text/markdown
708
708
 
709
709
  <img align=right src="https://raw.github.com/mdlacasse/Owl/main/docs/images/owl.png" width="250">
710
710
 
711
- -----
711
+ -------------------------------------------------------------------------------------
712
+
713
+ ### TL;DR
714
+ Owl is a planning tool that uses a linear programming optimization algorithm to provide guidance on retirement decisions. There are a few ways to run Owl.
712
715
 
713
- ### TLDR
714
- Owl is a planning tool that uses a linear programming optimization algorithm to provide guidance on retirement decisions.
716
+ - Run Owl directly on the Streamlit Community Server at [owlplanner.streamlit.app](https://owlplanner.streamlit.app).
715
717
 
716
- Go to [owlplanner.streamlit.app](https://owlplanner.streamlit.app) and explore the app to learn about its capabilities.
718
+ - Run locally on your computer using a Docker image.
719
+ Follow these [instructions](docker/README.md) for this option.
717
720
 
718
- -----
721
+ - Run locally on your computer using Python code and libraries.
722
+ Follow these [instructions](INSTALL.md) to install Owl from the source code and run it on your computer.
719
723
 
724
+ -------------------------------------------------------------------------------------
725
+ ## Overview
720
726
  This package is a retirement modeling framework for exploring the sensitivity of retirement financial decisions.
721
727
  Strictly speaking, it is not a planning tool, but more an environment for exploring *what if* scenarios.
722
728
  It provides different realizations of a financial strategy through the rigorous
@@ -750,21 +756,21 @@ The algorithms in Owl rely on the open-source HiGHS linear programming solver. T
750
756
  detailed description of the underlying
751
757
  mathematical model can be found [here](https://raw.github.com/mdlacasse/Owl/main/docs/owl.pdf).
752
758
 
753
- While Owl can be used through Jupyter notebooks,
754
- its simple API also serves as a back-end for a Web application using Streamlit.
755
- A hosted version of the app can be found at [owlplanner.streamlit.app](https://owlplanner.streamlit.app).
756
- Alternatively, the application can also be run locally by simply running the script
757
- `owlplanner.cmd` once all the dependencies have been installed.
759
+ It is anticipated that most end users will use Owl through the graphical interface
760
+ either at [owlplanner.streamlit.app](https://owlplanner.streamlit.app)
761
+ or [installed](INSTALL.md) on their own computer.
762
+ The underlying Python package can also be used directly through Python scripts or a Jupyter Notebook
763
+ as described [here](USER_GUIDE.md).
758
764
 
759
765
  Not every retirement decision strategy can be framed as an easy-to-solve optimization problem.
760
766
  In particular, if one is interested in comparing different withdrawal strategies,
761
- [FI Calc](ficalc.app) is a more appropriate and elegant application that addresses this need.
767
+ [FI Calc](ficalc.app) is an elegant application that addresses this need.
762
768
  If, however, you also want to optimize spending, bequest, and Roth conversions, with
763
769
  an approach also considering Medicare and federal income tax over the next few years,
764
770
  then Owl is definitely a tool that can help guide your decisions.
765
771
 
766
772
  --------------------------------------------------------------------------------------
767
- ## Basic capabilities
773
+ ## Capabilities
768
774
  Owl can optimize for either maximum net spending under the constraint of a given bequest (which can be zero),
769
775
  or maximize the after-tax value of a bequest under the constraint of a desired net spending profile,
770
776
  and under the assumption of a heirs marginal tax rate.
@@ -783,7 +789,7 @@ Other asset classes can easily be added, but would add complexity while only pro
783
789
  Historical data used are from
784
790
  [Aswath Damodaran](https://pages.stern.nyu.edu/~adamodar/) at the Stern School of Business.
785
791
  Asset allocations are selected for the duration of the plan, and these can glide linearly
786
- or along a configurable s-curve from now to the last year of the plan.
792
+ or along a configurable s-curve over the lifespan of the individual.
787
793
 
788
794
  Spending profiles are adjusted for inflation, and so are all other indexable quantities. Proflies can be
789
795
  flat or follow a *smile* curve which is also adjustable through two simple parameters.
@@ -794,10 +800,10 @@ the statistical characteristics (means and covariance matrix) of
794
800
  a selected historical year range. Pure *stochastic* rates can also be generated
795
801
  if the user provides means, volatility (expressed as standard deviation), and optionally
796
802
  the correlations between the different assets return rates provided as a matrix, or a list of
797
- the off-diagonal elements (see the notebook tutorial for details).
803
+ the off-diagonal elements (see documentation for details).
798
804
  Average rates calculated over a historical data period can also be chosen.
799
805
 
800
- Monte Carlo simulations capabilities are included and provide a probability of success and a histogram of
806
+ Monte Carlo simulations capabilities are included and provide a probability of success and a histogram of
801
807
  outcomes. These simulations can be used for either determining the probability distribution of the
802
808
  maximum net spending amount under
803
809
  the constraint of a desired bequest, or the probability distribution of the maximum
@@ -805,32 +811,37 @@ bequest under the constraint of a desired net spending amount. Unlike discrete-e
805
811
  simulators, Owl uses an optimization algorithm for every new scenario, which results in more
806
812
  calculations being performed. As a result, the number of cases to be considered should be kept
807
813
  to a reasonable number. For a few hundred cases, a few minutes of calculations can provide very good estimates
808
- and reliable probability distributions. Optimizing each solution is more representative in the sense that optimal solutions
809
- will naturally adjust to the return scenarios being considered. This is more realistic as retirees would certainly re-evaluate
810
- their expectations under severe market drops or gains. This optimal approach provides a net benefit over event-based simulations,
814
+ and reliable probability distributions.
815
+
816
+ Optimizing each solution is more representative than event-base simulators
817
+ in the sense that optimal solutions
818
+ will naturally adjust to the return scenarios being considered.
819
+ This is more realistic as retirees would certainly re-evaluate
820
+ their expectations under severe market drops or gains.
821
+ This optimal approach provides a net benefit over event-based simulators,
811
822
  which maintain a distribution strategy either fixed, or within guardrails for capturing the
812
- retirees' reactions to the market.
823
+ retirees' reactions to the market.
813
824
 
814
- Basic input parameters are given through function calls while optional additional time series can be read from
825
+ Basic input parameters can be entered through the user interface
826
+ while optional additional time series can be read from
815
827
  an Excel spreadsheet that contains future wages, contributions
816
- to savings accounts, and planned *big-ticket items* such as the purchase of a lake house, the sale of a boat,
817
- large gifts, or inheritance.
828
+ to savings accounts, and planned *big-ticket items* such as the purchase of a lake house,
829
+ the sale of a boat, large gifts, or inheritance.
818
830
 
819
831
  Three types of savings accounts are considered: taxable, tax-deferred, and tax-exempt,
820
832
  which are all tracked separately for married individuals. Asset transition to the surviving spouse
821
- is done according to beneficiary fractions for each account type.
833
+ is done according to beneficiary fractions for each type of savings account.
822
834
  Tax status covers married filing jointly and single, depending on the number of individuals reported.
823
835
 
824
- Medicare and IRMAA calculations are performed through a self-consistent loop on cash flow constraints. Future
825
- values are simple projections of current values with the assumed inflation rates.
826
-
827
- See one of the notebooks for a tutorial and representative user cases.
836
+ Medicare and IRMAA calculations are performed through a self-consistent loop on cash flow constraints.
837
+ Future values are simple projections of current values with the assumed inflation rates.
828
838
 
829
839
  ### Limitations
830
840
  Owl is work in progress. At the current time:
831
841
  - Only the US federal income tax is considered (and minimized through the optimization algorithm).
832
842
  Head of household filing status has not been added but can easily be.
833
- - Required minimum distributions are calculated, but tables for spouses more than 10 years apart are not included. An error message will be generated for these cases.
843
+ - Required minimum distributions are calculated, but tables for spouses more than 10 years apart are not included.
844
+ These cases are detected and will generate an error message.
834
845
  - Social security rule for surviving spouse assumes that benefits were taken at full retirement age.
835
846
  - Current version has no optimization of asset allocations between individuals and/or types of savings accounts.
836
847
  If there is interest, that could be added in the future.
@@ -839,12 +850,12 @@ If there is interest, that could be added in the future.
839
850
  This means that the Medicare premiums are calculated after an initial solution is generated,
840
851
  and then a new solution is re-generated with these premiums as a constraint.
841
852
  In some situations, when the income (MAGI) is near an IRMAA bracket, oscillatory solutions can arise.
842
- Owl will detect these cases and inform the user.
843
- While the solutions generated are very close to one another, Owl will pick the smallest one
853
+ While the solutions generated are very close to one another, Owl will pick the smallest solution
844
854
  for being conservative.
845
- - Part D is not included in the IRMAA calculations. Being considerably more, only Part B is taken into account.
846
- - Future tax brackets are pure speculations derived from the little we know now and projected to the next 30 years. Your guesses are as good as mine.
847
- Having a knob to adjust future rates might be an interesting feature to add for measuring the impact on Roth conversions.
855
+ - Part D is not included in the IRMAA calculations. Being considerably more significant,
856
+ only Part B is taken into account.
857
+ - Future tax brackets are pure speculations derived from the little we know now and projected to the next 30 years.
858
+ Your guesses are as good as mine.
848
859
 
849
860
  The solution from an optimization algorithm has only two states: feasible and infeasible.
850
861
  Therefore, unlike event-driven simulators that can tell you that your distribution strategy runs
@@ -854,215 +865,12 @@ estate value too large for the savings assets to support, even with zero net spe
854
865
  or maximizing the bequest subject to a net spending basis that is already too large for the savings
855
866
  assets to support, even with no estate being left.
856
867
 
857
- -----------------------------------------------------------------------
858
- ## An example of Owl's functionality
859
- With about 10 lines of Python code, one can generate a full case study.
860
- Here is a typical plan with some comments.
861
- A plan starts with the names of the individuals, their birth years and life expectancies, and a name for the plan.
862
- Dollar amounts are in k\$ (i.e. thousands) and ratios in percentage.
863
- ```python
864
- import owlplanner as owl
865
- # Jack was born in 1962 and expects to live to age 89. Jill was born in 1965 and hopes to live to age 92.
866
- # Plan starts on Jan 1st of this year.
867
- plan = owl.Plan(['Jack', 'Jill'], [1962, 1965], [89, 92], 'jack & jill - tutorial', startDate='01-01')
868
- # Jack has $90.5k in a taxable investment account, $600.5k in a tax-deferred account and $70k from 2 tax-exempt accounts.
869
- # Jill has $60.2k in her taxable account, $150k in a 403b, and $40k in a Roth IRA.
870
- plan.setAccountBalances(taxable=[90.5, 60.2], taxDeferred=[600.5, 150], taxFree=[50.6 + 20, 40.8])
871
- # An Excel file contains 2 tabs (one for Jill, one for Jack) describing anticipated wages and contributions.
872
- plan.readContributions('jack+jill.xlsx')
873
- # Jack will glide an s-curve for asset allocations from a 60/40 -> 70/30 stocks/bonds portfolio.
874
- # Jill will do the same thing but is a bit more conservative from 50/50 -> 70/30 stocks/bonds portfolio.
875
- plan.setInterpolationMethod('s-curve')
876
- plan.setAllocationRatios('individual', generic=[[[60, 40, 0, 0], [70, 30, 0, 0]], [[50, 50, 0, 0], [70, 30, 0, 0]]])
877
- # Jack has no pension, but Jill will receive $10k per year at 65 yo.
878
- plan.setPension([0, 10.5], [65, 65])
879
- # Jack anticipates receiving social security of $28.4k at age 70, and Jill $19.7k at age 62. All values are in today's $.
880
- plan.setSocialSecurity([28.4, 19.7], [70, 62])
881
- # Instead of a 'flat' profile, we select a 'smile' spending profile, with 60% needs for the survivor.
882
- plan.setSpendingProfile('smile', 60)
883
- # We will reproduce the historical sequence of returns starting in year 1969.
884
- plan.setRates('historical', 1969)
885
- # Jack and Jill want to leave a bequest of $500k, and limit Roth conversions to $100k per year.
886
- # Jill's 403b plan does not support in-plan Roth conversions.
887
- # We solve for the maximum net spending profile under these constraints.
888
- plan.solve('maxSpending', options={'maxRothConversion': 100, 'bequest': 500, 'noRothConversions': 'Jill'})
889
- ```
890
- The output can be seen using the following commands that display various plots of the decision variables in time.
891
- ```python
892
- plan.showNetSpending()
893
- plan.showGrossIncome()
894
- plan.showTaxes()
895
- plan.showSources()
896
- plan.showAccounts()
897
- plan.showAssetDistribution()
898
- ...
899
- ```
900
- By default, all these plots are in nominal dollars. To get values in today's $, a call to
901
- ```python
902
- plan.setDefaultPlots('today')
903
- ```
904
- would change all graphs to report in today's dollars. Each plot can also override the default by setting the `value`
905
- parameters to either *nominal* or *today*, such as in the following example, which shows the taxable ordinary
906
- income over the duration of the plan,
907
- along with inflation-adjusted extrapolated tax brackets. Notice how the optimized income is surfing
908
- the boundaries of tax brackets.
909
- ```python
910
- plan.showGrossIncome(value='nominal')
911
- ```
912
- <img src="https://raw.github.com/mdlacasse/Owl/main/docs/images/taxIncomePlot.png" width="75%">
913
-
914
- The optimal spending profile is shown in the next plot (in today's dollars). Notice the drop
915
- (recall we selected 60% survivor needs) at the passing of the first spouse.
916
- ```python
917
- plan.showProfile('today')
918
- ```
919
-
920
- <img src="https://raw.github.com/mdlacasse/Owl/main/docs/images/spendingPlot.png" width="75%">
921
-
922
- The following plot shows the account balances in nominal value for all savings accounts owned by Jack and Jill.
923
- It was generated using
924
- ```python
925
- plan.showAccounts(value='nominal')
926
- ```
927
- <img src="https://raw.github.com/mdlacasse/Owl/main/docs/images/savingsPlot.png" width="75%">
928
-
929
- while this plot shows the complex cash flow from all sources, which was generated with
930
- ```python
931
- plan.showSources(value='nominal')
932
- ```
933
- <img src="https://raw.github.com/mdlacasse/Owl/main/docs/images/sourcesPlot.png" width="75%">
934
-
935
- For taxes, the following call will display Medicare premiums (including Part B IRMAA fees) and federal income tax
936
- ```python
937
- plan.showTaxes(value='nominal')
938
- ```
939
- <img src="https://raw.github.com/mdlacasse/Owl/main/docs/images/taxesPlot.png" width="75%">
940
-
941
- For the case at hand, recall that asset allocations were selected above through
942
-
943
- ```python
944
- plan.setAllocationRatios('individual', generic=[[[60, 40, 0, 0], [70, 30, 0, 0]], [[50, 50, 0, 0], [70, 30, 0, 0]]])
945
- ```
946
- gliding from a 60%/40% stocks/bonds portfolio to 70%/30% for Jack, and 50%/50% -> 70%/30% for Jill.
947
- Assets distribution in all accounts in today's $ over time can be displayed from
948
- ```python
949
- plan.showAssetDistribution(value='today')
950
- ```
951
- <img src="https://raw.github.com/mdlacasse/Owl/main/docs/images/AD-taxable.png" width="75%">
952
- <img src="https://raw.github.com/mdlacasse/Owl/main/docs/images/AD-taxDef.png" width="75%">
953
- <img src="https://raw.github.com/mdlacasse/Owl/main/docs/images/AD-taxFree.png" width="75%">
954
-
955
- These plots are irregular because we used historical rates from 1969. The volatility of
956
- the rates offers Roth conversion benefits which are exploited by the optimizer.
957
- The rates used can be displayed by:
958
- ```python
959
- plan.showRates()
960
- ```
961
- <img src="https://raw.github.com/mdlacasse/Owl/main/docs/images/ratesPlot.png" width="75%">
962
-
963
- Values between brackets <> are the average values and volatility over the selected period.
964
-
965
- For the statisticians, rates distributions and correlations between them can be shown using:
966
- ```python
967
- plan.showRatesCorrelations()
968
- ```
969
- <img src="https://raw.github.com/mdlacasse/Owl/main/docs/images/ratesCorrelations.png" width="75%">
970
-
971
- A short text summary of the outcome of the optimization can be displayed through using:
972
- ```python
973
- plan.summary()
974
- ```
975
- The output of the last command reports that if future rates are exactly like those observed
976
- starting from 1969 and the following years, Jack and Jill could afford an annual spending of
977
- \\$97k starting this year
978
- (with a basis of \\$88.8k - the basis multiplies the profile which can vary over the course of the plan).
979
- The summary also contains some details:
980
- ```
981
- SUMMARY ================================================================
982
- Net yearly spending basis in 2025$: $91,812
983
- Net yearly spending for year 2025: $100,448
984
- Net spending remaining in year 2025: $100,448
985
- Total net spending in 2025$: $2,809,453 ($7,757,092 nominal)
986
- Total Roth conversions in 2025$: $320,639 ($456,454 nominal)
987
- Total income tax paid on ordinary income in 2025$: $247,788 ($469,522 nominal)
988
- Total tax paid on gains and dividends in 2025$: $3,313 ($3,768 nominal)
989
- Total Medicare premiums paid in 2025$: $117,660 ($343,388 nominal)
990
- Spousal wealth transfer from Jack to Jill in year 2051 (nominal): taxable: $0 tax-def: $57,224 tax-free: $2,102,173
991
- Sum of spousal bequests to Jill in year 2051 in 2025$: $499,341 ($2,159,397 nominal)
992
- Post-tax non-spousal bequests from Jack in year 2051 (nominal): taxable: $0 tax-def: $0 tax-free: $0
993
- Sum of post-tax non-spousal bequests from Jack in year 2051 in 2025$: $0 ($0 nominal)
994
- Post-tax account values at the end of final plan year 2057 (nominal): taxable: $0 tax-def: $0 tax-free: $2,488,808
995
- Total estate value at the end of final plan year 2057 in 2025$: $500,000 ($2,488,808 nominal)
996
- Plan starting date: 01-01
997
- Cumulative inflation factor from start date to end of plan: 4.98
998
- Jack's 27-year life horizon: 2025 -> 2051
999
- Jill's 33-year life horizon: 2025 -> 2057
1000
- Plan name: jack & jill - tutorial
1001
- Number of decision variables: 996
1002
- Number of constraints: 867
1003
- Case executed on: 2025-02-04 at 22:55:03
1004
- ------------------------------------------------------------------------
1005
- ```
1006
- And an Excel workbook can be saved with all the detailed amounts over the years by using the following command:
1007
- ```python
1008
- plan.saveWorkbook(overwrite=True)
1009
- ```
1010
- For Monte Carlo simulations, the mean return rates, their volatility and covariance are specified
1011
- and used to generate random scenarios. A histogram of outcomes is generated such as this one for Jack and Jill, which was generated
1012
- by selecting *stochastic* rates and using
1013
- ```
1014
- plan.runMC('maxSpending', ...)
1015
- ```
1016
- <img src="https://raw.github.com/mdlacasse/Owl/main/docs/images/MC-tutorial2a.png" width="75%">
1017
-
1018
- Similarly, the next one was generated using
1019
- ```
1020
- plan.runMC('maxBequest', ...)
1021
- ```
1022
- <img src="https://raw.github.com/mdlacasse/Owl/main/docs/images/MC-tutorial2b.png" width="75%">
1023
-
1024
-
1025
- See tutorial notebooks [1](https://github.com/mdlacasse/Owl/blob/main/notebooks/tutorial_1.ipynb),
1026
- [2](https://github.com/mdlacasse/Owl/blob/main/notebooks/tutorial_2.ipynb), and
1027
- [3](https://github.com/mdlacasse/Owl/blob/main/notebooks/tutorial_3.ipynb) for more info.
1028
-
1029
-
1030
868
  ---------------------------------------------------------------
1031
- ## Requirements
1032
-
1033
- If you have Python already installed on your computer, Owl can be installed as a package using the following commands:
1034
- ```shell
1035
- python -m build
1036
- pip install .
1037
- ```
1038
- These commands need to run from the Owl directory where you downloaded Owl from GitHub either through git or a zip file.
1039
- Pip will install all the required dependencies.
869
+ ## Documentation
1040
870
 
1041
- Owl relies on common Python modules such as NumPy, Pandas, SciPy, matplotlib, and Seaborn.
1042
- The user front-end was built on Streamlit.
1043
- Package `odfpy` might be required if one read files created by LibreOffice. Again, these dependencies
1044
- will be installed by pip.
1045
-
1046
- The simplest way to get started with Owl is to use the `streamlit` browser-based user interface
1047
- that is started by the `owlplanner.cmd` script, which will start a user interface on your own browser.
1048
- Here is a screenshot of one of the multiple tabs of the interface:
1049
-
1050
- <img src="https://raw.github.com/mdlacasse/Owl/main/docs/images/OwlUI.png" width="100%">
1051
-
1052
- Alternatively, one can prefer using Owl from Jupyter notebooks. For that purpose, the `examples` directory
1053
- contains many files as a tutorial. The Jupyter Notebook interface is a browser-based application for authoring documents that combines live-code with narrative text, equations and visualizations.
1054
- Jupyter will run in your default web browser, from your computer to your browser, and therefore no data is ever transferred on the Internet
1055
- (your computer, i.e., `localhost`, is the server).
1056
-
1057
- For simulating your own realizations, use the files beginning with the word *template*.
1058
- Make a copy and rename them with your own names while keeping the same extension.
1059
- Then you'll be able to personalize a case with your own numbers and start experimenting with Owl.
1060
- Notebooks with detailed explanations can be found in
1061
- [tutorial_1](https://github.com/mdlacasse/Owl/blob/main/examples/tutorial_1.ipynb),
1062
- [tutorial_2](https://github.com/mdlacasse/Owl/blob/main/examples/tutorial_1.ipynb), and
1063
- [tutorial_3](https://github.com/mdlacasse/Owl/blob/main/examples/tutorial_2.ipynb).
1064
-
1065
- Finally, you will also need the capability to read and edit Excel files. One can have an Excel license, or use the LibreOffice free alternative. You can also use Google docs.
871
+ - Documentation for the app user interface is available from the interface itself.
872
+ - Installation guide and software requirements can be found [here](INSTALL.md).
873
+ - User guide for the underlying Python package as used in a Jupyter notebook can be found [here](USER_GUIDE.md).
1066
874
 
1067
875
  ---------------------------------------------------------------------
1068
876
 
@@ -1071,12 +879,14 @@ Finally, you will also need the capability to read and edit Excel files. One can
1071
879
  - Image from [freepik](https://freepik.com)
1072
880
  - Optimization solver from [HiGHS](https://highs.dev)
1073
881
  - Streamlit Community Cloud [Streamlit](https://streamlit.io)
882
+ - Contributors: Josh (noimjosh@gmail.com) for Docker image code
1074
883
 
1075
884
  ---------------------------------------------------------------------
1076
885
 
1077
886
  Copyright &copy; 2024 - Martin-D. Lacasse
1078
887
 
1079
- Disclaimers: I am not a financial planner. You make your own decisions. This program comes with no guarantee. Use at your own risk.
888
+ Disclaimers: I am not a financial planner. You make your own decisions.
889
+ This program comes with no guarantee. Use at your own risk.
1080
890
 
1081
891
  --------------------------------------------------------
1082
892
 
@@ -1,17 +1,17 @@
1
1
  owlplanner/__init__.py,sha256=QqrdT0Qks20osBTg7h0vJHAxpP9lL7DA99xb0nYbtw4,254
2
2
  owlplanner/abcapi.py,sha256=LbzW_KcNy0IeHp42MUHwGu_H67B2h_e1_vu-c2ACTkQ,6646
3
- owlplanner/config.py,sha256=XFVcXFVpEuWXzybaijNGSTt72py3cYJ3oq0S1ujivl0,11702
3
+ owlplanner/config.py,sha256=Yzi_Xivd_EFfuHklIoQ-LNqKCxF2ruc8p-Il_HVgEaw,11817
4
4
  owlplanner/logging.py,sha256=tYMw04O-XYSzjTj36fmKJGLcE1VkK6k6oJNeqtKXzuc,2530
5
- owlplanner/plan.py,sha256=f_F7sIka4c_91dV7X5XHJkVvztXbtS_cqi24ncBI9gY,113084
5
+ owlplanner/plan.py,sha256=wpCeRSLnnYIEpRAw4MkUXjA5AgNTsuPC4F0Y51zLVuE,113526
6
6
  owlplanner/progress.py,sha256=8jlCvvtgDI89zXVNMBg1-lnEyhpPvKQS2X5oAIpoOVQ,384
7
7
  owlplanner/rates.py,sha256=TN407qU4n-bac1oymkQ_n2QKEPwFQxy6JZVGwgIkLQU,15585
8
- owlplanner/tax2025.py,sha256=PVteko6G9gjAT247GnTzAPUe_RaLnZUArFtdzf1dF3M,7014
8
+ owlplanner/tax2025.py,sha256=B-A5eU3wxdcAaxRCbT3qI-JEKoD_ZeNbg_86XhNdQEI,7745
9
9
  owlplanner/timelists.py,sha256=tYieZU67FT6TCcQQis36JaXGI7dT6NqD7RvdEjgJL4M,4026
10
10
  owlplanner/utils.py,sha256=HM70W60qB41zfnbl2LltNwAuLYHyy5XYbwnbNcaa6FE,2351
11
- owlplanner/version.py,sha256=n8NJ4iSncRuwW7_28_WeKgNkjAmkCzQOs0fAbkRFU5E,28
11
+ owlplanner/version.py,sha256=oJ5YLWQGjCt2MMP18haMf1cu0Tep1Sp6yhXiMqCOdyo,28
12
12
  owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
14
- owlplanner-2025.2.11.dist-info/METADATA,sha256=VkjJTA1BGuWJ1HnFS4tGWYrPvA1s_iV0YyIrnxYN2yk,63947
15
- owlplanner-2025.2.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- owlplanner-2025.2.11.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
17
- owlplanner-2025.2.11.dist-info/RECORD,,
14
+ owlplanner-2025.2.15.dist-info/METADATA,sha256=Cm3V5cze1lykizvnbTZdnOliJ-cuj8aqknSInq6i4nU,53506
15
+ owlplanner-2025.2.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ owlplanner-2025.2.15.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
17
+ owlplanner-2025.2.15.dist-info/RECORD,,