owlplanner 2025.8.1__py3-none-any.whl → 2025.11.2__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
@@ -296,6 +296,10 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
296
296
  # Solver Options.
297
297
  p.solverOptions = diconf["Solver Options"]
298
298
 
299
+ # Address legacy case files.
300
+ if diconf["Solver Options"].get("withMedicare", None) is True:
301
+ p.solverOptions["withMedicare"] = "loop"
302
+
299
303
  # Check consistency of noRothConversions.
300
304
  name = p.solverOptions.get("noRothConversions", "None")
301
305
  if name != "None" and name not in p.inames:
owlplanner/plan.py CHANGED
@@ -1021,7 +1021,7 @@ class Plan(object):
1021
1021
  Refer to companion document for explanations.
1022
1022
  All binary variables must be lumped at the end of the vector.
1023
1023
  """
1024
- medi = options.get("optimizeMedicare", False)
1024
+ medi = options.get("withMedicare", "loop") == "optimize"
1025
1025
 
1026
1026
  # Stack all variables in a single block vector with all binary variables at the end.
1027
1027
  C = {}
@@ -1223,8 +1223,8 @@ class Plan(object):
1223
1223
 
1224
1224
  for i in range(self.N_i):
1225
1225
  for j in range(self.N_j):
1226
- backTau = 1 - yearSpent * np.sum(self.tau_kn[:, 0] * self.alpha_ijkn[i, j, :, 0])
1227
- rhs = self.beta_ij[i, j] * backTau
1226
+ backTau = 1 + yearSpent * np.sum(self.tau_kn[:, 0] * self.alpha_ijkn[i, j, :, 0])
1227
+ rhs = self.beta_ij[i, j] / backTau
1228
1228
  self.B.setRange(_q3(self.C["b"], i, j, 0, self.N_i, self.N_j, self.N_n + 1), rhs, rhs)
1229
1229
 
1230
1230
  def _add_surplus_deposit_linking(self):
@@ -1386,7 +1386,7 @@ class Plan(object):
1386
1386
  self.B.setRange(_q3(self.C["zx"], i, n, 1, self.N_i, self.N_n, self.N_zx), 0, 0)
1387
1387
 
1388
1388
  def _configure_Medicare_binary_variables(self, options):
1389
- if not options.get("optimizeMedicare", False):
1389
+ if options.get("withMedicare", "loop") != "optimize":
1390
1390
  return
1391
1391
 
1392
1392
  bigM = options.get("bigM", 5e6)
@@ -1444,7 +1444,7 @@ class Plan(object):
1444
1444
  self.A.addRow(row2, -np.inf, rhs2)
1445
1445
 
1446
1446
  def _add_Medicare_costs(self, options):
1447
- if not options.get("optimizeMedicare", False):
1447
+ if options.get("withMedicare", "loop") != "optimize":
1448
1448
  return
1449
1449
 
1450
1450
  for n in range(self.nm):
@@ -1627,7 +1627,7 @@ class Plan(object):
1627
1627
  "netSpending",
1628
1628
  "noRothConversions",
1629
1629
  "oppCostX",
1630
- "optimizeMedicare",
1630
+ "withMedicare",
1631
1631
  "previousMAGIs",
1632
1632
  "solver",
1633
1633
  "spendingSlack",
@@ -1635,7 +1635,6 @@ class Plan(object):
1635
1635
  "units",
1636
1636
  "xorConstraints",
1637
1637
  "withSCLoop",
1638
- "withMedicare", # Ignore keyword.
1639
1638
  ]
1640
1639
  # We might modify options if required.
1641
1640
  options = {} if options is None else options
@@ -1713,7 +1712,7 @@ class Plan(object):
1713
1712
  """
1714
1713
  Self-consistent loop, regardless of solver.
