owlplanner 2025.5.15__py3-none-any.whl → 2025.5.30__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 +6 -7
- owlplanner/plan.py +83 -84
- owlplanner/plotting/base.py +3 -3
- owlplanner/plotting/matplotlib_backend.py +18 -21
- owlplanner/plotting/plotly_backend.py +26 -29
- owlplanner/version.py +1 -1
- {owlplanner-2025.5.15.dist-info → owlplanner-2025.5.30.dist-info}/METADATA +2 -2
- {owlplanner-2025.5.15.dist-info → owlplanner-2025.5.30.dist-info}/RECORD +10 -10
- {owlplanner-2025.5.15.dist-info → owlplanner-2025.5.30.dist-info}/WHEEL +0 -0
- {owlplanner-2025.5.15.dist-info → owlplanner-2025.5.30.dist-info}/licenses/LICENSE +0 -0
owlplanner/config.py
CHANGED
|
@@ -65,7 +65,7 @@ def saveConfig(myplan, file, mylog):
|
|
|
65
65
|
diconf["Rates Selection"] = {
|
|
66
66
|
"Heirs rate on tax-deferred estate": float(100 * myplan.nu),
|
|
67
67
|
"Long-term capital gain tax rate": float(100 * myplan.psi),
|
|
68
|
-
"Dividend
|
|
68
|
+
"Dividend rate": float(100 * myplan.mu),
|
|
69
69
|
"TCJA expiration year": myplan.yTCJA,
|
|
70
70
|
"Method": myplan.rateMethod,
|
|
71
71
|
}
|
|
@@ -182,20 +182,19 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
|
|
|
182
182
|
# status = diconf['Basic Info']['Status']
|
|
183
183
|
yobs = diconf["Basic Info"]["Birth year"]
|
|
184
184
|
expectancy = diconf["Basic Info"]["Life expectancy"]
|
|
185
|
-
startDate = diconf["Basic Info"].get("Start date", "today")
|
|
186
185
|
icount = len(yobs)
|
|
187
186
|
s = ["", "s"][icount - 1]
|
|
188
187
|
mylog.vprint(f"Plan for {icount} individual{s}: {inames}.")
|
|
189
|
-
p = plan.Plan(inames, yobs, expectancy, name,
|
|
188
|
+
p = plan.Plan(inames, yobs, expectancy, name, verbose=True, logstreams=logstreams)
|
|
190
189
|
p._description = diconf.get("Description", "")
|
|
191
190
|
|
|
192
191
|
# Assets.
|
|
192
|
+
startDate = diconf["Basic Info"].get("Start date", "today")
|
|
193
193
|
balances = {}
|
|
194
194
|
for acc in accountTypes:
|
|
195
195
|
balances[acc] = diconf["Assets"][f"{acc} savings balances"]
|
|
196
|
-
p.setAccountBalances(
|
|
197
|
-
|
|
198
|
-
)
|
|
196
|
+
p.setAccountBalances(taxable=balances["taxable"], taxDeferred=balances["tax-deferred"],
|
|
197
|
+
taxFree=balances["tax-free"], startDate=startDate)
|
|
199
198
|
if icount == 2:
|
|
200
199
|
phi_j = diconf["Assets"]["Beneficiary fractions"]
|
|
201
200
|
p.setBeneficiaryFractions(phi_j)
|
|
@@ -227,7 +226,7 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
|
|
|
227
226
|
p.setPension(pensionAmounts, pensionAges, pensionIsIndexed)
|
|
228
227
|
|
|
229
228
|
# Rates Selection.
|
|
230
|
-
p.setDividendRate(float(diconf["Rates Selection"]
|
|
229
|
+
p.setDividendRate(float(diconf["Rates Selection"].get("Dividend rate", 1.8))) # Fix for mod.
|
|
231
230
|
p.setLongTermCapitalTaxRate(float(diconf["Rates Selection"]["Long-term capital gain tax rate"]))
|
|
232
231
|
p.setHeirsTaxRate(float(diconf["Rates Selection"]["Heirs rate on tax-deferred estate"]))
|
|
233
232
|
p.yTCJA = int(diconf["Rates Selection"]["TCJA expiration year"])
|
owlplanner/plan.py
CHANGED
|
@@ -211,7 +211,7 @@ class Plan(object):
|
|
|
211
211
|
This is the main class of the Owl Project.
|
|
212
212
|
"""
|
|
213
213
|
|
|
214
|
-
def __init__(self, inames, yobs, expectancy, name, *,
|
|
214
|
+
def __init__(self, inames, yobs, expectancy, name, *, verbose=False, logstreams=None):
|
|
215
215
|
"""
|
|
216
216
|
Constructor requires three lists: the first
|
|
217
217
|
one contains the name(s) of the individual(s),
|
|
@@ -242,7 +242,9 @@ class Plan(object):
|
|
|
242
242
|
self.defaultPlots = "nominal"
|
|
243
243
|
self.defaultSolver = "HiGHS"
|
|
244
244
|
self._plotterName = None
|
|
245
|
-
|
|
245
|
+
# Pick a default plotting backend here.
|
|
246
|
+
# self.setPlotBackend("matplotlib")
|
|
247
|
+
self.setPlotBackend("plotly")
|
|
246
248
|
|
|
247
249
|
self.N_i = len(yobs)
|
|
248
250
|
if not (0 <= self.N_i <= 2):
|
|
@@ -281,9 +283,9 @@ class Plan(object):
|
|
|
281
283
|
|
|
282
284
|
# Default parameters:
|
|
283
285
|
self.psi = 0.15 # Long-term income tax rate on capital gains (decimal)
|
|
284
|
-
self.chi = 0.6
|
|
285
|
-
self.mu = 0.
|
|
286
|
-
self.nu = 0.30
|
|
286
|
+
self.chi = 0.6 # Survivor fraction
|
|
287
|
+
self.mu = 0.018 # Dividend rate (decimal)
|
|
288
|
+
self.nu = 0.30 # Heirs tax rate (decimal)
|
|
287
289
|
self.eta = (self.N_i - 1) / 2 # Spousal deposit ratio (0 or .5)
|
|
288
290
|
self.phi_j = np.array([1, 1, 1]) # Fractions left to other spouse at death
|
|
289
291
|
self.smileDip = 15 # Percent to reduce smile profile
|
|
@@ -307,6 +309,10 @@ class Plan(object):
|
|
|
307
309
|
# Previous 3 years for Medicare.
|
|
308
310
|
self.prevMAGI = np.zeros((3))
|
|
309
311
|
|
|
312
|
+
# Init previous balance to none.
|
|
313
|
+
self.beta_ij = None
|
|
314
|
+
self.startDate = None
|
|
315
|
+
|
|
310
316
|
# Default slack on profile.
|
|
311
317
|
self.lambdha = 0
|
|
312
318
|
|
|
@@ -320,9 +326,6 @@ class Plan(object):
|
|
|
320
326
|
# Prepare RMD time series.
|
|
321
327
|
self.rho_in = tx.rho_in(self.yobs, self.N_n)
|
|
322
328
|
|
|
323
|
-
# If none was given, default is to begin plan on today's date.
|
|
324
|
-
self._setStartingDate(startDate)
|
|
325
|
-
|
|
326
329
|
self._buildOffsetMap()
|
|
327
330
|
|
|
328
331
|
# Initialize guardrails to ensure proper configuration.
|
|
@@ -363,7 +366,7 @@ class Plan(object):
|
|
|
363
366
|
def _setStartingDate(self, mydate):
|
|
364
367
|
"""
|
|
365
368
|
Set the date when the plan starts in the current year.
|
|
366
|
-
This is for reproducibility purposes.
|
|
369
|
+
This is mostly for reproducibility purposes and back projecting known balances to Jan 1st.
|
|
367
370
|
String format of mydate is 'month/day'.
|
|
368
371
|
"""
|
|
369
372
|
import calendar
|
|
@@ -463,7 +466,7 @@ class Plan(object):
|
|
|
463
466
|
|
|
464
467
|
def setDividendRate(self, mu):
|
|
465
468
|
"""
|
|
466
|
-
Set dividend tax rate. Rate is in percent. Default
|
|
469
|
+
Set dividend tax rate. Rate is in percent. Default 1.8%.
|
|
467
470
|
"""
|
|
468
471
|
if not (0 <= mu <= 100):
|
|
469
472
|
raise ValueError("Rate must be between 0 and 100.")
|
|
@@ -549,9 +552,6 @@ class Plan(object):
|
|
|
549
552
|
ns = max(0, self.yobs[i] + ages[i] - thisyear)
|
|
550
553
|
nd = self.horizons[i]
|
|
551
554
|
self.pi_in[i, ns:nd] = amounts[i]
|
|
552
|
-
# Only include remaining part of current year.
|
|
553
|
-
if ns == 0:
|
|
554
|
-
self.pi_in[i, 0] *= self.yearFracLeft
|
|
555
555
|
|
|
556
556
|
self.pensionAmounts = np.array(amounts)
|
|
557
557
|
self.pensionAges = np.array(ages, dtype=np.int32)
|
|
@@ -583,9 +583,6 @@ class Plan(object):
|
|
|
583
583
|
ns = max(0, self.yobs[i] + ages[i] - thisyear)
|
|
584
584
|
nd = self.horizons[i]
|
|
585
585
|
self.zeta_in[i, ns:nd] = amounts[i]
|
|
586
|
-
# Only include remaining part of current year.
|
|
587
|
-
if ns == 0:
|
|
588
|
-
self.zeta_in[i, 0] *= self.yearFracLeft
|
|
589
586
|
|
|
590
587
|
if self.N_i == 2:
|
|
591
588
|
# Approximate calculation for spousal benefit (only valid at FRA).
|
|
@@ -618,8 +615,6 @@ class Plan(object):
|
|
|
618
615
|
self.mylog.vprint("Securing", u.pc(self.chi, f=0), "of spending amount for surviving spouse.")
|
|
619
616
|
|
|
620
617
|
self.xi_n = _genXi_n(profile, self.chi, self.n_d, self.N_n, dip, increase, delay)
|
|
621
|
-
# Account for time elapsed in the current year.
|
|
622
|
-
self.xi_n[0] *= self.yearFracLeft
|
|
623
618
|
|
|
624
619
|
self.spendingProfile = profile
|
|
625
620
|
self.smileDip = dip
|
|
@@ -654,9 +649,6 @@ class Plan(object):
|
|
|
654
649
|
self.tau_kn = dr.genSeries(self.N_n).transpose()
|
|
655
650
|
self.mylog.vprint(f"Generating rate series of {len(self.tau_kn[0])} years using {method} method.")
|
|
656
651
|
|
|
657
|
-
# Account for how late we are now in the first year and reduce rate accordingly.
|
|
658
|
-
self.tau_kn[:, 0] *= self.yearFracLeft
|
|
659
|
-
|
|
660
652
|
# Once rates are selected, (re)build cumulative inflation multipliers.
|
|
661
653
|
self.gamma_n = _genGamma_n(self.tau_kn)
|
|
662
654
|
self._adjustedParameters = False
|
|
@@ -706,7 +698,7 @@ class Plan(object):
|
|
|
706
698
|
|
|
707
699
|
return amount * self.gamma_n[span]
|
|
708
700
|
|
|
709
|
-
def setAccountBalances(self, *, taxable, taxDeferred, taxFree, units="k"):
|
|
701
|
+
def setAccountBalances(self, *, taxable, taxDeferred, taxFree, startDate=None, units="k"):
|
|
710
702
|
"""
|
|
711
703
|
Three lists containing the balance of all assets in each category for
|
|
712
704
|
each spouse. For single individuals, these lists will contain only
|
|
@@ -725,11 +717,15 @@ class Plan(object):
|
|
|
725
717
|
taxDeferred = u.rescale(taxDeferred, fac)
|
|
726
718
|
taxFree = u.rescale(taxFree, fac)
|
|
727
719
|
|
|
728
|
-
self.
|
|
729
|
-
self.
|
|
730
|
-
self.
|
|
731
|
-
self.
|
|
732
|
-
self.beta_ij = self.
|
|
720
|
+
self.bet_ji = np.zeros((self.N_j, self.N_i))
|
|
721
|
+
self.bet_ji[0][:] = taxable
|
|
722
|
+
self.bet_ji[1][:] = taxDeferred
|
|
723
|
+
self.bet_ji[2][:] = taxFree
|
|
724
|
+
self.beta_ij = self.bet_ji.transpose()
|
|
725
|
+
|
|
726
|
+
# If none was given, default is to begin plan on today's date.
|
|
727
|
+
self._setStartingDate(startDate)
|
|
728
|
+
|
|
733
729
|
self.caseStatus = "modified"
|
|
734
730
|
|
|
735
731
|
self.mylog.vprint("Taxable balances:", *[u.d(taxable[i]) for i in range(self.N_i)])
|
|
@@ -929,10 +925,6 @@ class Plan(object):
|
|
|
929
925
|
self.myRothX_in[i, :h] = self.timeLists[iname]["Roth conv"].iloc[:h]
|
|
930
926
|
self.Lambda_in[i, :h] = self.timeLists[iname]["big-ticket items"].iloc[:h]
|
|
931
927
|
|
|
932
|
-
# In 1st year, reduce wages and contributions depending on starting date.
|
|
933
|
-
self.omega_in[:, 0] *= self.yearFracLeft
|
|
934
|
-
self.kappa_ijn[:, :, 0] *= self.yearFracLeft
|
|
935
|
-
|
|
936
928
|
self.caseStatus = "modified"
|
|
937
929
|
|
|
938
930
|
return self.timeLists
|
|
@@ -1215,13 +1207,17 @@ class Plan(object):
|
|
|
1215
1207
|
spending = options["netSpending"]
|
|
1216
1208
|
if not isinstance(spending, (int, float)):
|
|
1217
1209
|
raise ValueError(f"Desired spending provided {spending} is not a number.")
|
|
1218
|
-
spending *= self.optionsUnits
|
|
1210
|
+
spending *= self.optionsUnits
|
|
1219
1211
|
self.B.setRange(_q1(self.C["g"], 0, self.N_n), spending, spending)
|
|
1220
1212
|
|
|
1221
1213
|
def _add_initial_balances(self):
|
|
1214
|
+
# Back project balances to the beginning of the year.
|
|
1215
|
+
yearSpent = 1 - self.yearFracLeft
|
|
1216
|
+
|
|
1222
1217
|
for i in range(self.N_i):
|
|
1223
1218
|
for j in range(self.N_j):
|
|
1224
|
-
|
|
1219
|
+
backTau = 1 - yearSpent * np.sum(self.tau_kn[:, 0] * self.alpha_ijkn[i, j, :, 0])
|
|
1220
|
+
rhs = self.beta_ij[i, j] * backTau
|
|
1225
1221
|
self.B.setRange(_q3(self.C["b"], i, j, 0, self.N_i, self.N_j, self.N_n + 1), rhs, rhs)
|
|
1226
1222
|
|
|
1227
1223
|
def _add_surplus_deposit_linking(self):
|
|
@@ -2063,43 +2059,43 @@ class Plan(object):
|
|
|
2063
2059
|
# Results
|
|
2064
2060
|
dic["Plan name"] = self._name
|
|
2065
2061
|
dic["Net yearly spending basis"] = u.d(self.g_n[0] / self.xi_n[0])
|
|
2066
|
-
dic[f"Net spending for year {now}"] = u.d(self.g_n[0]
|
|
2067
|
-
dic[f"Net spending remaining in year {now}"] = u.d(self.g_n[0])
|
|
2062
|
+
dic[f"Net spending for year {now}"] = u.d(self.g_n[0])
|
|
2063
|
+
dic[f"Net spending remaining in year {now}"] = u.d(self.g_n[0] * self.yearFracLeft)
|
|
2068
2064
|
|
|
2069
2065
|
totSpending = np.sum(self.g_n, axis=0)
|
|
2070
2066
|
totSpendingNow = np.sum(self.g_n / self.gamma_n[:-1], axis=0)
|
|
2071
|
-
dic["Total net spending"] = f"{u.d(totSpendingNow)}"
|
|
2067
|
+
dic[" Total net spending"] = f"{u.d(totSpendingNow)}"
|
|
2072
2068
|
dic["[Total net spending]"] = f"{u.d(totSpending)}"
|
|
2073
2069
|
|
|
2074
2070
|
totRoth = np.sum(self.x_in, axis=(0, 1))
|
|
2075
2071
|
totRothNow = np.sum(np.sum(self.x_in, axis=0) / self.gamma_n[:-1], axis=0)
|
|
2076
|
-
dic["Total Roth conversions"] = f"{u.d(totRothNow)}"
|
|
2072
|
+
dic[" Total Roth conversions"] = f"{u.d(totRothNow)}"
|
|
2077
2073
|
dic["[Total Roth conversions]"] = f"{u.d(totRoth)}"
|
|
2078
2074
|
|
|
2079
2075
|
taxPaid = np.sum(self.T_n, axis=0)
|
|
2080
2076
|
taxPaidNow = np.sum(self.T_n / self.gamma_n[:-1], axis=0)
|
|
2081
|
-
dic["Total
|
|
2082
|
-
dic["[Total
|
|
2077
|
+
dic[" Total tax paid on ordinary income"] = f"{u.d(taxPaidNow)}"
|
|
2078
|
+
dic["[Total tax paid on ordinary income]"] = f"{u.d(taxPaid)}"
|
|
2083
2079
|
for t in range(self.N_t):
|
|
2084
2080
|
taxPaid = np.sum(self.T_tn[t], axis=0)
|
|
2085
2081
|
taxPaidNow = np.sum(self.T_tn[t] / self.gamma_n[:-1], axis=0)
|
|
2086
2082
|
tname = tx.taxBracketNames[t]
|
|
2087
|
-
dic[f"
|
|
2088
|
-
dic[f"
|
|
2083
|
+
dic[f"» Subtotal in tax bracket {tname}"] = f"{u.d(taxPaidNow)}"
|
|
2084
|
+
dic[f"» [Subtotal in tax bracket {tname}]"] = f"{u.d(taxPaid)}"
|
|
2089
2085
|
|
|
2090
2086
|
penaltyPaid = np.sum(self.P_n, axis=0)
|
|
2091
2087
|
penaltyPaidNow = np.sum(self.P_n / self.gamma_n[:-1], axis=0)
|
|
2092
|
-
dic["
|
|
2093
|
-
dic["
|
|
2088
|
+
dic["» Subtotal in early withdrawal penalty"] = f"{u.d(penaltyPaidNow)}"
|
|
2089
|
+
dic["» [Subtotal in early withdrawal penalty]"] = f"{u.d(penaltyPaid)}"
|
|
2094
2090
|
|
|
2095
2091
|
taxPaid = np.sum(self.U_n, axis=0)
|
|
2096
2092
|
taxPaidNow = np.sum(self.U_n / self.gamma_n[:-1], axis=0)
|
|
2097
|
-
dic["Total tax paid on gains and dividends"] = f"{u.d(taxPaidNow)}"
|
|
2093
|
+
dic[" Total tax paid on gains and dividends"] = f"{u.d(taxPaidNow)}"
|
|
2098
2094
|
dic["[Total tax paid on gains and dividends]"] = f"{u.d(taxPaid)}"
|
|
2099
2095
|
|
|
2100
2096
|
taxPaid = np.sum(self.M_n, axis=0)
|
|
2101
2097
|
taxPaidNow = np.sum(self.M_n / self.gamma_n[:-1], axis=0)
|
|
2102
|
-
dic["Total Medicare premiums paid"] = f"{u.d(taxPaidNow)}"
|
|
2098
|
+
dic[" Total Medicare premiums paid"] = f"{u.d(taxPaidNow)}"
|
|
2103
2099
|
dic["[Total Medicare premiums paid]"] = f"{u.d(taxPaid)}"
|
|
2104
2100
|
|
|
2105
2101
|
if self.N_i == 2 and self.n_d < self.N_n:
|
|
@@ -2107,50 +2103,51 @@ class Plan(object):
|
|
|
2107
2103
|
p_j[1] *= 1 - self.nu
|
|
2108
2104
|
nx = self.n_d - 1
|
|
2109
2105
|
ynx = self.year_n[nx]
|
|
2106
|
+
ynxNow = 1./self.gamma_n[nx + 1]
|
|
2110
2107
|
totOthers = np.sum(p_j)
|
|
2111
|
-
totOthersNow = totOthers / self.gamma_n[nx + 1]
|
|
2112
2108
|
q_j = self.partialEstate_j * self.phi_j
|
|
2113
2109
|
totSpousal = np.sum(q_j)
|
|
2114
|
-
totSpousalNow = totSpousal / self.gamma_n[nx + 1]
|
|
2115
2110
|
iname_s = self.inames[self.i_s]
|
|
2116
2111
|
iname_d = self.inames[self.i_d]
|
|
2117
|
-
dic[
|
|
2118
|
-
dic[f"
|
|
2119
|
-
|
|
2120
|
-
dic[f"
|
|
2121
|
-
|
|
2122
|
-
dic[f"
|
|
2123
|
-
|
|
2124
|
-
dic[f"
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
dic[f"Sum of post-tax non-spousal
|
|
2128
|
-
|
|
2129
|
-
dic[f"
|
|
2130
|
-
|
|
2131
|
-
dic[f"
|
|
2132
|
-
|
|
2133
|
-
dic[f"
|
|
2134
|
-
|
|
2135
|
-
dic[f"-- [Post-tax non-spousal bequests from {iname_d} in year {ynx} - tax-free]"] = (
|
|
2136
|
-
f"{u.d(p_j[2])}")
|
|
2112
|
+
dic["Year of partial bequest"] = (f"{ynx}")
|
|
2113
|
+
dic[f" Sum of spousal transfer to {iname_s}"] = (f"{u.d(ynxNow*totSpousal)}")
|
|
2114
|
+
dic[f"[Sum of spousal transfer to {iname_s}]"] = (f"{u.d(totSpousal)}")
|
|
2115
|
+
dic[f"» Spousal transfer to {iname_s} - taxable"] = (f"{u.d(ynxNow*q_j[0])}")
|
|
2116
|
+
dic[f"» [Spousal transfer to {iname_s} - taxable]"] = (f"{u.d(q_j[0])}")
|
|
2117
|
+
dic[f"» Spousal transfer to {iname_s} - tax-def"] = (f"{u.d(ynxNow*q_j[1])}")
|
|
2118
|
+
dic[f"» [Spousal transfer to {iname_s} - tax-def]"] = (f"{u.d(q_j[1])}")
|
|
2119
|
+
dic[f"» Spousal transfer to {iname_s} - tax-free"] = (f"{u.d(ynxNow*q_j[2])}")
|
|
2120
|
+
dic[f"» [Spousal transfer to {iname_s} - tax-free]"] = (f"{u.d(q_j[2])}")
|
|
2121
|
+
|
|
2122
|
+
dic[f" Sum of post-tax non-spousal bequest from {iname_d}"] = (f"{u.d(ynxNow*totOthers)}")
|
|
2123
|
+
dic[f"[Sum of post-tax non-spousal bequest from {iname_d}]"] = (f"{u.d(totOthers)}")
|
|
2124
|
+
dic[f"» Post-tax non-spousal bequest from {iname_d} - taxable"] = (f"{u.d(ynxNow*p_j[0])}")
|
|
2125
|
+
dic[f"» [Post-tax non-spousal bequest from {iname_d} - taxable]"] = (f"{u.d(p_j[0])}")
|
|
2126
|
+
dic[f"» Post-tax non-spousal bequest from {iname_d} - tax-def"] = (f"{u.d(ynxNow*p_j[1])}")
|
|
2127
|
+
dic[f"» [Post-tax non-spousal bequest from {iname_d} - tax-def]"] = (f"{u.d(p_j[1])}")
|
|
2128
|
+
dic[f"» Post-tax non-spousal bequest from {iname_d} - tax-free"] = (f"{u.d(ynxNow*p_j[2])}")
|
|
2129
|
+
dic[f"» [Post-tax non-spousal bequest from {iname_d} - tax-free]"] = (f"{u.d(p_j[2])}")
|
|
2137
2130
|
|
|
2138
2131
|
estate = np.sum(self.b_ijn[:, :, self.N_n], axis=0)
|
|
2139
2132
|
estate[1] *= 1 - self.nu
|
|
2140
|
-
|
|
2133
|
+
endyear = self.year_n[-1]
|
|
2134
|
+
lyNow = 1./self.gamma_n[-1]
|
|
2141
2135
|
totEstate = np.sum(estate)
|
|
2142
|
-
|
|
2143
|
-
dic[
|
|
2144
|
-
dic[
|
|
2145
|
-
dic[
|
|
2146
|
-
dic[
|
|
2147
|
-
dic[
|
|
2136
|
+
dic["Year of final bequest"] = (f"{endyear}")
|
|
2137
|
+
dic[" Total value of final bequest"] = (f"{u.d(lyNow*totEstate)}")
|
|
2138
|
+
dic["[Total value of final bequest]"] = (f"{u.d(totEstate)}")
|
|
2139
|
+
dic["» Post-tax final bequest account value - taxable"] = (f"{u.d(lyNow*estate[0])}")
|
|
2140
|
+
dic["» [Post-tax final bequest account value - taxable]"] = (f"{u.d(estate[0])}")
|
|
2141
|
+
dic["» Post-tax final bequest account value - tax-def"] = (f"{u.d(lyNow*estate[1])}")
|
|
2142
|
+
dic["» [Post-tax final bequest account value - tax-def]"] = (f"{u.d(estate[1])}")
|
|
2143
|
+
dic["» Post-tax final bequest account value - tax-free"] = (f"{u.d(lyNow*estate[2])}")
|
|
2144
|
+
dic["» [Post-tax final bequest account value - tax-free]"] = (f"{u.d(estate[2])}")
|
|
2148
2145
|
|
|
2149
2146
|
dic["Plan starting date"] = str(self.startDate)
|
|
2150
|
-
dic[
|
|
2147
|
+
dic["Cumulative inflation factor at end of final year"] = (f"{self.gamma_n[-1]:.2f}")
|
|
2151
2148
|
for i in range(self.N_i):
|
|
2152
|
-
dic[f"{self.inames[i]:>
|
|
2153
|
-
|
|
2149
|
+
dic[f"{self.inames[i]:>14}'s life horizon"] = (f"{now} -> {now + self.horizons[i] - 1}")
|
|
2150
|
+
dic[f"{self.inames[i]:>14}'s years planned"] = (f"{self.horizons[i]}")
|
|
2154
2151
|
|
|
2155
2152
|
dic["Plan name"] = self._name
|
|
2156
2153
|
dic["Number of decision variables"] = str(self.A.nvars)
|
|
@@ -2200,7 +2197,7 @@ class Plan(object):
|
|
|
2200
2197
|
self.mylog.vprint("Warning: Rate method must be selected before plotting.")
|
|
2201
2198
|
return None
|
|
2202
2199
|
|
|
2203
|
-
fig = self._plotter.plot_rates(self._name, self.tau_kn, self.year_n,
|
|
2200
|
+
fig = self._plotter.plot_rates(self._name, self.tau_kn, self.year_n,
|
|
2204
2201
|
self.N_k, self.rateMethod, self.rateFrm, self.rateTo, tag)
|
|
2205
2202
|
|
|
2206
2203
|
if figure:
|
|
@@ -2252,9 +2249,9 @@ class Plan(object):
|
|
|
2252
2249
|
return None
|
|
2253
2250
|
|
|
2254
2251
|
@_checkCaseStatus
|
|
2255
|
-
def
|
|
2252
|
+
def showAssetComposition(self, tag="", value=None, figure=False):
|
|
2256
2253
|
"""
|
|
2257
|
-
Plot the
|
|
2254
|
+
Plot the composition of each savings account in thousands of dollars
|
|
2258
2255
|
during the simulation time. This function will generate three
|
|
2259
2256
|
graphs, one for taxable accounts, one the tax-deferred accounts,
|
|
2260
2257
|
and one for tax-free accounts.
|
|
@@ -2265,8 +2262,8 @@ class Plan(object):
|
|
|
2265
2262
|
the default behavior of setDefaultPlots().
|
|
2266
2263
|
"""
|
|
2267
2264
|
value = self._checkValue(value)
|
|
2268
|
-
figures = self._plotter.
|
|
2269
|
-
|
|
2265
|
+
figures = self._plotter.plot_asset_composition(self.year_n, self.inames, self.b_ijkn,
|
|
2266
|
+
self.gamma_n, value, self._name, tag)
|
|
2270
2267
|
if figure:
|
|
2271
2268
|
return figures
|
|
2272
2269
|
|
|
@@ -2376,7 +2373,9 @@ class Plan(object):
|
|
|
2376
2373
|
title = self._name + "\nIncome Tax"
|
|
2377
2374
|
if tag:
|
|
2378
2375
|
title += " - " + tag
|
|
2379
|
-
|
|
2376
|
+
# All taxes: ordinary income and dividends.
|
|
2377
|
+
allTaxes = self.T_n + self.U_n
|
|
2378
|
+
fig = self._plotter.plot_taxes(self.year_n, allTaxes, self.M_n, self.gamma_n,
|
|
2380
2379
|
value, title, self.inames)
|
|
2381
2380
|
if figure:
|
|
2382
2381
|
return fig
|
|
@@ -2512,6 +2511,7 @@ class Plan(object):
|
|
|
2512
2511
|
self.b_ijn[i][0][-1],
|
|
2513
2512
|
0,
|
|
2514
2513
|
0,
|
|
2514
|
+
0,
|
|
2515
2515
|
self.b_ijn[i][1][-1],
|
|
2516
2516
|
0,
|
|
2517
2517
|
0,
|
|
@@ -2520,7 +2520,6 @@ class Plan(object):
|
|
|
2520
2520
|
self.b_ijn[i][2][-1],
|
|
2521
2521
|
0,
|
|
2522
2522
|
0,
|
|
2523
|
-
0,
|
|
2524
2523
|
]
|
|
2525
2524
|
ws.append(lastRow)
|
|
2526
2525
|
_formatSpreadsheet(ws, "currency")
|
owlplanner/plotting/base.py
CHANGED
|
@@ -25,7 +25,7 @@ class PlotBackend(ABC):
|
|
|
25
25
|
pass
|
|
26
26
|
|
|
27
27
|
@abstractmethod
|
|
28
|
-
def plot_rates(self, name, tau_kn, year_n,
|
|
28
|
+
def plot_rates(self, name, tau_kn, year_n, N_k, rate_method,
|
|
29
29
|
rate_frm=None, rate_to=None, tag=""):
|
|
30
30
|
"""Plot rate values used over the time horizon."""
|
|
31
31
|
pass
|
|
@@ -51,8 +51,8 @@ class PlotBackend(ABC):
|
|
|
51
51
|
pass
|
|
52
52
|
|
|
53
53
|
@abstractmethod
|
|
54
|
-
def
|
|
55
|
-
"""Plot asset
|
|
54
|
+
def plot_asset_composition(self, year_n, inames, b_ijkn, gamma_n, value, name, tag):
|
|
55
|
+
"""Plot asset composition over time."""
|
|
56
56
|
pass
|
|
57
57
|
|
|
58
58
|
@abstractmethod
|
|
@@ -108,6 +108,9 @@ class MatplotlibBackend(PlotBackend):
|
|
|
108
108
|
df.drop("partial", axis=1, inplace=True)
|
|
109
109
|
means = df.mean(axis=0, numeric_only=True)
|
|
110
110
|
medians = df.median(axis=0, numeric_only=True)
|
|
111
|
+
my[0] = my[1]
|
|
112
|
+
|
|
113
|
+
nfields = len(means)
|
|
111
114
|
|
|
112
115
|
df /= 1000
|
|
113
116
|
if len(df) > 0:
|
|
@@ -118,7 +121,7 @@ class MatplotlibBackend(PlotBackend):
|
|
|
118
121
|
sbn.histplot(df, multiple="dodge", kde=True, ax=axes)
|
|
119
122
|
legend = []
|
|
120
123
|
# Don't know why but legend is reversed from df.
|
|
121
|
-
for q in range(
|
|
124
|
+
for q in range(nfields - 1, -1, -1):
|
|
122
125
|
dmedian = u.d(medians.iloc[q], latex=True)
|
|
123
126
|
dmean = u.d(means.iloc[q], latex=True)
|
|
124
127
|
legend.append(f"{my[q]}: $M$: {dmedian}, $\\bar{{x}}$: {dmean}")
|
|
@@ -126,13 +129,14 @@ class MatplotlibBackend(PlotBackend):
|
|
|
126
129
|
plt.xlabel(f"{thisyear} $k")
|
|
127
130
|
plt.title(objective)
|
|
128
131
|
leads = [f"partial {my[0]}", f" final {my[1]}"]
|
|
129
|
-
|
|
132
|
+
leads = leads if nfields == 2 else leads[1:]
|
|
133
|
+
elif nfields == 2:
|
|
130
134
|
# Show partial bequest and net spending as two separate histograms.
|
|
131
135
|
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
|
|
132
136
|
cols = ["partial", objective]
|
|
133
137
|
leads = [f"partial {my[0]}", objective]
|
|
134
|
-
for q in
|
|
135
|
-
sbn.histplot(df[
|
|
138
|
+
for q, col in enumerate(cols):
|
|
139
|
+
sbn.histplot(df[col], kde=True, ax=axes[q])
|
|
136
140
|
dmedian = u.d(medians.iloc[q], latex=True)
|
|
137
141
|
dmean = u.d(means.iloc[q], latex=True)
|
|
138
142
|
legend = [f"$M$: {dmedian}, $\\bar{{x}}$: {dmean}"]
|
|
@@ -154,7 +158,7 @@ class MatplotlibBackend(PlotBackend):
|
|
|
154
158
|
|
|
155
159
|
plt.suptitle(title)
|
|
156
160
|
|
|
157
|
-
for q in range(
|
|
161
|
+
for q in range(nfields):
|
|
158
162
|
print(f"{leads[q]:>12}: Median ({thisyear} $): {u.d(medians.iloc[q])}", file=description)
|
|
159
163
|
print(f"{leads[q]:>12}: Mean ({thisyear} $): {u.d(means.iloc[q])}", file=description)
|
|
160
164
|
mmin = 1000 * df.iloc[:, q].min()
|
|
@@ -209,7 +213,7 @@ class MatplotlibBackend(PlotBackend):
|
|
|
209
213
|
g.figure.suptitle(title, y=1.08)
|
|
210
214
|
return g.figure
|
|
211
215
|
|
|
212
|
-
def plot_rates(self, name, tau_kn, year_n,
|
|
216
|
+
def plot_rates(self, name, tau_kn, year_n, N_k,
|
|
213
217
|
rate_method, rate_frm=None, rate_to=None, tag=""):
|
|
214
218
|
"""Plot rate values used over the time horizon."""
|
|
215
219
|
fig, ax = plt.subplots()
|
|
@@ -230,19 +234,12 @@ class MatplotlibBackend(PlotBackend):
|
|
|
230
234
|
ltype = ["-", "-.", ":", "--"]
|
|
231
235
|
|
|
232
236
|
for k in range(N_k):
|
|
233
|
-
|
|
234
|
-
if year_frac_left == 1:
|
|
235
|
-
data = 100 * tau_kn[k]
|
|
236
|
-
years = year_n
|
|
237
|
-
else:
|
|
238
|
-
data = 100 * tau_kn[k, 1:]
|
|
239
|
-
years = year_n[1:]
|
|
240
|
-
|
|
237
|
+
data = 100 * tau_kn[k]
|
|
241
238
|
# Use ddof=1 to match pandas' statistical calculations from numpy.
|
|
242
239
|
label = (
|
|
243
240
|
rate_name[k] + " <" + "{:.1f}".format(np.mean(data)) + " +/- {:.1f}".format(np.std(data, ddof=1)) + "%>"
|
|
244
241
|
)
|
|
245
|
-
ax.plot(
|
|
242
|
+
ax.plot(year_n, data, label=label, ls=ltype[k % N_k])
|
|
246
243
|
|
|
247
244
|
ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
|
|
248
245
|
ax.legend(loc="best", reverse=False, fontsize=8, framealpha=0.7)
|
|
@@ -330,8 +327,8 @@ class MatplotlibBackend(PlotBackend):
|
|
|
330
327
|
|
|
331
328
|
return self._line_income_plot(year_n, series, style, title, yformat)[0]
|
|
332
329
|
|
|
333
|
-
def
|
|
334
|
-
"""Plot asset
|
|
330
|
+
def plot_asset_composition(self, year_n, inames, b_ijkn, gamma_n, value, name, tag):
|
|
331
|
+
"""Plot asset composition over time."""
|
|
335
332
|
if value == "nominal":
|
|
336
333
|
yformat = r"\$k (nominal)"
|
|
337
334
|
infladjust = 1
|
|
@@ -352,7 +349,7 @@ class MatplotlibBackend(PlotBackend):
|
|
|
352
349
|
y2stack[namek] = np.zeros((len(inames), len(years_n)))
|
|
353
350
|
for i in range(len(inames)):
|
|
354
351
|
y2stack[namek][i][:] = b_ijkn[i][jDic[jkey]][kDic[kkey]][:] / infladjust
|
|
355
|
-
title = name + "\
|
|
352
|
+
title = name + "\nAsset Composition - " + jkey
|
|
356
353
|
if tag:
|
|
357
354
|
title += " - " + tag
|
|
358
355
|
fig, ax = self._stack_plot(years_n, inames, title, range(len(inames)),
|
|
@@ -420,12 +417,12 @@ class MatplotlibBackend(PlotBackend):
|
|
|
420
417
|
|
|
421
418
|
def plot_taxes(self, year_n, T_n, M_n, gamma_n, value, title, inames):
|
|
422
419
|
"""Plot taxes over time."""
|
|
423
|
-
style = {"income
|
|
420
|
+
style = {"income tax": "-", "Medicare": "-."}
|
|
424
421
|
if value == "nominal":
|
|
425
|
-
series = {"income
|
|
422
|
+
series = {"income tax": T_n, "Medicare": M_n}
|
|
426
423
|
yformat = r"\$k (nominal)"
|
|
427
424
|
else:
|
|
428
|
-
series = {"income
|
|
425
|
+
series = {"income tax": T_n / gamma_n[:-1], "Medicare": M_n / gamma_n[:-1]}
|
|
429
426
|
yformat = r"\$k (" + str(year_n[0]) + r"\$)"
|
|
430
427
|
fig, ax = self._line_income_plot(year_n, series, style, title, yformat)
|
|
431
428
|
|
|
@@ -88,6 +88,9 @@ class PlotlyBackend(PlotBackend):
|
|
|
88
88
|
margin=dict(b=150)
|
|
89
89
|
)
|
|
90
90
|
|
|
91
|
+
# Format y-axis as number
|
|
92
|
+
fig.update_yaxes(tickformat=",.1f")
|
|
93
|
+
|
|
91
94
|
return fig
|
|
92
95
|
|
|
93
96
|
def plot_gross_income(self, year_n, G_n, gamma_n, value, title, tax_brackets):
|
|
@@ -195,6 +198,11 @@ class PlotlyBackend(PlotBackend):
|
|
|
195
198
|
# Format y-axis as k
|
|
196
199
|
fig.update_yaxes(tickformat=",.0f")
|
|
197
200
|
|
|
201
|
+
ymin = np.min(net_data)
|
|
202
|
+
ymax = np.max(net_data)
|
|
203
|
+
if np.abs(ymax - ymin) < 1:
|
|
204
|
+
fig.update_layout(yaxis=dict(range=[np.floor(ymin)-1, np.ceil(ymax)+1]))
|
|
205
|
+
|
|
198
206
|
return fig
|
|
199
207
|
|
|
200
208
|
def plot_taxes(self, year_n, T_n, M_n, gamma_n, value, title, inames):
|
|
@@ -215,7 +223,7 @@ class PlotlyBackend(PlotBackend):
|
|
|
215
223
|
fig.add_trace(go.Scatter(
|
|
216
224
|
x=year_n,
|
|
217
225
|
y=income_tax_data,
|
|
218
|
-
name="income
|
|
226
|
+
name="income tax",
|
|
219
227
|
line=dict(width=2)
|
|
220
228
|
))
|
|
221
229
|
|
|
@@ -250,7 +258,7 @@ class PlotlyBackend(PlotBackend):
|
|
|
250
258
|
|
|
251
259
|
return fig
|
|
252
260
|
|
|
253
|
-
def plot_rates(self, name, tau_kn, year_n,
|
|
261
|
+
def plot_rates(self, name, tau_kn, year_n, N_k, rate_method, rate_frm=None, rate_to=None, tag=""):
|
|
254
262
|
"""Plot rate values used over the time horizon."""
|
|
255
263
|
fig = go.Figure()
|
|
256
264
|
|
|
@@ -273,13 +281,7 @@ class PlotlyBackend(PlotBackend):
|
|
|
273
281
|
|
|
274
282
|
# Plot each rate
|
|
275
283
|
for k in range(N_k):
|
|
276
|
-
|
|
277
|
-
if year_frac_left == 1:
|
|
278
|
-
data = 100 * tau_kn[k]
|
|
279
|
-
years = year_n
|
|
280
|
-
else:
|
|
281
|
-
data = 100 * tau_kn[k, 1:]
|
|
282
|
-
years = year_n[1:]
|
|
284
|
+
data = 100 * tau_kn[k]
|
|
283
285
|
|
|
284
286
|
# Calculate mean and std
|
|
285
287
|
mean_val = np.mean(data)
|
|
@@ -288,7 +290,7 @@ class PlotlyBackend(PlotBackend):
|
|
|
288
290
|
|
|
289
291
|
# Add trace
|
|
290
292
|
fig.add_trace(go.Scatter(
|
|
291
|
-
x=
|
|
293
|
+
x=year_n,
|
|
292
294
|
y=data,
|
|
293
295
|
name=label,
|
|
294
296
|
line=dict(
|
|
@@ -569,7 +571,10 @@ class PlotlyBackend(PlotBackend):
|
|
|
569
571
|
df.drop("partial", axis=1, inplace=True)
|
|
570
572
|
means = df.mean(axis=0, numeric_only=True)
|
|
571
573
|
medians = df.median(axis=0, numeric_only=True)
|
|
574
|
+
my[0] = my[1]
|
|
572
575
|
|
|
576
|
+
colors = ["orange", "green"]
|
|
577
|
+
nfields = len(means)
|
|
573
578
|
# Convert to thousands
|
|
574
579
|
df /= 1000
|
|
575
580
|
|
|
@@ -591,7 +596,7 @@ class PlotlyBackend(PlotBackend):
|
|
|
591
596
|
x=df[col],
|
|
592
597
|
name=label,
|
|
593
598
|
opacity=0.7,
|
|
594
|
-
marker_color=
|
|
599
|
+
marker_color=colors[i]
|
|
595
600
|
))
|
|
596
601
|
|
|
597
602
|
# Update layout
|
|
@@ -612,8 +617,9 @@ class PlotlyBackend(PlotBackend):
|
|
|
612
617
|
)
|
|
613
618
|
|
|
614
619
|
leads = [f"partial {my[0]}", f" final {my[1]}"]
|
|
620
|
+
leads = leads if nfields == 2 else leads[1:]
|
|
615
621
|
|
|
616
|
-
elif
|
|
622
|
+
elif nfields == 2:
|
|
617
623
|
# Two separate histograms
|
|
618
624
|
fig = make_subplots(
|
|
619
625
|
rows=1, cols=2,
|
|
@@ -634,23 +640,12 @@ class PlotlyBackend(PlotBackend):
|
|
|
634
640
|
go.Histogram(
|
|
635
641
|
x=df[col],
|
|
636
642
|
name=label,
|
|
637
|
-
marker_color=
|
|
638
|
-
showlegend=
|
|
643
|
+
marker_color=colors[i],
|
|
644
|
+
showlegend=True
|
|
639
645
|
),
|
|
640
646
|
row=1, col=i+1
|
|
641
647
|
)
|
|
642
648
|
|
|
643
|
-
# Add statistics annotation
|
|
644
|
-
fig.add_annotation(
|
|
645
|
-
x=0.01, y=0.99,
|
|
646
|
-
xref=f"x{i+1}",
|
|
647
|
-
yref="paper",
|
|
648
|
-
text=label,
|
|
649
|
-
showarrow=False,
|
|
650
|
-
font=dict(size=10),
|
|
651
|
-
bgcolor="rgba(0, 0, 0, 0)"
|
|
652
|
-
)
|
|
653
|
-
|
|
654
649
|
# Update layout
|
|
655
650
|
fig.update_layout(
|
|
656
651
|
title=title,
|
|
@@ -661,7 +656,9 @@ class PlotlyBackend(PlotBackend):
|
|
|
661
656
|
|
|
662
657
|
# Update y-axis labels
|
|
663
658
|
fig.update_yaxes(title_text="Count", row=1, col=1)
|
|
664
|
-
fig.update_yaxes(title_text="Count", row=1, col=2)
|
|
659
|
+
# fig.update_yaxes(title_text="Count", row=1, col=2)
|
|
660
|
+
fig.update_xaxes(title_text=f"{thisyear} $k", row=1, col=1)
|
|
661
|
+
fig.update_xaxes(title_text=f"{thisyear} $k", row=1, col=2)
|
|
665
662
|
|
|
666
663
|
else:
|
|
667
664
|
# Single histogram for net spending
|
|
@@ -697,7 +694,7 @@ class PlotlyBackend(PlotBackend):
|
|
|
697
694
|
leads = [objective]
|
|
698
695
|
|
|
699
696
|
# Add statistics to description
|
|
700
|
-
for q in range(
|
|
697
|
+
for q in range(nfields):
|
|
701
698
|
print(f"{leads[q]:>12}: Median ({thisyear} $): {u.d(medians.iloc[q])}", file=description)
|
|
702
699
|
print(f"{leads[q]:>12}: Mean ({thisyear} $): {u.d(means.iloc[q])}", file=description)
|
|
703
700
|
mmin = 1000 * df.iloc[:, q].min()
|
|
@@ -710,7 +707,7 @@ class PlotlyBackend(PlotBackend):
|
|
|
710
707
|
|
|
711
708
|
return None, description
|
|
712
709
|
|
|
713
|
-
def
|
|
710
|
+
def plot_asset_composition(self, year_n, inames, b_ijkn, gamma_n, value, name, tag):
|
|
714
711
|
"""Plot asset distribution over time."""
|
|
715
712
|
# Set up value formatting
|
|
716
713
|
if value == "nominal":
|
|
@@ -762,7 +759,7 @@ class PlotlyBackend(PlotBackend):
|
|
|
762
759
|
))
|
|
763
760
|
|
|
764
761
|
# Update layout
|
|
765
|
-
title = f"{name}<br>
|
|
762
|
+
title = f"{name}<br>Asset Composition - {jkey}"
|
|
766
763
|
if tag:
|
|
767
764
|
title += f" - {tag}"
|
|
768
765
|
|
owlplanner/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2025.05.
|
|
1
|
+
__version__ = "2025.05.30"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: owlplanner
|
|
3
|
-
Version: 2025.5.
|
|
3
|
+
Version: 2025.5.30
|
|
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
|
|
@@ -714,7 +714,7 @@ Description-Content-Type: text/markdown
|
|
|
714
714
|
-------------------------------------------------------------------------------------
|
|
715
715
|
|
|
716
716
|
### TL;DR
|
|
717
|
-
Owl is a financial
|
|
717
|
+
Owl is a retirement financial planning tool that uses a linear programming
|
|
718
718
|
optimization algorithm to provide guidance on retirement decisions
|
|
719
719
|
such as contributions, withdrawals, Roth conversions, and more.
|
|
720
720
|
Users can select varying return rates to perform historical back testing,
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
owlplanner/__init__.py,sha256=hJ2i4m2JpHPAKyQLjYOXpJzeEsgcTcKD-Vhm0AIjjWg,592
|
|
2
2
|
owlplanner/abcapi.py,sha256=m0vtoEzz9HJV7fOK_d7OnK7ha2Qbf7wLLPCJ9YZzR1k,6851
|
|
3
|
-
owlplanner/config.py,sha256=
|
|
3
|
+
owlplanner/config.py,sha256=v6T6A_90rVyl4sfX8KLpI8wkzt9HCjUiGDsPS-4VTec,12588
|
|
4
4
|
owlplanner/mylogging.py,sha256=tYMw04O-XYSzjTj36fmKJGLcE1VkK6k6oJNeqtKXzuc,2530
|
|
5
|
-
owlplanner/plan.py,sha256=
|
|
5
|
+
owlplanner/plan.py,sha256=BnojjOQzzFdcT4dL8EALzc_vzXO2qQJJXjY98nRZIyA,107114
|
|
6
6
|
owlplanner/progress.py,sha256=8jlCvvtgDI89zXVNMBg1-lnEyhpPvKQS2X5oAIpoOVQ,384
|
|
7
7
|
owlplanner/rates.py,sha256=MiaibxJY82JGpAhGyF2BJTm5-rmVAUuG8KLApVQhjvU,14816
|
|
8
8
|
owlplanner/tax2025.py,sha256=wmlZpYeeGNrbyn5g7wOFqhWbggppodtHqc-ex5XRooI,7850
|
|
9
9
|
owlplanner/timelists.py,sha256=wNYnJqxJ6QqE6jHh5lfFqYngfw5wUFrI15LSsM5ae8s,3949
|
|
10
10
|
owlplanner/utils.py,sha256=WpJgn79YZfH8UCkcmhd-AZlxlGuz1i1-UDBRXImsY6I,2485
|
|
11
|
-
owlplanner/version.py,sha256=
|
|
11
|
+
owlplanner/version.py,sha256=h-tHOHZjN9oy3hW0oN8p91JT5cGNx7ALbl5_KcaZl3g,28
|
|
12
12
|
owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
|
|
14
14
|
owlplanner/plotting/__init__.py,sha256=VnF6ui78YrTrg1dA6hBIdI02ahzEaHVR3ZEdDe_i880,103
|
|
15
|
-
owlplanner/plotting/base.py,sha256=
|
|
15
|
+
owlplanner/plotting/base.py,sha256=LP1TByl1tO4m087O6VpbZ_TTMnErHJGLTxXZXC9cuKQ,2431
|
|
16
16
|
owlplanner/plotting/factory.py,sha256=i1k8m_ISnJw06f_JWlMvOQ7Q0PgV_BoLm05uLwFPvOQ,883
|
|
17
|
-
owlplanner/plotting/matplotlib_backend.py,sha256
|
|
18
|
-
owlplanner/plotting/plotly_backend.py,sha256=
|
|
19
|
-
owlplanner-2025.5.
|
|
20
|
-
owlplanner-2025.5.
|
|
21
|
-
owlplanner-2025.5.
|
|
22
|
-
owlplanner-2025.5.
|
|
17
|
+
owlplanner/plotting/matplotlib_backend.py,sha256=iJm3IBeMA5VUYG_zZxKPIzt4Izv2QWtWvlP656zwJVk,17738
|
|
18
|
+
owlplanner/plotting/plotly_backend.py,sha256=5nqEUJXwLPW1vL9hQijxIUK57sWHvya6ZqIIYof-OjE,32944
|
|
19
|
+
owlplanner-2025.5.30.dist-info/METADATA,sha256=lail2zj8dulkBtJ71340wAUjE3u6LZNNaEjgN4FAo0A,54024
|
|
20
|
+
owlplanner-2025.5.30.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
+
owlplanner-2025.5.30.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
22
|
+
owlplanner-2025.5.30.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|