1715
1714
  """
1716
- optimizeMedicare = options.get("optimizeMedicare", False)
1715
+ includeMedicare = options.get("withMedicare", "loop") == "loop"
1717
1716
  withSCLoop = options.get("withSCLoop", True)
1718
1717
 
1719
1718
  if objective == "maxSpending":
@@ -1724,7 +1723,7 @@ class Plan(object):
1724
1723
  it = 0
1725
1724
  old_x = np.zeros(self.nvars)
1726
1725
  old_objfns = [np.inf]
1727
- self._computeNLstuff(None, optimizeMedicare)
1726
+ self._computeNLstuff(None, includeMedicare)
1728
1727
  while True:
1729
1728
  objfn, xx, solverSuccess, solverMsg = solverMethod(objective, options)
1730
1729
 
@@ -1735,7 +1734,7 @@ class Plan(object):
1735
1734
  if not withSCLoop:
1736
1735
  break
1737
1736
 
1738
- self._computeNLstuff(xx, optimizeMedicare)
1737
+ self._computeNLstuff(xx, includeMedicare)
1739
1738
 
1740
1739
  delta = xx - old_x
1741
1740
  absSolDiff = np.sum(np.abs(delta), axis=0)/100
@@ -1955,7 +1954,7 @@ class Plan(object):
1955
1954
 
1956
1955
  return J_n
1957
1956
 
1958
- def _computeNLstuff(self, x, optimizeMedicare):
1957
+ def _computeNLstuff(self, x, includeMedicare):
1959
1958
  """
1960
1959
  Compute MAGI, Medicare costs, long-term capital gain tax rate, and
1961
1960
  net investment income tax (NIIT).
@@ -1972,7 +1971,7 @@ class Plan(object):
1972
1971
  self.J_n = self._computeNIIT(self.MAGI_n, self.I_n, self.Q_n)
1973
1972
  self.psi_n = tx.capitalGainTaxRate(self.N_i, self.MAGI_n, self.gamma_n[:-1], self.n_d, self.N_n)
1974
1973
  # Compute Medicare through self-consistent loop.
1975
- if not optimizeMedicare:
1974
+ if includeMedicare:
1976
1975
  self.M_n = tx.mediCosts(self.yobs, self.horizons, self.MAGI_n, self.prevMAGI, self.gamma_n[:-1], self.N_n)
1977
1976
 
1978
1977
  return None
owlplanner/tax2025.py CHANGED
@@ -70,7 +70,7 @@ taxBrackets_preTCJA = np.array(
70
70
  ]
71
71
  )
72
72
 
73
- # These are 2025 current (adjusted for inflation).
73
+ # These are 2025 current.
74
74
  stdDeduction_OBBBA = np.array([15750, 31500]) # Single, MFJ
75
75
  # These are speculated (adjusted for inflation).
76
76
  stdDeduction_preTCJA = np.array([8300, 16600]) # Single, MFJ
@@ -78,7 +78,7 @@ stdDeduction_preTCJA = np.array([8300, 16600]) # Single, MFJ
78
78
  # These are current (adjusted for inflation) per individual.
79
79
  extra65Deduction = np.array([2000, 1600]) # Single, MFJ
80
80
 
81
- # Thresholds for capital gains (adjusted for inflation).
81
+ # Thresholds setting capital gains brackets 0%, 15%, 20% (adjusted for inflation).
82
82
  capGainRates = np.array(
83
83
  [
84
84
  [48350, 533400],
owlplanner/tax2026.py ADDED
@@ -0,0 +1,339 @@
1
+ """
2
+
3
+ Owl/tax2026
4
+ ---
5
+
6
+ A retirement planner using linear programming optimization.
7
+
8
+ See companion document for a complete explanation and description
9
+ of all variables and parameters.
10
+
11
+ Module to handle all tax calculations.
12
+
13
+ Copyright © 2026 - Martin-D. Lacasse
14
+
15
+ Disclaimers: This code is for educational purposes only and does not constitute financial advice.
16
+
17
+ """
18
+
19
+ import numpy as np
20
+ from datetime import date
21
+
22
+
23
+ ##############################################################################
24
+ # Prepare the data.
25
+
26
+ taxBracketNames = ["10%", "12/15%", "22/25%", "24/28%", "32/33%", "35%", "37/40%"]
27
+
28
+ rates_OBBBA = np.array([0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.370])
29
+ rates_preTCJA = np.array([0.10, 0.15, 0.25, 0.28, 0.33, 0.35, 0.396])
30
+
31
+ ###############################################################################
32
+ # Start of section where rates need to be actualized every year.
33
+ ###############################################################################
34
+ # Single [0] and married filing jointly [1].
35
+
36
+ # These are 2025 current.
37
+ taxBrackets_OBBBA = np.array(
38
+ [
39
+ [12400, 50400, 105700, 201775, 256225, 640600, 9999999],
40
+ [24800, 100800, 211400, 403550, 512450, 768700, 9999999],
41
+ ]
42
+ )
43
+
44
+ irmaaBrackets = np.array(
45
+ [
46
+ [0, 106000, 133000, 167000, 200000, 500000],
47
+ [0, 212000, 266000, 334000, 400000, 750000],
48
+ ]
49
+ )
50
+
51
+ # Index [0] stores the standard Medicare part B premium.
52
+ # Following values are incremental IRMAA part B monthly fees.
53
+ irmaaFees = 12 * np.array([185.00, 74.00, 111.00, 110.90, 111.00, 37.00])
54
+
55
+ # Make projection for pre-TCJA using 2017 to current year.
56
+ # taxBrackets_2017 = np.array(
57
+ # [ [9325, 37950, 91900, 191650, 416700, 418400, 9999999],
58
+ # [18650, 75900, 153100, 233350, 416700, 470700, 9999999],
59
+ # ])
60
+ #
61
+ # stdDeduction_2017 = [6350, 12700]
62
+ #
63
+ # For 2025, I used a 30.5% adjustment from 2017, rounded to closest 50.
64
+ #
65
+ # These are speculated.
66
+ taxBrackets_preTCJA = np.array(
67
+ [
68
+ [12150, 49550, 119950, 250200, 544000, 546200, 9999999], # Single
69
+ [24350, 99100, 199850, 304600, 543950, 614450, 9999999], # MFJ
70
+ ]
71
+ )
72
+
73
+ # These are 2025 current.
74
+ stdDeduction_OBBBA = np.array([16100, 32200]) # Single, MFJ
75
+ # These are speculated (adjusted for inflation to 2026). TODO
76
+ stdDeduction_preTCJA = np.array([8300, 16600]) # Single, MFJ
77
+
78
+ # These are current (adjusted for inflation) per individual.
79
+ extra65Deduction = np.array([2000, 1600]) # Single, MFJ
80
+
81
+ # Thresholds setting capital gains brackets 0%, 15%, 20% (adjusted for inflation).
82
+ capGainRates = np.array(
83
+ [
84
+ [48350, 533400],
85
+ [96700, 600050],
86
+ ]
87
+ )
88
+
89
+ # Thresholds for net investment income tax (not adjusted for inflation).
90
+ niitThreshold = np.array([200000, 250000])
91
+ niitRate = 0.038
92
+
93
+ # Thresholds for 65+ bonus for circumventing tax on social security.
94
+ bonusThreshold = np.array([75000, 150000])
95
+
96
+ ###############################################################################
97
+ # End of section where rates need to be actualized every year.
98
+ ###############################################################################
99
+
100
+
101
+ def mediVals(yobs, horizons, gamma_n, Nn, Nq):
102
+ """
103
+ Return tuple (nm, L, C) of year index when Medicare starts and vectors L, and C
104
+ defining end points of constant piecewise linear functions representing IRMAA fees.
105
+ """
106
+ thisyear = date.today().year
107
+ assert Nq == len(irmaaFees), f"Inconsistent value of Nq: {Nq}."
108
+ assert Nq == len(irmaaBrackets[0]), "Inconsistent IRMAA brackets array."
109
+ Ni = len(yobs)
110
+ # What index year will Medicare start? 65 - age.
111
+ nm = 65 - (thisyear - yobs)
112
+ nm = np.min(nm)
113
+ # Has it already started?
114
+ nm = max(0, nm)
115
+ Nmed = Nn - nm
116
+
117
+ L = np.zeros((Nmed, Nq-1))
118
+ C = np.zeros((Nmed, Nq))
119
+
120
+ # Year starts at offset nm in the plan.
121
+ for nn in range(Nmed):
122
+ imed = 0
123
+ n = nm + nn
124
+ if thisyear + n - yobs[0] >= 65 and n < horizons[0]:
125
+ imed += 1
126
+ if Ni == 2 and thisyear + n - yobs[1] >= 65 and n < horizons[1]:
127
+ imed += 1
128
+ if imed:
129
+ status = 0 if Ni == 1 else 1 if n < horizons[0] and n < horizons[1] else 0
130
+ L[nn] = gamma_n[n] * irmaaBrackets[status][1:]
131
+ C[nn] = imed * gamma_n[n] * irmaaFees
132
+ else:
133
+ raise RuntimeError("mediVals: This should never happen.")
134
+
135
+ return nm, L, C
136
+
137
+
138
+ def capitalGainTaxRate(Ni, magi_n, gamma_n, nd, Nn):
139
+ """
140
+ Return an array of decimal rates for capital gains.
141
+ Parameter nd is the index year of first passing of a spouse, if applicable,
142
+ nd == Nn for single individuals.
143
+ """
144
+ status = Ni - 1
145
+ cgRate_n = np.zeros(Nn)
146
+
147
+ for n in range(Nn):
148
+ if status and n == nd:
149
+ status -= 1
150
+
151
+ if magi_n[n] > gamma_n[n] * capGainRates[status][1]:
152
+ cgRate_n[n] = 0.20
153
+ elif magi_n[n] > gamma_n[n] * capGainRates[status][0]:
154
+ cgRate_n[n] = 0.15
155
+
156
+ return cgRate_n
157
+
158
+
159
+ def mediCosts(yobs, horizons, magi, prevmagi, gamma_n, Nn):
160
+ """
161
+ Compute Medicare costs directly.
162
+ """
163
+ thisyear = date.today().year
164
+ Ni = len(yobs)
165
+ costs = np.zeros(Nn)
166
+ for n in range(Nn):
167
+ status = 0 if Ni == 1 else 1 if n < horizons[0] and n < horizons[1] else 0
168
+ for i in range(Ni):
169
+ if thisyear + n - yobs[i] >= 65 and n < horizons[i]:
170
+ # Start with the (inflation-adjusted) basic Medicare part B premium.
171
+ costs[n] += gamma_n[n] * irmaaFees[0]
172
+ if n < 2:
173
+ mymagi = prevmagi[n]
174
+ else:
175
+ mymagi = magi[n - 2]
176
+ for q in range(1, 6):
177
+ if mymagi > gamma_n[n] * irmaaBrackets[status][q]:
178
+ costs[n] += gamma_n[n] * irmaaFees[q]
179
+
180
+ return costs
181
+
182
+
183
+ def taxParams(yobs, i_d, n_d, N_n, gamma_n, MAGI_n, yOBBBA=2099):
184
+ """
185
+ Input is year of birth, index of shortest-lived individual,
186
+ lifespan of shortest-lived individual, total number of years
187
+ in the plan, and the year that preTCJA rates might come back.
188
+
189
+ It returns 3 time series:
190
+ 1) Standard deductions at year n (sigma_n).
191
+ 2) Tax rate in year n (theta_tn)
192
+ 3) Delta from top to bottom of tax brackets (Delta_tn)
193
+ This is pure speculation on future values.
194
+ Returned values are not indexed for inflation.
195
+ """
196
+ # Compute the deltas in-place between brackets, starting from the end.
197
+ deltaBrackets_OBBBA = np.array(taxBrackets_OBBBA)
198
+ deltaBrackets_preTCJA = np.array(taxBrackets_preTCJA)
199
+ for t in range(6, 0, -1):
200
+ for i in range(2):
201
+ deltaBrackets_OBBBA[i, t] -= deltaBrackets_OBBBA[i, t - 1]
202
+ deltaBrackets_preTCJA[i, t] -= deltaBrackets_preTCJA[i, t - 1]
203
+
204
+ # Prepare the 3 arrays to return - use transpose for easy slicing.
205
+ sigmaBar = np.zeros((N_n))
206
+ Delta = np.zeros((N_n, 7))
207
+ theta = np.zeros((N_n, 7))
208
+
209
+ filingStatus = len(yobs) - 1
210
+ souls = list(range(len(yobs)))
211
+ thisyear = date.today().year
212
+
213
+ for n in range(N_n):
214
+ # First check if shortest-lived individual is still with us.
215
+ if n == n_d:
216
+ souls.remove(i_d)
217
+ filingStatus -= 1
218
+
219
+ if thisyear + n < yOBBBA:
220
+ sigmaBar[n] = stdDeduction_OBBBA[filingStatus] * gamma_n[n]
221
+ Delta[n, :] = deltaBrackets_OBBBA[filingStatus, :]
222
+ else:
223
+ sigmaBar[n] = stdDeduction_preTCJA[filingStatus] * gamma_n[n]
224
+ Delta[n, :] = deltaBrackets_preTCJA[filingStatus, :]
225
+
226
+ # Add 65+ additional exemption(s) and "bonus" phasing out.
227
+ for i in souls:
228
+ if thisyear + n - yobs[i] >= 65:
229
+ sigmaBar[n] += extra65Deduction[filingStatus] * gamma_n[n]
230
+ if thisyear + n <= 2028:
231
+ sigmaBar[n] += 6000 * max(0, 1 - 0.06*max(0, MAGI_n[n] - bonusThreshold[filingStatus]))
232
+
233
+ # Fill in future tax rates for year n.
234
+ if thisyear + n < yOBBBA:
235
+ theta[n, :] = rates_OBBBA[:]
236
+ else:
237
+ theta[n, :] = rates_preTCJA[:]
238
+
239
+ Delta = Delta.transpose()
240
+ theta = theta.transpose()
241
+
242
+ # Return series unadjusted for inflation, except for sigmaBar, in STD order.
243
+ return sigmaBar, theta, Delta
244
+
245
+
246
+ def taxBrackets(N_i, n_d, N_n, yOBBBA=2099):
247
+ """
248
+ Return dictionary containing future tax brackets
249
+ unadjusted for inflation for plotting.
250
+ """
251
+ if not (0 < N_i <= 2):
252
+ raise ValueError(f"Cannot process {N_i} individuals.")
253
+
254
+ n_d = min(n_d, N_n)
255
+ status = N_i - 1
256
+
257
+ # Number of years left in OBBBA from this year.
258
+ thisyear = date.today().year
259
+ if yOBBBA < thisyear:
260
+ raise ValueError(f"Expiration year {yOBBBA} cannot be in the past.")
261
+
262
+ ytc = yOBBBA - thisyear
263
+
264
+ data = {}
265
+ for t in range(len(taxBracketNames) - 1):
266
+ array = np.zeros(N_n)
267
+ for n in range(N_n):
268
+ stat = status if n < n_d else 0
269
+ array[n] = taxBrackets_OBBBA[stat][t] if n < ytc else taxBrackets_preTCJA[stat][t]
270
+
271
+ data[taxBracketNames[t]] = array
272
+
273
+ return data
274
+
275
+
276
+ def rho_in(yobs, N_n):
277
+ """
278
+ Return Required Minimum Distribution fractions for each individual.
279
+ This implementation does not support spouses with more than
280
+ 10-year difference.
281
+ It starts at age 73 until it goes to 75 in 2033.
282
+ """
283
+ # Notice that table starts at age 72.
284
+ rmdTable = [
285
+ 27.4,
286
+ 26.5,
287
+ 25.5,
288
+ 24.6,
289
+ 23.7,
290
+ 22.9,
291
+ 22.0,
292
+ 21.1,
293
+ 20.2,
294
+ 19.4,
295
+ 18.5,
296
+ 17.7,
297
+ 16.8,
298
+ 16.0,
299
+ 15.2,
300
+ 14.4,
301
+ 13.7,
302
+ 12.9,
303
+ 12.2,
304
+ 11.5,
305
+ 10.8,
306
+ 10.1,
307
+ 9.5,
308
+ 8.9,
309
+ 8.4,
310
+ 7.8,
311
+ 7.3,
312
+ 6.8,
313
+ 6.4,
314
+ 6.0,
315
+ 5.6,
316
+ 5.2,
317
+ 4.9,
318
+ 4.6,
319
+ ]
320
+
321
+ N_i = len(yobs)
322
+ if N_i == 2 and abs(yobs[0] - yobs[1]) > 10:
323
+ raise RuntimeError("RMD: Unsupported age difference of more than 10 years.")
324
+
325
+ rho = np.zeros((N_i, N_n))
326
+ thisyear = date.today().year
327
+ for i in range(N_i):
328
+ agenow = thisyear - yobs[i]
329
+ # Account for increase of RMD age between 2023 and 2032.
330
+ yrmd = 70 if yobs[i] < 1949 else 72 if 1949 <= yobs[i] <= 1950 else 73 if 1951 <= yobs[i] <= 1959 else 75
331
+ for n in range(N_n):
332
+ yage = agenow + n
333
+
334
+ if yage < yrmd:
335
+ pass # rho[i][n] = 0
336
+ else:
337
+ rho[i][n] = 1.0 / rmdTable[yage - 72]
338
+
339
+ return rho
owlplanner/timelists.py CHANGED
@@ -54,7 +54,7 @@ def read(finput, inames, horizons, mylog):
54
54
  else:
55
55
  # Read all worksheets in memory but only process those with proper names.
56
56
  try:
57
- dfDict = pd.read_excel(finput, sheet_name=None)
57
+ dfDict = pd.read_excel(finput, sheet_name=None, usecols=_timeHorizonItems)
58
58
  except Exception as e:
59
59
  raise Exception(f"Could not read file {finput}: {e}.") from e
60
60
  streamName = f"file '{finput}'"
owlplanner/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2025.08.01"
1
+ __version__ = "2025.11.02"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owlplanner
3
- Version: 2025.8.1
3
+ Version: 2025.11.2
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
@@ -709,7 +709,7 @@ Description-Content-Type: text/markdown
709
709
 
710
710
  ## A retirement exploration tool based on linear programming
711
711
 
712
- <img align=right src="https://raw.github.com/mdlacasse/Owl/main/docs/images/owl.png" width="250">
712
+ <img align=right src="https://github.com/mdlacasse/Owl/blob/main/docs/images/owl.png?raw=true" width="250">
713
713
 
714
714
  -------------------------------------------------------------------------------------
715
715
 
@@ -739,12 +739,12 @@ Strictly speaking, it is not a planning tool, but more an environment for explor
739
739
  It provides different realizations of a financial strategy through the rigorous
740
740
  mathematical optimization of relevant decision variables. Two major objective goals can be set: either
741
741
  maximize net spending, or after-tax bequest under various constraints.
742
- Look at *Basic capabilities* below for more detail.
742
+ Look at the *Capabilities* section below for more detail.
743
743
 
744
744
  One can certainly have a savings plan, but due to the volatility of financial investments,
745
745
  it is impossible to have a certain asset earnings plan. This does not mean one cannot make decisions.
746
746
  These decisions need to be guided with an understanding of the sensitivity of the parameters.
747
- This is exactly where this tool fits it. Given your savings capabilities and spending desires,
747
+ This is exactly where this tool fits in. Given your savings capabilities and spending desires,
748
748
  it can generate different future realizations of
749
749
  your strategy under different market assumptions, helping to better understand your financial situation.
750
750
 
@@ -758,7 +758,7 @@ collecting your data, or academic papers that share the results without really s
758
758
  the underlying mathematical models.
759
759
  The algorithms in Owl rely on the open-source HiGHS linear programming solver. The complete formulation and
760
760
  detailed description of the underlying
761
- mathematical model can be found [here](https://raw.github.com/mdlacasse/Owl/main/docs/owl.pdf).
761
+ mathematical model can be found [here](https://github.com/mdlacasse/Owl/blob/main/docs/owl.pdf).
762
762
 
763
763
  It is anticipated that most end users will use Owl through the graphical interface
764
764
  either at [owlplanner.streamlit.app](https://owlplanner.streamlit.app)
@@ -768,7 +768,7 @@ as described [here](USER_GUIDE.md).
768
768
 
769
769
  Not every retirement decision strategy can be framed as an easy-to-solve optimization problem.
770
770
  In particular, if one is interested in comparing different withdrawal strategies,
771
- [FI Calc](ficalc.app) is an elegant application that addresses this need.
771
+ [FI Calc](https://ficalc.app) is an elegant application that addresses this need.
772
772
  If, however, you also want to optimize spending, bequest, and Roth conversions, with
773
773
  an approach also considering Medicare and federal income tax over the next few years,
774
774
  then Owl is definitely a tool that can help guide your decisions.
@@ -840,7 +840,9 @@ Tax status covers married filing jointly and single, depending on the number of
840
840
  Maturation rules for Roth contributions and conversions are implemented as constraints
841
841
  limiting withdrawal amounts to cover Roth account balances for 5 years after the events.
842
842
  Medicare and IRMAA calculations are performed through a self-consistent loop on cash flow constraints.
843
- Future values are simple projections of current values with the assumed inflation rates.
843
+ They can also be optimized explicitly as an option, but this choice can lead to longer calculations
844
+ due to the use of the many additional binary variables required by the formulation.
845
+ Future Medicare and IRMAA values are simple projections of current values with the assumed inflation rates.
844
846
 
845
847
  ### Limitations
846
848
  Owl is work in progress. At the current time:
@@ -851,15 +853,16 @@ These cases are detected and will generate an error message.
851
853
  - Social security rule for surviving spouse assumes that benefits were taken at full retirement age.
852
854
  - Current version has no optimization of asset allocations between individuals and/or types of savings accounts.
853
855
  If there is interest, that could be added in the future.
854
- - In the current implementation, social securiy is always taxed at 85%.
855
- - Medicare calculations are done through a self-consistent loop.
856
- This means that the Medicare premiums are calculated after an initial solution is generated,
856
+ - In the current implementation, social securiy is always taxed at 85%, assuming that your taxable income will be larger than 34 k$ (single) or 44 k$ (married filing jointly).
857
+ - When Medicare calculations are done through a self-consistent loop,
858
+ the Medicare premiums are calculated after an initial solution is generated,
857
859
  and then a new solution is re-generated with these premiums as a constraint.
858
860
  In some situations, when the income (MAGI) is near an IRMAA bracket, oscillatory solutions can arise.
859
861
  While the solutions generated are very close to one another, Owl will pick the smallest solution
860
- for being conservative.
861
- - Part D is not included in the IRMAA calculations. Being considerably more significant,
862
- only Part B is taken into account.
862
+ for being conservative. While sometimes computationally costly,
863
+ a comparison with a full Medicare optimization should always be performed.
864
+ - Part D is not included in the IRMAA calculations. Only Part B is taken into account,
865
+ which is considerably more significant.
863
866
  - Future tax brackets are pure speculations derived from the little we know now and projected to the next 30 years.
864
867
  Your guesses are as good as mine.
865
868
 
@@ -886,8 +889,11 @@ assets to support, even with no estate being left.
886
889
  - Optimization solver from [HiGHS](https://highs.dev)
887
890
  - Streamlit Community Cloud [Streamlit](https://streamlit.io)
888
891
  - Contributors: Josh (noimjosh@gmail.com) for Docker image code,
892
+ kg333 for fixing an error in Docker's instructions,
889
893
  Dale Seng (sengsational) for great insights and suggestions,
890
- Robert E. Anderson (NH-RedAnt) for bug fixes and suggestions, Clark Jefcoat (hubcity) for fruitful interactions.
894
+ Robert E. Anderson (NH-RedAnt) for bug fixes and suggestions,
895
+ Clark Jefcoat (hubcity) for fruitful interactions,
896
+ Benjamin Quinn (blquinn) and Gene Wood (gene1wood) for improvements and bug fixes.
891
897
 
892
898
  ---------------------------------------------------------------------
893
899
 
@@ -1,14 +1,15 @@
1
1
  owlplanner/__init__.py,sha256=hJ2i4m2JpHPAKyQLjYOXpJzeEsgcTcKD-Vhm0AIjjWg,592
2
2
  owlplanner/abcapi.py,sha256=rtg7d0UbftinokR9VlB49VUjDjzUq3ONnJbhMXVIrgo,6879
3
- owlplanner/config.py,sha256=JJOtS6HyqA4qUHUZydSGG_RMWaCfVMRSOAfWbt4evMI,12461
3
+ owlplanner/config.py,sha256=UF2Dy6E9PiX6Ua8B1R0aYCNUoIYmY46up8awf_36B_Q,12615
4
4
  owlplanner/mylogging.py,sha256=OVGeDFO7LIZG91R6HMpZBzjno-B8PH8Fo00Jw2Pdgqw,2558
5
- owlplanner/plan.py,sha256=keKC9-XeQxza9--D7TaQfcZCLkGDy5yVV9D5pN25MHg,115211
5
+ owlplanner/plan.py,sha256=NDdV0Eri76JMpiNPeuMzLYGX-lDDBEbjXd6Qg6k_CIw,115180
6
6
  owlplanner/progress.py,sha256=dUUlFmSAKUei36rUj2BINRY10f_YEUo_e23d0es6nrc,530
7
7
  owlplanner/rates.py,sha256=9Nmo8AKsyi5PoCUrzhr06phkSlNTv-TXzj5iYFU76AY,14113
8
- owlplanner/tax2025.py,sha256=2Jb_UbPT6ye-znRjA0nSaF8T8M17QW4MoRPDoW9XJ8s,10833
9
- owlplanner/timelists.py,sha256=Q4kBt9kKAa5qxsvOe9wfyUtCQVgiwPmJXTwXUPRBBv8,4066
8
+ owlplanner/tax2025.py,sha256=4KYaT6TO6WU7wDjgdRW48lqfwvVCtaXs9tcw1nleKhg,10834
9
+ owlplanner/tax2026.py,sha256=hgCiCJWVzJITk0cA8W-zxl-a0kObijPZ1yXc0F6MAwk,10848
10
+ owlplanner/timelists.py,sha256=UdzH6A_-w4REn4A1po7yndSiy1R8_R-i_C-94re4JYY,4093
10
11
  owlplanner/utils.py,sha256=afAjeO6Msf6Rn4jwz_7Ody9rHGWlBR7iQFqe1xzLNQc,2513
11
- owlplanner/version.py,sha256=Or3KaHd8BXidAsAkScGwQRvtWIF5uufpsnfgNdR-Kpw,28
12
+ owlplanner/version.py,sha256=7KKWNrt_Dn84IwHPr2R5et9VpN8p8GhpeUnuavkZnVA,28
12
13
  owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
14
  owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
14
15
  owlplanner/plotting/__init__.py,sha256=uhxqtUi0OI-QWNOO2LkXgQViW_9yM3rYb-204Wit974,250
@@ -16,7 +17,7 @@ owlplanner/plotting/base.py,sha256=UimGKpMTV-dVm3BX5Apr_Ltorc7dlDLCRPRQ3RF_v7c,2
16
17
  owlplanner/plotting/factory.py,sha256=EDopIAPQr9zHRgemObko18FlCeRNhNCoLNNFAOq-X6s,1030
17
18
  owlplanner/plotting/matplotlib_backend.py,sha256=AOEkapD94U5hGNoS0EdbRoe8mgdMHH4oOvkXADZS914,17957
18
19
  owlplanner/plotting/plotly_backend.py,sha256=AO33GxBHGYG5osir_H1iRRtGxdhs4AjfLV2d_xm35nY,33138
19
- owlplanner-2025.8.1.dist-info/METADATA,sha256=9WRoRbNMdk2kxbT4-Coq912VJixT9rlF1Cv48Po4VJw,54044
20
- owlplanner-2025.8.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
- owlplanner-2025.8.1.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
22
- owlplanner-2025.8.1.dist-info/RECORD,,
20
+ owlplanner-2025.11.2.dist-info/METADATA,sha256=MIS1cRREb2Fe56Qu8BJ_9tF5YXB3gFL2Ldaufm2k0Ak,54622
21
+ owlplanner-2025.11.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
+ owlplanner-2025.11.2.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
23
+ owlplanner-2025.11.2.dist-info/RECORD,,