owlplanner 2025.2.9__py3-none-any.whl → 2025.2.11__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/abcapi.py +6 -6
- owlplanner/config.py +16 -16
- owlplanner/logging.py +1 -1
- owlplanner/plan.py +139 -183
- owlplanner/progress.py +1 -1
- owlplanner/rates.py +15 -15
- owlplanner/tax2025.py +1 -1
- owlplanner/timelists.py +5 -5
- owlplanner/utils.py +1 -1
- owlplanner/version.py +1 -1
- {owlplanner-2025.2.9.dist-info → owlplanner-2025.2.11.dist-info}/METADATA +1 -1
- owlplanner-2025.2.11.dist-info/RECORD +17 -0
- owlplanner-2025.2.9.dist-info/RECORD +0 -17
- {owlplanner-2025.2.9.dist-info → owlplanner-2025.2.11.dist-info}/WHEEL +0 -0
- {owlplanner-2025.2.9.dist-info → owlplanner-2025.2.11.dist-info}/licenses/LICENSE +0 -0
owlplanner/abcapi.py
CHANGED
|
@@ -43,7 +43,7 @@ class Row(object):
|
|
|
43
43
|
"""
|
|
44
44
|
Add an element at index ``ind`` of value ``val`` to the row.
|
|
45
45
|
"""
|
|
46
|
-
assert 0 <= ind and ind < self.nvars, "Index
|
|
46
|
+
assert 0 <= ind and ind < self.nvars, f"Index {ind} out of range."
|
|
47
47
|
self.ind.append(ind)
|
|
48
48
|
self.val.append(val)
|
|
49
49
|
|
|
@@ -154,7 +154,7 @@ class Bounds(object):
|
|
|
154
154
|
self.integrality = []
|
|
155
155
|
|
|
156
156
|
def setBinary(self, ii):
|
|
157
|
-
assert 0 <= ii and ii < self.nvars, "Index
|
|
157
|
+
assert 0 <= ii and ii < self.nvars, f"Index {ii} out of range."
|
|
158
158
|
self.ind.append(ii)
|
|
159
159
|
self.lb.append(0)
|
|
160
160
|
self.ub.append(1)
|
|
@@ -162,21 +162,21 @@ class Bounds(object):
|
|
|
162
162
|
self.integrality.append(ii)
|
|
163
163
|
|
|
164
164
|
def set0_Ub(self, ii, ub):
|
|
165
|
-
assert 0 <= ii and ii < self.nvars, "Index
|
|
165
|
+
assert 0 <= ii and ii < self.nvars, f"Index {ii} out of range."
|
|
166
166
|
self.ind.append(ii)
|
|
167
167
|
self.lb.append(0)
|
|
168
168
|
self.ub.append(ub)
|
|
169
169
|
self.key.append("ra")
|
|
170
170
|
|
|
171
171
|
def setLb_Inf(self, ii, lb):
|
|
172
|
-
assert 0 <= ii and ii < self.nvars, "Index
|
|
172
|
+
assert 0 <= ii and ii < self.nvars, f"Index {ii} out of range."
|
|
173
173
|
self.ind.append(ii)
|
|
174
174
|
self.lb.append(lb)
|
|
175
175
|
self.ub.append(np.inf)
|
|
176
176
|
self.key.append("lo")
|
|
177
177
|
|
|
178
178
|
def setRange(self, ii, lb, ub):
|
|
179
|
-
assert 0 <= ii and ii < self.nvars, "Index
|
|
179
|
+
assert 0 <= ii and ii < self.nvars, f"Index {ii} out of range."
|
|
180
180
|
self.ind.append(ii)
|
|
181
181
|
self.lb.append(lb)
|
|
182
182
|
self.ub.append(ub)
|
|
@@ -223,7 +223,7 @@ class Objective(object):
|
|
|
223
223
|
self.val = []
|
|
224
224
|
|
|
225
225
|
def setElem(self, ind, val):
|
|
226
|
-
assert 0 <= ind and ind < self.nvars, "Index
|
|
226
|
+
assert 0 <= ind and ind < self.nvars, f"Index {ind} out of range."
|
|
227
227
|
self.ind.append(ind)
|
|
228
228
|
self.val.append(val)
|
|
229
229
|
|
owlplanner/config.py
CHANGED
|
@@ -42,7 +42,7 @@ def saveConfig(plan, file, mylog):
|
|
|
42
42
|
diconf["Assets"] = {}
|
|
43
43
|
for j in range(plan.N_j):
|
|
44
44
|
amounts = plan.beta_ij[:, j] / 1000
|
|
45
|
-
diconf["Assets"]["
|
|
45
|
+
diconf["Assets"][f"{accountTypes[j]} savings balances"] = amounts.tolist()
|
|
46
46
|
if plan.N_i == 2:
|
|
47
47
|
diconf["Assets"]["Beneficiary fractions"] = plan.phi_j.tolist()
|
|
48
48
|
diconf["Assets"]["Spousal surplus deposit fraction"] = plan.eta
|
|
@@ -113,23 +113,23 @@ def saveConfig(plan, file, mylog):
|
|
|
113
113
|
filename = filename + ".toml"
|
|
114
114
|
if not filename.startswith("case_"):
|
|
115
115
|
filename = "case_" + filename
|
|
116
|
-
mylog.vprint("Saving plan case file as '
|
|
116
|
+
mylog.vprint(f"Saving plan case file as '{filename}'.")
|
|
117
117
|
|
|
118
118
|
try:
|
|
119
119
|
with open(filename, "w") as casefile:
|
|
120
120
|
toml.dump(diconf, casefile, encoder=toml.TomlNumpyEncoder())
|
|
121
121
|
except Exception as e:
|
|
122
|
-
raise RuntimeError("Failed to save case file
|
|
122
|
+
raise RuntimeError(f"Failed to save case file {filename}: {e}")
|
|
123
123
|
elif isinstance(file, StringIO):
|
|
124
124
|
try:
|
|
125
125
|
string = toml.dumps(diconf, encoder=toml.TomlNumpyEncoder())
|
|
126
126
|
file.write(string)
|
|
127
127
|
except Exception as e:
|
|
128
|
-
raise RuntimeError("Failed to save case to StringIO:
|
|
128
|
+
raise RuntimeError(f"Failed to save case to StringIO: {e}")
|
|
129
129
|
elif file is None:
|
|
130
130
|
pass
|
|
131
131
|
else:
|
|
132
|
-
raise ValueError("Argument
|
|
132
|
+
raise ValueError(f"Argument {type(file)} has unknown type")
|
|
133
133
|
|
|
134
134
|
return diconf
|
|
135
135
|
|
|
@@ -151,27 +151,27 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
|
|
|
151
151
|
if not filename.endswith(".toml"):
|
|
152
152
|
filename = filename + ".toml"
|
|
153
153
|
|
|
154
|
-
mylog.vprint("Reading plan from case file '
|
|
154
|
+
mylog.vprint(f"Reading plan from case file '{filename}'.")
|
|
155
155
|
|
|
156
156
|
try:
|
|
157
157
|
with open(filename, "r") as f:
|
|
158
158
|
diconf = toml.load(f)
|
|
159
159
|
except Exception as e:
|
|
160
|
-
raise FileNotFoundError("File
|
|
160
|
+
raise FileNotFoundError(f"File {filename} not found: {e}")
|
|
161
161
|
elif isinstance(file, BytesIO):
|
|
162
162
|
try:
|
|
163
163
|
string = file.getvalue().decode("utf-8")
|
|
164
164
|
diconf = toml.loads(string)
|
|
165
165
|
except Exception as e:
|
|
166
|
-
raise RuntimeError("Cannot read from BytesIO:
|
|
166
|
+
raise RuntimeError(f"Cannot read from BytesIO: {e}")
|
|
167
167
|
elif isinstance(file, StringIO):
|
|
168
168
|
try:
|
|
169
169
|
string = file.getvalue()
|
|
170
170
|
diconf = toml.loads(string)
|
|
171
171
|
except Exception as e:
|
|
172
|
-
raise RuntimeError("Cannot read from StringIO:
|
|
172
|
+
raise RuntimeError(f"Cannot read from StringIO: {e}")
|
|
173
173
|
else:
|
|
174
|
-
raise ValueError("
|
|
174
|
+
raise ValueError(f"Type {type(file)} not a valid type")
|
|
175
175
|
|
|
176
176
|
# Basic Info.
|
|
177
177
|
name = diconf["Plan Name"]
|
|
@@ -181,14 +181,14 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
|
|
|
181
181
|
expectancy = diconf["Basic Info"]["Life expectancy"]
|
|
182
182
|
startDate = diconf["Basic Info"]["Start date"]
|
|
183
183
|
icount = len(yobs)
|
|
184
|
-
|
|
185
|
-
mylog.vprint("Plan for
|
|
184
|
+
s = ["", "s"][icount - 1]
|
|
185
|
+
mylog.vprint(f"Plan for {icount} individual{s}: {inames}.")
|
|
186
186
|
p = plan.Plan(inames, yobs, expectancy, name, startDate=startDate, verbose=True, logstreams=logstreams)
|
|
187
187
|
|
|
188
188
|
# Assets.
|
|
189
189
|
balances = {}
|
|
190
190
|
for acc in accountTypes:
|
|
191
|
-
balances[acc] = diconf["Assets"]["
|
|
191
|
+
balances[acc] = diconf["Assets"][f"{acc} savings balances"]
|
|
192
192
|
p.setAccountBalances(
|
|
193
193
|
taxable=balances["taxable"], taxDeferred=balances["tax-deferred"], taxFree=balances["tax-free"]
|
|
194
194
|
)
|
|
@@ -207,11 +207,11 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
|
|
|
207
207
|
elif dirname != "" and os.path.exists(dirname + "/" + timeListsFileName):
|
|
208
208
|
myfile = dirname + "/" + timeListsFileName
|
|
209
209
|
else:
|
|
210
|
-
raise FileNotFoundError("File '
|
|
210
|
+
raise FileNotFoundError(f"File '{timeListsFileName}' not found.")
|
|
211
211
|
p.readContributions(myfile)
|
|
212
212
|
else:
|
|
213
213
|
p.timeListsFileName = timeListsFileName
|
|
214
|
-
mylog.vprint("Ignoring to read contributions file
|
|
214
|
+
mylog.vprint(f"Ignoring to read contributions file {timeListsFileName}.")
|
|
215
215
|
|
|
216
216
|
# Fixed Income.
|
|
217
217
|
ssecAmounts = np.array(diconf["Fixed Income"]["Social security amounts"], dtype=np.float32)
|
|
@@ -272,7 +272,7 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
|
|
|
272
272
|
generic=boundsAR["generic"],
|
|
273
273
|
)
|
|
274
274
|
else:
|
|
275
|
-
raise ValueError("Unknown asset allocation type
|
|
275
|
+
raise ValueError(f"Unknown asset allocation type {allocType}.")
|
|
276
276
|
|
|
277
277
|
# Optimization Parameters.
|
|
278
278
|
p.objective = diconf["Optimization Parameters"]["Objective"]
|
owlplanner/logging.py
CHANGED
|
@@ -27,7 +27,7 @@ class Logger(object):
|
|
|
27
27
|
self._logstreams = 2 * logstreams
|
|
28
28
|
self.vprint("Using logstream as stream logger.")
|
|
29
29
|
else:
|
|
30
|
-
raise ValueError("Log streams
|
|
30
|
+
raise ValueError(f"Log streams {logstreams} must be a list.")
|
|
31
31
|
|
|
32
32
|
def setVerbose(self, verbose=True):
|
|
33
33
|
"""
|
owlplanner/plan.py
CHANGED
|
@@ -82,7 +82,7 @@ def _genXi_n(profile, fraction, n_d, N_n, a, b, c):
|
|
|
82
82
|
xi[n_d:] *= fraction
|
|
83
83
|
xi *= neutralSum / xi.sum()
|
|
84
84
|
else:
|
|
85
|
-
raise ValueError("Unknown profile type
|
|
85
|
+
raise ValueError(f"Unknown profile type {profile}.")
|
|
86
86
|
|
|
87
87
|
return xi
|
|
88
88
|
|
|
@@ -161,7 +161,7 @@ def _checkCaseStatus(func):
|
|
|
161
161
|
@wraps(func)
|
|
162
162
|
def wrapper(self, *args, **kwargs):
|
|
163
163
|
if self.caseStatus != "solved":
|
|
164
|
-
self.mylog.vprint("Preventing to run method
|
|
164
|
+
self.mylog.vprint(f"Preventing to run method {func.__name__}() while case is {self.caseStatus}.")
|
|
165
165
|
return None
|
|
166
166
|
return func(self, *args, **kwargs)
|
|
167
167
|
|
|
@@ -177,11 +177,11 @@ def _checkConfiguration(func):
|
|
|
177
177
|
@wraps(func)
|
|
178
178
|
def wrapper(self, *args, **kwargs):
|
|
179
179
|
if self.xi_n is None:
|
|
180
|
-
msg = "You must define a spending profile before calling
|
|
180
|
+
msg = f"You must define a spending profile before calling {func.__name__}()."
|
|
181
181
|
self.mylog.vprint(msg)
|
|
182
182
|
raise RuntimeError(msg)
|
|
183
183
|
if self.alpha_ijkn is None:
|
|
184
|
-
msg = "You must define an allocation profile before calling
|
|
184
|
+
msg = f"You must define an allocation profile before calling {func.__name__}()."
|
|
185
185
|
self.mylog.vprint(msg)
|
|
186
186
|
raise RuntimeError(msg)
|
|
187
187
|
return func(self, *args, **kwargs)
|
|
@@ -201,9 +201,7 @@ def _timer(func):
|
|
|
201
201
|
result = func(self, *args, **kwargs)
|
|
202
202
|
pt = time.process_time() - pt0
|
|
203
203
|
rt = time.time() - rt0
|
|
204
|
-
self.mylog.vprint(
|
|
205
|
-
"CPU time used: %dm%.1fs, Wall time: %dm%.1fs." % (int(pt / 60), pt % 60, int(rt / 60), rt % 60)
|
|
206
|
-
)
|
|
204
|
+
self.mylog.vprint(f"CPU time used: {int(pt / 60)}m{pt % 60:.1f}s, Wall time: {int(rt / 60)}m{rt % 60:.1f}s.")
|
|
207
205
|
return result
|
|
208
206
|
|
|
209
207
|
return wrapper
|
|
@@ -245,9 +243,9 @@ class Plan(object):
|
|
|
245
243
|
self.defaultSolver = "HiGHS"
|
|
246
244
|
|
|
247
245
|
self.N_i = len(yobs)
|
|
248
|
-
assert 0 < self.N_i and self.N_i <= 2, "Cannot support
|
|
249
|
-
assert self.N_i == len(expectancy), "Expectancy must have
|
|
250
|
-
assert self.N_i == len(inames), "Names for individuals must have
|
|
246
|
+
assert 0 < self.N_i and self.N_i <= 2, f"Cannot support {self.N_i} individuals."
|
|
247
|
+
assert self.N_i == len(expectancy), f"Expectancy must have {self.N_i} entries."
|
|
248
|
+
assert self.N_i == len(inames), f"Names for individuals must have {self.N_i} entries."
|
|
251
249
|
assert inames[0] != "" or (self.N_i == 2 and inames[1] == ""), "Name for each individual must be provided."
|
|
252
250
|
|
|
253
251
|
self.filingStatus = ["single", "married"][self.N_i - 1]
|
|
@@ -301,13 +299,11 @@ class Plan(object):
|
|
|
301
299
|
self.prevMAGI = np.zeros((2))
|
|
302
300
|
|
|
303
301
|
# Scenario starts at the beginning of this year and ends at the end of the last year.
|
|
304
|
-
self.
|
|
305
|
-
|
|
306
|
-
)
|
|
302
|
+
s = ["", "s"][self.N_i - 1]
|
|
303
|
+
self.mylog.vprint(f"Preparing scenario of {self.N_n} years for {self.N_i} individual{s}.")
|
|
307
304
|
for i in range(self.N_i):
|
|
308
|
-
self.
|
|
309
|
-
|
|
310
|
-
)
|
|
305
|
+
endyear = thisyear + self.horizons[i] - 1
|
|
306
|
+
self.mylog.vprint(f"{self.inames[i]:>14}: life horizon from {thisyear} -> {endyear}.")
|
|
311
307
|
|
|
312
308
|
# Prepare income tax and RMD time series.
|
|
313
309
|
self.rho_in = tx.rho_in(self.yobs, self.N_n)
|
|
@@ -340,7 +336,7 @@ class Plan(object):
|
|
|
340
336
|
|
|
341
337
|
def setLogstreams(self, verbose, logstreams):
|
|
342
338
|
self.mylog = logging.Logger(verbose, logstreams)
|
|
343
|
-
# self.mylog.vprint("Setting logstreams to
|
|
339
|
+
# self.mylog.vprint(f"Setting logstreams to {logstreams}.")
|
|
344
340
|
|
|
345
341
|
def logger(self):
|
|
346
342
|
return self.mylog
|
|
@@ -382,7 +378,7 @@ class Plan(object):
|
|
|
382
378
|
# Take midnight as the reference.
|
|
383
379
|
self.yearFracLeft = 1 - (refdate.timetuple().tm_yday - 1) / (365 + lp)
|
|
384
380
|
|
|
385
|
-
self.mylog.vprint("Setting 1st-year starting date to
|
|
381
|
+
self.mylog.vprint(f"Setting 1st-year starting date to {self.startDate}.")
|
|
386
382
|
|
|
387
383
|
return None
|
|
388
384
|
|
|
@@ -397,7 +393,7 @@ class Plan(object):
|
|
|
397
393
|
if value in opts:
|
|
398
394
|
return value
|
|
399
395
|
|
|
400
|
-
raise ValueError("Value type must be one of:
|
|
396
|
+
raise ValueError(f"Value type must be one of: {opts}")
|
|
401
397
|
|
|
402
398
|
return None
|
|
403
399
|
|
|
@@ -407,7 +403,7 @@ class Plan(object):
|
|
|
407
403
|
to distinguish graph outputs and as base name for
|
|
408
404
|
saving configurations and workbooks.
|
|
409
405
|
"""
|
|
410
|
-
self.mylog.vprint("Renaming plan
|
|
406
|
+
self.mylog.vprint(f"Renaming plan {self._name} -> {newname}.")
|
|
411
407
|
self._name = newname
|
|
412
408
|
|
|
413
409
|
def setSpousalDepositFraction(self, eta):
|
|
@@ -425,8 +421,8 @@ class Plan(object):
|
|
|
425
421
|
self.mylog.vprint("Deposit fraction can only be 0 for single individuals.")
|
|
426
422
|
eta = 0
|
|
427
423
|
else:
|
|
428
|
-
self.mylog.vprint("Setting spousal surplus deposit fraction to
|
|
429
|
-
self.mylog.vprint("\t
|
|
424
|
+
self.mylog.vprint(f"Setting spousal surplus deposit fraction to {eta:.1f}.")
|
|
425
|
+
self.mylog.vprint(f"\t{self.inames[0]}: {1-eta:.1f}, {self.inames[1]}: {eta:.1f}")
|
|
430
426
|
self.eta = eta
|
|
431
427
|
|
|
432
428
|
def setDefaultPlots(self, value):
|
|
@@ -435,7 +431,7 @@ class Plan(object):
|
|
|
435
431
|
"""
|
|
436
432
|
|
|
437
433
|
self.defaultPlots = self._checkValue(value)
|
|
438
|
-
self.mylog.vprint("Setting plots default value to
|
|
434
|
+
self.mylog.vprint(f"Setting plots default value to {value}.")
|
|
439
435
|
|
|
440
436
|
def setDividendRate(self, mu):
|
|
441
437
|
"""
|
|
@@ -443,7 +439,7 @@ class Plan(object):
|
|
|
443
439
|
"""
|
|
444
440
|
assert 0 <= mu and mu <= 100, "Rate must be between 0 and 100."
|
|
445
441
|
mu /= 100
|
|
446
|
-
self.mylog.vprint("Dividend return rate on equities set to
|
|
442
|
+
self.mylog.vprint(f"Dividend return rate on equities set to {u.pc(mu, f=1)}.")
|
|
447
443
|
self.mu = mu
|
|
448
444
|
self.caseStatus = "modified"
|
|
449
445
|
|
|
@@ -453,7 +449,7 @@ class Plan(object):
|
|
|
453
449
|
"""
|
|
454
450
|
assert 0 <= psi and psi <= 100, "Rate must be between 0 and 100."
|
|
455
451
|
psi /= 100
|
|
456
|
-
self.mylog.vprint("Long-term capital gain income tax set to
|
|
452
|
+
self.mylog.vprint(f"Long-term capital gain income tax set to {u.pc(psi, f=0)}.")
|
|
457
453
|
self.psi = psi
|
|
458
454
|
self.caseStatus = "modified"
|
|
459
455
|
|
|
@@ -462,17 +458,18 @@ class Plan(object):
|
|
|
462
458
|
Set fractions of savings accounts that is left to surviving spouse.
|
|
463
459
|
Default is [1, 1, 1] for taxable, tax-deferred, adn tax-exempt accounts.
|
|
464
460
|
"""
|
|
465
|
-
assert len(phi) == self.N_j, "Fractions must have
|
|
461
|
+
assert len(phi) == self.N_j, f"Fractions must have {self.N_j} entries."
|
|
466
462
|
for j in range(self.N_j):
|
|
467
463
|
assert 0 <= phi[j] <= 1, "Fractions must be between 0 and 1."
|
|
468
464
|
|
|
469
465
|
self.phi_j = np.array(phi, dtype=np.float32)
|
|
470
|
-
self.mylog.vprint("Spousal beneficiary fractions set to",
|
|
466
|
+
self.mylog.vprint("Spousal beneficiary fractions set to",
|
|
467
|
+
["{:.2f}".format(self.phi_j[j]) for j in range(self.N_j)])
|
|
471
468
|
self.caseStatus = "modified"
|
|
472
469
|
|
|
473
470
|
if np.any(self.phi_j != 1):
|
|
474
471
|
self.mylog.vprint("Consider changing spousal deposit fraction for better convergence.")
|
|
475
|
-
self.mylog.vprint("\tRecommended: setSpousalDepositFraction(
|
|
472
|
+
self.mylog.vprint(f"\tRecommended: setSpousalDepositFraction({self.i_d}.)")
|
|
476
473
|
|
|
477
474
|
def setHeirsTaxRate(self, nu):
|
|
478
475
|
"""
|
|
@@ -481,7 +478,7 @@ class Plan(object):
|
|
|
481
478
|
"""
|
|
482
479
|
assert 0 <= nu and nu <= 100, "Rate must be between 0 and 100."
|
|
483
480
|
nu /= 100
|
|
484
|
-
self.mylog.vprint("Heirs tax rate on tax-deferred portion of estate set to
|
|
481
|
+
self.mylog.vprint(f"Heirs tax rate on tax-deferred portion of estate set to {u.pc(nu, f=0)}.")
|
|
485
482
|
self.nu = nu
|
|
486
483
|
self.caseStatus = "modified"
|
|
487
484
|
|
|
@@ -490,14 +487,15 @@ class Plan(object):
|
|
|
490
487
|
Set value of pension for each individual and commencement age.
|
|
491
488
|
Units are in $k, unless specified otherwise: 'k', 'M', or '1'.
|
|
492
489
|
"""
|
|
493
|
-
assert len(amounts) == self.N_i, "Amounts must have
|
|
494
|
-
assert len(ages) == self.N_i, "Ages must have
|
|
495
|
-
assert len(indexed) >= self.N_i, "Indexed list must have at least
|
|
490
|
+
assert len(amounts) == self.N_i, f"Amounts must have {self.N_i} entries."
|
|
491
|
+
assert len(ages) == self.N_i, f"Ages must have {self.N_i} entries."
|
|
492
|
+
assert len(indexed) >= self.N_i, f"Indexed list must have at least {self.N_i} entries."
|
|
496
493
|
|
|
497
494
|
fac = u.getUnits(units)
|
|
498
495
|
amounts = u.rescale(amounts, fac)
|
|
499
496
|
|
|
500
|
-
self.mylog.vprint("Setting pension of", [u.d(amounts[i]) for i in range(self.N_i)],
|
|
497
|
+
self.mylog.vprint("Setting pension of", [u.d(amounts[i]) for i in range(self.N_i)],
|
|
498
|
+
"at age(s)", [int(ages[i]) for i in range(self.N_i)])
|
|
501
499
|
|
|
502
500
|
thisyear = date.today().year
|
|
503
501
|
# Use zero array freshly initialized.
|
|
@@ -522,17 +520,15 @@ class Plan(object):
|
|
|
522
520
|
Set value of social security for each individual and commencement age.
|
|
523
521
|
Units are in $k, unless specified otherwise: 'k', 'M', or '1'.
|
|
524
522
|
"""
|
|
525
|
-
assert len(amounts) == self.N_i, "Amounts must have
|
|
526
|
-
assert len(ages) == self.N_i, "Ages must have
|
|
523
|
+
assert len(amounts) == self.N_i, f"Amounts must have {self.N_i} entries."
|
|
524
|
+
assert len(ages) == self.N_i, f"Ages must have {self.N_i} entries."
|
|
527
525
|
|
|
528
526
|
fac = u.getUnits(units)
|
|
529
527
|
amounts = u.rescale(amounts, fac)
|
|
530
528
|
|
|
531
529
|
self.mylog.vprint(
|
|
532
|
-
"Setting social security benefits of",
|
|
533
|
-
[
|
|
534
|
-
"at age(s)",
|
|
535
|
-
ages,
|
|
530
|
+
"Setting social security benefits of", [u.d(amounts[i]) for i in range(self.N_i)],
|
|
531
|
+
"at age(s)", [int(ages[i]) for i in range(self.N_i)],
|
|
536
532
|
)
|
|
537
533
|
|
|
538
534
|
thisyear = date.today().year
|
|
@@ -560,10 +556,10 @@ class Plan(object):
|
|
|
560
556
|
as a second argument. Default value is 60%.
|
|
561
557
|
Dip and increase are percent changes in the smile profile.
|
|
562
558
|
"""
|
|
563
|
-
assert 0 <= percent and percent <= 100, "Survivor value
|
|
564
|
-
assert 0 <= dip and dip <= 100, "Dip value
|
|
565
|
-
assert -100 <= increase and increase <= 100, "Increase value
|
|
566
|
-
assert 0 <= delay and delay <= self.N_n - 2, "Delay value
|
|
559
|
+
assert 0 <= percent and percent <= 100, f"Survivor value {percent} outside range."
|
|
560
|
+
assert 0 <= dip and dip <= 100, f"Dip value {dip} outside range."
|
|
561
|
+
assert -100 <= increase and increase <= 100, f"Increase value {increase} outside range."
|
|
562
|
+
assert 0 <= delay and delay <= self.N_n - 2, f"Delay value {delay} outside year range."
|
|
567
563
|
|
|
568
564
|
self.chi = percent / 100
|
|
569
565
|
|
|
@@ -606,13 +602,7 @@ class Plan(object):
|
|
|
606
602
|
self.rateFrm = frm
|
|
607
603
|
self.rateTo = to
|
|
608
604
|
self.tau_kn = dr.genSeries(self.N_n).transpose()
|
|
609
|
-
self.mylog.vprint(
|
|
610
|
-
"Generating rate series of",
|
|
611
|
-
len(self.tau_kn[0]),
|
|
612
|
-
"years using",
|
|
613
|
-
method,
|
|
614
|
-
"method.",
|
|
615
|
-
)
|
|
605
|
+
self.mylog.vprint(f"Generating rate series of {len(self.tau_kn[0])} years using {method} method.")
|
|
616
606
|
|
|
617
607
|
# Account for how late we are now in the first year and reduce rate accordingly.
|
|
618
608
|
self.tau_kn[:, 0] *= self.yearFracLeft
|
|
@@ -671,10 +661,10 @@ class Plan(object):
|
|
|
671
661
|
each spouse. For single individuals, these lists will contain only
|
|
672
662
|
one entry. Units are in $k, unless specified otherwise: 'k', 'M', or '1'.
|
|
673
663
|
"""
|
|
674
|
-
plurals = ["", "y", "ies"]
|
|
675
|
-
assert len(taxable) == self.N_i, "taxable must have
|
|
676
|
-
assert len(taxDeferred) == self.N_i, "taxDeferred must have
|
|
677
|
-
assert len(taxFree) == self.N_i, "taxFree must have
|
|
664
|
+
plurals = ["", "y", "ies"][self.N_i]
|
|
665
|
+
assert len(taxable) == self.N_i, f"taxable must have {self.N_i} entr{plurals}."
|
|
666
|
+
assert len(taxDeferred) == self.N_i, f"taxDeferred must have {self.N_i} entr{plurals}."
|
|
667
|
+
assert len(taxFree) == self.N_i, f"taxFree must have {self.N_i} entr{plurals}."
|
|
678
668
|
|
|
679
669
|
fac = u.getUnits(units)
|
|
680
670
|
taxable = u.rescale(taxable, fac)
|
|
@@ -716,12 +706,12 @@ class Plan(object):
|
|
|
716
706
|
self.interpCenter = center
|
|
717
707
|
self.interpWidth = width
|
|
718
708
|
else:
|
|
719
|
-
raise ValueError("Method
|
|
709
|
+
raise ValueError(f"Method {method} not supported.")
|
|
720
710
|
|
|
721
711
|
self.interpMethod = method
|
|
722
712
|
self.caseStatus = "modified"
|
|
723
713
|
|
|
724
|
-
self.mylog.vprint("Asset allocation interpolation method set to
|
|
714
|
+
self.mylog.vprint(f"Asset allocation interpolation method set to {method}.")
|
|
725
715
|
|
|
726
716
|
def setAllocationRatios(self, allocType, taxable=None, taxDeferred=None, taxFree=None, generic=None):
|
|
727
717
|
"""
|
|
@@ -750,31 +740,19 @@ class Plan(object):
|
|
|
750
740
|
if allocType == "account":
|
|
751
741
|
# Make sure we have proper input.
|
|
752
742
|
for item in [taxable, taxDeferred, taxFree]:
|
|
753
|
-
assert len(item) == self.N_i, "
|
|
743
|
+
assert len(item) == self.N_i, f"{item} must have one entry per individual."
|
|
754
744
|
for i in range(self.N_i):
|
|
755
745
|
# Initial and final.
|
|
756
|
-
assert len(item[i]) == 2, "
|
|
757
|
-
item,
|
|
758
|
-
i,
|
|
759
|
-
)
|
|
746
|
+
assert len(item[i]) == 2, f"{item}[{i}] must have 2 lists (initial and final)."
|
|
760
747
|
for z in range(2):
|
|
761
|
-
assert len(item[i][z]) == self.N_k, "
|
|
762
|
-
item,
|
|
763
|
-
i,
|
|
764
|
-
z,
|
|
765
|
-
self.N_k,
|
|
766
|
-
)
|
|
748
|
+
assert len(item[i][z]) == self.N_k, f"{item}[{i}][{z}] must have {self.N_k} entries."
|
|
767
749
|
assert abs(sum(item[i][z]) - 100) < 0.01, "Sum of percentages must add to 100."
|
|
768
750
|
|
|
769
751
|
for i in range(self.N_i):
|
|
770
|
-
self.mylog.vprint(
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
)
|
|
775
|
-
self.mylog.vprint(" taxable:", taxable[i][0], "->", taxable[i][1])
|
|
776
|
-
self.mylog.vprint(" taxDeferred:", taxDeferred[i][0], "->", taxDeferred[i][1])
|
|
777
|
-
self.mylog.vprint(" taxFree:", taxFree[i][0], "->", taxFree[i][1])
|
|
752
|
+
self.mylog.vprint(f"{self.inames[i]}: Setting gliding allocation ratios (%) to {allocType}.")
|
|
753
|
+
self.mylog.vprint(f" taxable: {taxable[i][0]} -> {taxable[i][1]}")
|
|
754
|
+
self.mylog.vprint(f" taxDeferred: {taxDeferred[i][0]} -> {taxDeferred[i][1]}")
|
|
755
|
+
self.mylog.vprint(f" taxFree: {taxFree[i][0]} -> {taxFree[i][1]}")
|
|
778
756
|
|
|
779
757
|
# Order in alpha is j, i, 0/1, k.
|
|
780
758
|
alpha = {}
|
|
@@ -798,22 +776,14 @@ class Plan(object):
|
|
|
798
776
|
assert len(generic) == self.N_i, "generic must have one list per individual."
|
|
799
777
|
for i in range(self.N_i):
|
|
800
778
|
# Initial and final.
|
|
801
|
-
assert len(generic[i]) == 2, "generic[
|
|
779
|
+
assert len(generic[i]) == 2, f"generic[{i}] must have 2 lists (initial and final)."
|
|
802
780
|
for z in range(2):
|
|
803
|
-
assert len(generic[i][z]) == self.N_k, "generic[
|
|
804
|
-
i,
|
|
805
|
-
z,
|
|
806
|
-
self.N_k,
|
|
807
|
-
)
|
|
781
|
+
assert len(generic[i][z]) == self.N_k, f"generic[{i}][{z}] must have {self.N_k} entries."
|
|
808
782
|
assert abs(sum(generic[i][z]) - 100) < 0.01, "Sum of percentages must add to 100."
|
|
809
783
|
|
|
810
784
|
for i in range(self.N_i):
|
|
811
|
-
self.mylog.vprint(
|
|
812
|
-
|
|
813
|
-
": Setting gliding allocation ratios (%) to",
|
|
814
|
-
allocType,
|
|
815
|
-
)
|
|
816
|
-
self.mylog.vprint("\t", generic[i][0], "->", generic[i][1])
|
|
785
|
+
self.mylog.vprint(f"{self.inames[i]}: Setting gliding allocation ratios (%) to {allocType}.")
|
|
786
|
+
self.mylog.vprint(f"\t{generic[i][0]} -> {generic[i][1]}")
|
|
817
787
|
|
|
818
788
|
for i in range(self.N_i):
|
|
819
789
|
Nin = self.horizons[i] + 1
|
|
@@ -829,14 +799,11 @@ class Plan(object):
|
|
|
829
799
|
elif allocType == "spouses":
|
|
830
800
|
assert len(generic) == 2, "generic must have 2 entries (initial and final)."
|
|
831
801
|
for z in range(2):
|
|
832
|
-
assert len(generic[z]) == self.N_k, "generic[
|
|
833
|
-
z,
|
|
834
|
-
self.N_k,
|
|
835
|
-
)
|
|
802
|
+
assert len(generic[z]) == self.N_k, f"generic[{z}] must have {self.N_k} entries."
|
|
836
803
|
assert abs(sum(generic[z]) - 100) < 0.01, "Sum of percentages must add to 100."
|
|
837
804
|
|
|
838
|
-
self.mylog.vprint("Setting gliding allocation ratios (%) to
|
|
839
|
-
self.mylog.vprint("\t
|
|
805
|
+
self.mylog.vprint(f"Setting gliding allocation ratios (%) to {allocType}.")
|
|
806
|
+
self.mylog.vprint(f"\t{generic[0]} -> {generic[1]}")
|
|
840
807
|
|
|
841
808
|
# Use longest-lived spouse for both time scales.
|
|
842
809
|
Nxn = max(self.horizons) + 1
|
|
@@ -854,7 +821,7 @@ class Plan(object):
|
|
|
854
821
|
self.ARCoord = allocType
|
|
855
822
|
self.caseStatus = "modified"
|
|
856
823
|
|
|
857
|
-
self.mylog.vprint("Interpolating assets allocation ratios using
|
|
824
|
+
self.mylog.vprint(f"Interpolating assets allocation ratios using {self.interpMethod} method.")
|
|
858
825
|
|
|
859
826
|
def readContributions(self, filename):
|
|
860
827
|
"""
|
|
@@ -880,7 +847,7 @@ class Plan(object):
|
|
|
880
847
|
try:
|
|
881
848
|
filename, self.timeLists = timelists.read(filename, self.inames, self.horizons, self.mylog)
|
|
882
849
|
except Exception as e:
|
|
883
|
-
raise Exception("Unsuccessful read of contributions:
|
|
850
|
+
raise Exception(f"Unsuccessful read of contributions: {e}")
|
|
884
851
|
return False
|
|
885
852
|
|
|
886
853
|
self.timeListsFileName = filename
|
|
@@ -1045,13 +1012,7 @@ class Plan(object):
|
|
|
1045
1012
|
self.nvars = _qC(C["z"], self.N_i, self.N_n, self.N_z)
|
|
1046
1013
|
|
|
1047
1014
|
self.C = C
|
|
1048
|
-
self.mylog.vprint(
|
|
1049
|
-
"Problem has",
|
|
1050
|
-
len(C),
|
|
1051
|
-
"distinct time series forming",
|
|
1052
|
-
self.nvars,
|
|
1053
|
-
"decision variables.",
|
|
1054
|
-
)
|
|
1015
|
+
self.mylog.vprint(f"Problem has {len(C)} distinct time series forming {self.nvars} decision variables.")
|
|
1055
1016
|
|
|
1056
1017
|
return None
|
|
1057
1018
|
|
|
@@ -1132,7 +1093,7 @@ class Plan(object):
|
|
|
1132
1093
|
# Roth conversions equalities/inequalities.
|
|
1133
1094
|
if "maxRothConversion" in options:
|
|
1134
1095
|
if options["maxRothConversion"] == "file":
|
|
1135
|
-
# self.mylog.vprint(
|
|
1096
|
+
# self.mylog.vprint(f"Fixing Roth conversions to those from file {self.timeListsFileName}.")
|
|
1136
1097
|
for i in range(Ni):
|
|
1137
1098
|
for n in range(self.horizons[i]):
|
|
1138
1099
|
rhs = self.myRothX_in[i][n]
|
|
@@ -1157,7 +1118,7 @@ class Plan(object):
|
|
|
1157
1118
|
try:
|
|
1158
1119
|
i_x = self.inames.index(rhsopt)
|
|
1159
1120
|
except ValueError:
|
|
1160
|
-
raise ValueError("Unknown individual
|
|
1121
|
+
raise ValueError(f"Unknown individual {rhsopt} for noRothConversions:")
|
|
1161
1122
|
|
|
1162
1123
|
for n in range(Nn):
|
|
1163
1124
|
B.set0_Ub(_q2(Cx, i_x, n, Ni, Nn), zero)
|
|
@@ -1397,10 +1358,10 @@ class Plan(object):
|
|
|
1397
1358
|
"""
|
|
1398
1359
|
if yend + self.N_n > self.year_n[0]:
|
|
1399
1360
|
yend = self.year_n[0] - self.N_n - 1
|
|
1400
|
-
self.mylog.vprint("Warning: Upper bound for year range re-adjusted to
|
|
1361
|
+
self.mylog.vprint(f"Warning: Upper bound for year range re-adjusted to {yend}.")
|
|
1401
1362
|
N = yend - ystart + 1
|
|
1402
1363
|
|
|
1403
|
-
self.mylog.vprint("Running historical range from
|
|
1364
|
+
self.mylog.vprint(f"Running historical range from {ystart} to {yend}.")
|
|
1404
1365
|
|
|
1405
1366
|
self.mylog.setVerbose(verbose)
|
|
1406
1367
|
|
|
@@ -1409,7 +1370,7 @@ class Plan(object):
|
|
|
1409
1370
|
elif objective == "maxBequest":
|
|
1410
1371
|
columns = ["partial", "final"]
|
|
1411
1372
|
else:
|
|
1412
|
-
self.mylog.print("Invalid objective
|
|
1373
|
+
self.mylog.print(f"Invalid objective {objective}.")
|
|
1413
1374
|
return None
|
|
1414
1375
|
|
|
1415
1376
|
df = pd.DataFrame(columns=columns)
|
|
@@ -1450,7 +1411,7 @@ class Plan(object):
|
|
|
1450
1411
|
self.mylog.print("It is pointless to run Monte Carlo simulations with fixed rates.")
|
|
1451
1412
|
return
|
|
1452
1413
|
|
|
1453
|
-
self.mylog.vprint("Running
|
|
1414
|
+
self.mylog.vprint(f"Running {N} Monte Carlo simulations.")
|
|
1454
1415
|
self.mylog.setVerbose(verbose)
|
|
1455
1416
|
|
|
1456
1417
|
# Turn off Medicare by default, unless specified in options.
|
|
@@ -1465,7 +1426,7 @@ class Plan(object):
|
|
|
1465
1426
|
elif objective == "maxBequest":
|
|
1466
1427
|
columns = ["partial", "final"]
|
|
1467
1428
|
else:
|
|
1468
|
-
self.mylog.print("Invalid objective
|
|
1429
|
+
self.mylog.print(f"Invalid objective {objective}.")
|
|
1469
1430
|
return None
|
|
1470
1431
|
|
|
1471
1432
|
df = pd.DataFrame(columns=columns)
|
|
@@ -1505,8 +1466,9 @@ class Plan(object):
|
|
|
1505
1466
|
|
|
1506
1467
|
description = io.StringIO()
|
|
1507
1468
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1469
|
+
pSuccess = u.pc(len(df) / N)
|
|
1470
|
+
print(f"Success rate: {pSuccess} on {N} samples.", file=description)
|
|
1471
|
+
title = f"$N$ = {N}, $P$ = {pSuccess}"
|
|
1510
1472
|
means = df.mean(axis=0, numeric_only=True)
|
|
1511
1473
|
medians = df.median(axis=0, numeric_only=True)
|
|
1512
1474
|
|
|
@@ -1518,13 +1480,14 @@ class Plan(object):
|
|
|
1518
1480
|
# or if solution led to empty accounts at the end of first spouse's life.
|
|
1519
1481
|
if np.all(self.phi_j == 1) or medians.iloc[0] < 1:
|
|
1520
1482
|
if medians.iloc[0] < 1:
|
|
1521
|
-
print("Optimized solutions all have null partial bequest in year
|
|
1483
|
+
print(f"Optimized solutions all have null partial bequest in year {my[0]}.", file=description)
|
|
1522
1484
|
df.drop("partial", axis=1, inplace=True)
|
|
1523
1485
|
means = df.mean(axis=0, numeric_only=True)
|
|
1524
1486
|
medians = df.median(axis=0, numeric_only=True)
|
|
1525
1487
|
|
|
1526
1488
|
df /= 1000
|
|
1527
1489
|
if len(df) > 0:
|
|
1490
|
+
thisyear = self.year_n[0]
|
|
1528
1491
|
if objective == "maxBequest":
|
|
1529
1492
|
fig, axes = plt.subplots()
|
|
1530
1493
|
# Show both partial and final bequests in the same histogram.
|
|
@@ -1532,37 +1495,36 @@ class Plan(object):
|
|
|
1532
1495
|
legend = []
|
|
1533
1496
|
# Don't know why but legend is reversed from df.
|
|
1534
1497
|
for q in range(len(means) - 1, -1, -1):
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
)
|
|
1498
|
+
dmedian = u.d(medians.iloc[q], latex=True)
|
|
1499
|
+
dmean = u.d(means.iloc[q], latex=True)
|
|
1500
|
+
legend.append(f"{my[q]}: $M$: {dmedian}, $\\bar{{x}}$: {dmean}")
|
|
1539
1501
|
plt.legend(legend, shadow=True)
|
|
1540
|
-
plt.xlabel("
|
|
1502
|
+
plt.xlabel(f"{thisyear} $k")
|
|
1541
1503
|
plt.title(objective)
|
|
1542
|
-
leads = ["partial
|
|
1504
|
+
leads = [f"partial {my[0]}", f" final {my[1]}"]
|
|
1543
1505
|
elif len(means) == 2:
|
|
1544
1506
|
# Show partial bequest and net spending as two separate histograms.
|
|
1545
1507
|
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
|
|
1546
1508
|
cols = ["partial", objective]
|
|
1547
|
-
leads = ["partial
|
|
1509
|
+
leads = [f"partial {my[0]}", objective]
|
|
1548
1510
|
for q in range(2):
|
|
1549
1511
|
sbn.histplot(df[cols[q]], kde=True, ax=axes[q])
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
]
|
|
1512
|
+
dmedian = u.d(medians.iloc[q], latex=True)
|
|
1513
|
+
dmean = u.d(means.iloc[q], latex=True)
|
|
1514
|
+
legend = [f"$M$: {dmedian}, $\\bar{{x}}$: {dmean}"]
|
|
1553
1515
|
axes[q].set_label(legend)
|
|
1554
1516
|
axes[q].legend(labels=legend)
|
|
1555
1517
|
axes[q].set_title(leads[q])
|
|
1556
|
-
axes[q].set_xlabel("
|
|
1518
|
+
axes[q].set_xlabel(f"{thisyear} $k")
|
|
1557
1519
|
else:
|
|
1558
1520
|
# Show net spending as single histogram.
|
|
1559
1521
|
fig, axes = plt.subplots()
|
|
1560
1522
|
sbn.histplot(df[objective], kde=True, ax=axes)
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
]
|
|
1523
|
+
dmedian = u.d(medians.iloc[0], latex=True)
|
|
1524
|
+
dmean = u.d(means.iloc[0], latex=True)
|
|
1525
|
+
legend = [f"$M$: {dmedian}, $\\bar{{x}}$: {dmean}"]
|
|
1564
1526
|
plt.legend(legend, shadow=True)
|
|
1565
|
-
plt.xlabel("
|
|
1527
|
+
plt.xlabel(f"{thisyear} $k")
|
|
1566
1528
|
plt.title(objective)
|
|
1567
1529
|
leads = [objective]
|
|
1568
1530
|
|
|
@@ -1570,15 +1532,13 @@ class Plan(object):
|
|
|
1570
1532
|
# plt.show()
|
|
1571
1533
|
|
|
1572
1534
|
for q in range(len(means)):
|
|
1573
|
-
print("
|
|
1574
|
-
print("
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
file=description,
|
|
1579
|
-
)
|
|
1535
|
+
print(f"{leads[q]:>12}: Median ({thisyear} $): {u.d(medians.iloc[q])}", file=description)
|
|
1536
|
+
print(f"{leads[q]:>12}: Mean ({thisyear} $): {u.d(means.iloc[q])}", file=description)
|
|
1537
|
+
mmin = 1000 * df.iloc[:, q].min()
|
|
1538
|
+
mmax = 1000 * df.iloc[:, q].max()
|
|
1539
|
+
print(f"{leads[q]:>12}: Range: {u.d(mmin)} - {u.d(mmax)}", file=description)
|
|
1580
1540
|
nzeros = len(df.iloc[:, q][df.iloc[:, q] < 0.001])
|
|
1581
|
-
print("
|
|
1541
|
+
print(f"{leads[q]:>12}: N zero solns: {nzeros}", file=description)
|
|
1582
1542
|
|
|
1583
1543
|
return fig, description
|
|
1584
1544
|
|
|
@@ -1637,13 +1597,13 @@ class Plan(object):
|
|
|
1637
1597
|
|
|
1638
1598
|
for opt in myoptions:
|
|
1639
1599
|
if opt not in knownOptions:
|
|
1640
|
-
raise ValueError("Option
|
|
1600
|
+
raise ValueError(f"Option {opt} is not one of {knownOptions}.")
|
|
1641
1601
|
|
|
1642
1602
|
if objective not in knownObjectives:
|
|
1643
|
-
raise ValueError("Objective
|
|
1603
|
+
raise ValueError(f"Objective {objective} is not one of {knownObjectives}.")
|
|
1644
1604
|
|
|
1645
1605
|
if objective == "maxBequest" and "netSpending" not in myoptions:
|
|
1646
|
-
raise RuntimeError("Objective
|
|
1606
|
+
raise RuntimeError(f"Objective {objective} needs netSpending option.")
|
|
1647
1607
|
|
|
1648
1608
|
if objective == "maxBequest" and "bequest" in myoptions:
|
|
1649
1609
|
self.mylog.vprint("Ignoring bequest option provided.")
|
|
@@ -1672,7 +1632,7 @@ class Plan(object):
|
|
|
1672
1632
|
if "solver" in options:
|
|
1673
1633
|
solver = myoptions["solver"]
|
|
1674
1634
|
if solver not in knownSolvers:
|
|
1675
|
-
raise ValueError("Unknown solver
|
|
1635
|
+
raise ValueError(f"Unknown solver {solver}.")
|
|
1676
1636
|
else:
|
|
1677
1637
|
solver = self.defaultSolver
|
|
1678
1638
|
|
|
@@ -1735,7 +1695,7 @@ class Plan(object):
|
|
|
1735
1695
|
|
|
1736
1696
|
self._estimateMedicare(solution.x)
|
|
1737
1697
|
|
|
1738
|
-
self.mylog.vprint("Iteration:
|
|
1698
|
+
self.mylog.vprint(f"Iteration: {it} objective: {u.d(solution.fun * objFac, f=2)}")
|
|
1739
1699
|
|
|
1740
1700
|
delta = solution.x - old_x
|
|
1741
1701
|
absdiff = np.sum(np.abs(delta), axis=0)
|
|
@@ -1757,9 +1717,9 @@ class Plan(object):
|
|
|
1757
1717
|
old_x = solution.x
|
|
1758
1718
|
|
|
1759
1719
|
if solution.success:
|
|
1760
|
-
self.mylog.vprint("Self-consistent Medicare loop returned after
|
|
1720
|
+
self.mylog.vprint(f"Self-consistent Medicare loop returned after {it} iterations.")
|
|
1761
1721
|
self.mylog.vprint(solution.message)
|
|
1762
|
-
self.mylog.vprint("Objective:
|
|
1722
|
+
self.mylog.vprint(f"Objective: {u.d(solution.fun * objFac)}")
|
|
1763
1723
|
# self.mylog.vprint('Upper bound:', u.d(-solution.mip_dual_bound))
|
|
1764
1724
|
self._aggregateResults(solution.x)
|
|
1765
1725
|
self._timestamp = datetime.now().strftime("%Y-%m-%d at %H:%M:%S")
|
|
@@ -1871,7 +1831,7 @@ class Plan(object):
|
|
|
1871
1831
|
task.set_Stream(mosek.streamtype.wrn, _streamPrinter)
|
|
1872
1832
|
# task.writedata(self._name+'.ptf')
|
|
1873
1833
|
if solsta == mosek.solsta.integer_optimal:
|
|
1874
|
-
self.mylog.vprint("Self-consistent Medicare loop returned after
|
|
1834
|
+
self.mylog.vprint(f"Self-consistent Medicare loop returned after {it} iterations.")
|
|
1875
1835
|
task.solutionsummary(mosek.streamtype.msg)
|
|
1876
1836
|
self.mylog.vprint("Objective:", u.d(solution * objFac))
|
|
1877
1837
|
self.caseStatus = "solved"
|
|
@@ -2052,7 +2012,7 @@ class Plan(object):
|
|
|
2052
2012
|
"""
|
|
2053
2013
|
_estate = np.sum(self.b_ijn[:, :, :, self.N_n], axis=(0, 2))
|
|
2054
2014
|
_estate[1] *= 1 - self.nu
|
|
2055
|
-
self.mylog.vprint("Estate value of
|
|
2015
|
+
self.mylog.vprint(f"Estate value of {u.d(sum(_estate))} at the end of year {self.year_n[-1]}.")
|
|
2056
2016
|
|
|
2057
2017
|
return None
|
|
2058
2018
|
|
|
@@ -2104,67 +2064,63 @@ class Plan(object):
|
|
|
2104
2064
|
|
|
2105
2065
|
totIncome = np.sum(self.g_n, axis=0)
|
|
2106
2066
|
totIncomeNow = np.sum(self.g_n / self.gamma_n[:-1], axis=0)
|
|
2107
|
-
dic[f"Total net spending in {now}$"] = "
|
|
2067
|
+
dic[f"Total net spending in {now}$"] = f"{u.d(totIncomeNow)} ({u.d(totIncome)} nominal)"
|
|
2108
2068
|
|
|
2109
2069
|
totRoth = np.sum(self.x_in, axis=(0, 1))
|
|
2110
2070
|
totRothNow = np.sum(np.sum(self.x_in, axis=0) / self.gamma_n[:-1], axis=0)
|
|
2111
|
-
dic[f"Total Roth conversions in {now}$"] = "
|
|
2071
|
+
dic[f"Total Roth conversions in {now}$"] = f"{u.d(totRothNow)} ({u.d(totRoth)} nominal)"
|
|
2112
2072
|
|
|
2113
2073
|
taxPaid = np.sum(self.T_n, axis=0)
|
|
2114
2074
|
taxPaidNow = np.sum(self.T_n / self.gamma_n[:-1], axis=0)
|
|
2115
|
-
dic[f"Total income tax paid on ordinary income in {now}$"] = "
|
|
2075
|
+
dic[f"Total income tax paid on ordinary income in {now}$"] = f"{u.d(taxPaidNow)} ({u.d(taxPaid)} nominal)"
|
|
2116
2076
|
|
|
2117
2077
|
taxPaid = np.sum(self.U_n, axis=0)
|
|
2118
2078
|
taxPaidNow = np.sum(self.U_n / self.gamma_n[:-1], axis=0)
|
|
2119
|
-
dic[f"Total tax paid on gains and dividends in {now}$"] = "
|
|
2079
|
+
dic[f"Total tax paid on gains and dividends in {now}$"] = f"{u.d(taxPaidNow)} ({u.d(taxPaid)} nominal)"
|
|
2120
2080
|
|
|
2121
2081
|
taxPaid = np.sum(self.M_n, axis=0)
|
|
2122
2082
|
taxPaidNow = np.sum(self.M_n / self.gamma_n[:-1], axis=0)
|
|
2123
|
-
dic[f"Total Medicare premiums paid in {now}$"] = "
|
|
2083
|
+
dic[f"Total Medicare premiums paid in {now}$"] = f"{u.d(taxPaidNow)} ({u.d(taxPaid)} nominal)"
|
|
2124
2084
|
|
|
2125
2085
|
if self.N_i == 2 and self.n_d < self.N_n:
|
|
2126
2086
|
p_j = self.partialEstate_j * (1 - self.phi_j)
|
|
2127
2087
|
p_j[1] *= 1 - self.nu
|
|
2128
2088
|
nx = self.n_d - 1
|
|
2089
|
+
ynx = self.year_n[nx]
|
|
2129
2090
|
totOthers = np.sum(p_j)
|
|
2130
2091
|
totOthersNow = totOthers / self.gamma_n[nx + 1]
|
|
2131
2092
|
q_j = self.partialEstate_j * self.phi_j
|
|
2132
2093
|
totSpousal = np.sum(q_j)
|
|
2133
2094
|
totSpousalNow = totSpousal / self.gamma_n[nx + 1]
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2095
|
+
iname_s = self.inames[self.i_s]
|
|
2096
|
+
iname_d = self.inames[self.i_d]
|
|
2097
|
+
dic[f"Spousal wealth transfer from {iname_d} to {iname_s} in year {ynx} (nominal)"] = (
|
|
2098
|
+
f"taxable: {u.d(q_j[0])} tax-def: {u.d(q_j[1])} tax-free: {u.d(q_j[2])}")
|
|
2138
2099
|
|
|
2139
|
-
dic["Sum of spousal bequests to
|
|
2140
|
-
"
|
|
2141
|
-
)
|
|
2100
|
+
dic[f"Sum of spousal bequests to {iname_s} in year {ynx} in {now}$"] = (
|
|
2101
|
+
f"{u.d(totSpousalNow)} ({u.d(totSpousal)} nominal)")
|
|
2142
2102
|
dic[
|
|
2143
|
-
"Post-tax non-spousal bequests from
|
|
2144
|
-
|
|
2103
|
+
f"Post-tax non-spousal bequests from {iname_d} in year {ynx} (nominal)"] = (
|
|
2104
|
+
f"taxable: {u.d(p_j[0])} tax-def: {u.d(p_j[1])} tax-free: {u.d(p_j[2])}")
|
|
2145
2105
|
dic[
|
|
2146
|
-
"Sum of post-tax non-spousal bequests from
|
|
2147
|
-
|
|
2148
|
-
] = "%s (%s nominal)" % (u.d(totOthersNow), u.d(totOthers))
|
|
2106
|
+
f"Sum of post-tax non-spousal bequests from {iname_d} in year {ynx} in {now}$"] = (
|
|
2107
|
+
f"{u.d(totOthersNow)} ({u.d(totOthers)} nominal)")
|
|
2149
2108
|
|
|
2150
2109
|
estate = np.sum(self.b_ijn[:, :, self.N_n], axis=0)
|
|
2151
2110
|
estate[1] *= 1 - self.nu
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2111
|
+
lastyear = self.year_n[-1]
|
|
2112
|
+
dic[f"Post-tax account values at the end of final plan year {lastyear} (nominal)"] = (
|
|
2113
|
+
f"taxable: {u.d(estate[0])} tax-def: {u.d(estate[1])} tax-free: {u.d(estate[2])}")
|
|
2155
2114
|
|
|
2156
2115
|
totEstate = np.sum(estate)
|
|
2157
2116
|
totEstateNow = totEstate / self.gamma_n[-1]
|
|
2158
|
-
dic["Total estate value at the end of final plan year
|
|
2159
|
-
"
|
|
2160
|
-
)
|
|
2117
|
+
dic[f"Total estate value at the end of final plan year {lastyear} in {now}$"] = (
|
|
2118
|
+
f"{u.d(totEstateNow)} ({u.d(totEstate)} nominal)")
|
|
2161
2119
|
dic["Plan starting date"] = str(self.startDate)
|
|
2162
|
-
dic["Cumulative inflation factor from start date to end of plan"] = "
|
|
2120
|
+
dic["Cumulative inflation factor from start date to end of plan"] = f"{self.gamma_n[-1]:.2f}"
|
|
2163
2121
|
for i in range(self.N_i):
|
|
2164
|
-
dic["
|
|
2165
|
-
now
|
|
2166
|
-
now + self.horizons[i] - 1,
|
|
2167
|
-
)
|
|
2122
|
+
dic[f"{self.inames[i]:>12}'s {self.horizons[i]:02}-year life horizon"] = (
|
|
2123
|
+
f"{now} -> {now + self.horizons[i] - 1}")
|
|
2168
2124
|
|
|
2169
2125
|
dic["Plan name"] = self._name
|
|
2170
2126
|
dic["Number of decision variables"] = str(self.A.nvars)
|
|
@@ -2182,7 +2138,7 @@ class Plan(object):
|
|
|
2182
2138
|
import seaborn as sbn
|
|
2183
2139
|
|
|
2184
2140
|
if self.rateMethod in [None, "user", "historical average", "conservative"]:
|
|
2185
|
-
self.mylog.vprint("Warning: Cannot plot correlations for
|
|
2141
|
+
self.mylog.vprint(f"Warning: Cannot plot correlations for {self.rateMethod} rate method.")
|
|
2186
2142
|
return None
|
|
2187
2143
|
|
|
2188
2144
|
rateNames = [
|
|
@@ -2218,7 +2174,7 @@ class Plan(object):
|
|
|
2218
2174
|
# plt.subplots_adjust(wspace=0.3, hspace=0.3)
|
|
2219
2175
|
|
|
2220
2176
|
title = self._name + "\n"
|
|
2221
|
-
title += "Rates Correlations (N
|
|
2177
|
+
title += f"Rates Correlations (N={self.N_n}) {self.rateMethod}"
|
|
2222
2178
|
if self.rateMethod in ["historical", "histochastic"]:
|
|
2223
2179
|
title += " (" + str(self.rateFrm) + "-" + str(self.rateTo) + ")"
|
|
2224
2180
|
|
|
@@ -2418,7 +2374,7 @@ class Plan(object):
|
|
|
2418
2374
|
elif self.ARCoord == "account":
|
|
2419
2375
|
acList = ["taxable", "tax-deferred", "tax-free"]
|
|
2420
2376
|
else:
|
|
2421
|
-
raise ValueError("Unknown coordination
|
|
2377
|
+
raise ValueError(f"Unknown coordination {self.ARCoord}.")
|
|
2422
2378
|
|
|
2423
2379
|
figures = []
|
|
2424
2380
|
assetDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
|
|
@@ -2876,12 +2832,12 @@ class Plan(object):
|
|
|
2876
2832
|
df.to_csv(fname)
|
|
2877
2833
|
break
|
|
2878
2834
|
except PermissionError:
|
|
2879
|
-
self.mylog.print('Failed to save "
|
|
2835
|
+
self.mylog.print(f'Failed to save "{fname}": Permission denied.')
|
|
2880
2836
|
key = input("Close file and try again? [Yn] ")
|
|
2881
2837
|
if key == "n":
|
|
2882
2838
|
break
|
|
2883
2839
|
except Exception as e:
|
|
2884
|
-
raise Exception("Unanticipated exception
|
|
2840
|
+
raise Exception(f"Unanticipated exception: {e}.")
|
|
2885
2841
|
|
|
2886
2842
|
return None
|
|
2887
2843
|
|
|
@@ -2945,7 +2901,7 @@ def _stackPlot(x, inames, title, irange, series, snames, location, yformat="\\$k
|
|
|
2945
2901
|
ax.set_ylabel("%")
|
|
2946
2902
|
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(100 * x), ",")))
|
|
2947
2903
|
else:
|
|
2948
|
-
raise RuntimeError("Unknown yformat:
|
|
2904
|
+
raise RuntimeError(f"Unknown yformat: {yformat}.")
|
|
2949
2905
|
|
|
2950
2906
|
return fig, ax
|
|
2951
2907
|
|
|
@@ -2963,7 +2919,7 @@ def _saveWorkbook(wb, basename, overwrite, mylog):
|
|
|
2963
2919
|
fname = basename
|
|
2964
2920
|
|
|
2965
2921
|
if overwrite is False and isfile(fname):
|
|
2966
|
-
mylog.print('File "
|
|
2922
|
+
mylog.print(f'File "{fname}" already exists.')
|
|
2967
2923
|
key = input("Overwrite? [Ny] ")
|
|
2968
2924
|
if key != "y":
|
|
2969
2925
|
mylog.vprint("Skipping save and returning.")
|
|
@@ -2971,16 +2927,16 @@ def _saveWorkbook(wb, basename, overwrite, mylog):
|
|
|
2971
2927
|
|
|
2972
2928
|
while True:
|
|
2973
2929
|
try:
|
|
2974
|
-
mylog.vprint('Saving plan as "
|
|
2930
|
+
mylog.vprint(f'Saving plan as "{fname}".')
|
|
2975
2931
|
wb.save(fname)
|
|
2976
2932
|
break
|
|
2977
2933
|
except PermissionError:
|
|
2978
|
-
mylog.print('Failed to save "
|
|
2934
|
+
mylog.print(f'Failed to save "{fname}": Permission denied.')
|
|
2979
2935
|
key = input("Close file and try again? [Yn] ")
|
|
2980
2936
|
if key == "n":
|
|
2981
2937
|
break
|
|
2982
2938
|
except Exception as e:
|
|
2983
|
-
raise Exception("Unanticipated exception
|
|
2939
|
+
raise Exception(f"Unanticipated exception {e}.")
|
|
2984
2940
|
|
|
2985
2941
|
return None
|
|
2986
2942
|
|
|
@@ -3004,7 +2960,7 @@ def _formatSpreadsheet(ws, ftype):
|
|
|
3004
2960
|
ws.column_dimensions[column].width = width
|
|
3005
2961
|
return None
|
|
3006
2962
|
else:
|
|
3007
|
-
raise RuntimeError("Unknown format:
|
|
2963
|
+
raise RuntimeError(f"Unknown format: {ftype}.")
|
|
3008
2964
|
|
|
3009
2965
|
for cell in ws[1] + ws["A"]:
|
|
3010
2966
|
cell.style = "Pandas"
|
owlplanner/progress.py
CHANGED
owlplanner/rates.py
CHANGED
|
@@ -121,9 +121,9 @@ def historicalValue(amount, year):
|
|
|
121
121
|
valued at the beginning of the year specified.
|
|
122
122
|
"""
|
|
123
123
|
thisyear = date.today().year
|
|
124
|
-
assert TO == thisyear - 1, "Rates file needs to be updated to be current to
|
|
125
|
-
assert year >= FROM, "Only data from
|
|
126
|
-
assert year <= thisyear, "Year must be <
|
|
124
|
+
assert TO == thisyear - 1, f"Rates file needs to be updated to be current to {thisyear}."
|
|
125
|
+
assert year >= FROM, f"Only data from {FROM} is available."
|
|
126
|
+
assert year <= thisyear, f"Year must be < {thisyear} for historical data."
|
|
127
127
|
|
|
128
128
|
span = thisyear - year
|
|
129
129
|
ub = len(Inflation)
|
|
@@ -203,7 +203,7 @@ class Rates(object):
|
|
|
203
203
|
"stochastic",
|
|
204
204
|
"histochastic",
|
|
205
205
|
]:
|
|
206
|
-
raise ValueError("Unknown rate selection method
|
|
206
|
+
raise ValueError(f"Unknown rate selection method {method}.")
|
|
207
207
|
|
|
208
208
|
Nk = len(self._defRates)
|
|
209
209
|
# First process fixed methods relying on values.
|
|
@@ -221,7 +221,7 @@ class Rates(object):
|
|
|
221
221
|
self._setFixedRates(self._conservRates)
|
|
222
222
|
elif method == "user":
|
|
223
223
|
assert values is not None, "Fixed values must be provided with the user option."
|
|
224
|
-
assert len(values) == Nk, "
|
|
224
|
+
assert len(values) == Nk, f"Values must have {Nk} items."
|
|
225
225
|
self.means = np.array(values, dtype=float)
|
|
226
226
|
# Convert percent to decimal for storing.
|
|
227
227
|
self.means /= 100.0
|
|
@@ -230,8 +230,8 @@ class Rates(object):
|
|
|
230
230
|
elif method == "stochastic":
|
|
231
231
|
assert values is not None, "Mean values must be provided with the stochastic option."
|
|
232
232
|
assert stdev is not None, "Standard deviations must be provided with the stochastic option."
|
|
233
|
-
assert len(values) == Nk, "
|
|
234
|
-
assert len(stdev) == Nk, "stdev must have
|
|
233
|
+
assert len(values) == Nk, f"Values must have {Nk} items."
|
|
234
|
+
assert len(stdev) == Nk, f"stdev must have {Nk} items."
|
|
235
235
|
self.means = np.array(values, dtype=float)
|
|
236
236
|
self.stdev = np.array(stdev, dtype=float)
|
|
237
237
|
# Convert percent to decimal for storing.
|
|
@@ -256,7 +256,7 @@ class Rates(object):
|
|
|
256
256
|
x += 1
|
|
257
257
|
corrarr = newcorr
|
|
258
258
|
else:
|
|
259
|
-
raise RuntimeError("Unable to process correlation shape of
|
|
259
|
+
raise RuntimeError(f"Unable to process correlation shape of {corrarr.shape}.")
|
|
260
260
|
|
|
261
261
|
self.corr = corrarr
|
|
262
262
|
assert np.array_equal(self.corr, self.corr.T), "Correlation matrix must be symmetric."
|
|
@@ -271,25 +271,25 @@ class Rates(object):
|
|
|
271
271
|
else:
|
|
272
272
|
# Then methods relying on historical data range.
|
|
273
273
|
assert frm is not None, "From year must be provided with this option."
|
|
274
|
-
assert FROM <= frm and frm <= TO,
|
|
275
|
-
assert FROM <= to and to <= TO,
|
|
274
|
+
assert FROM <= frm and frm <= TO, f"Lower range 'frm={frm}' out of bounds."
|
|
275
|
+
assert FROM <= to and to <= TO, f"Upper range 'to={to}' out of bounds."
|
|
276
276
|
assert frm < to, "Unacceptable range."
|
|
277
277
|
self.frm = frm
|
|
278
278
|
self.to = to
|
|
279
279
|
|
|
280
280
|
if method == "historical":
|
|
281
|
-
self.mylog.vprint("Using historical rates representing data from
|
|
281
|
+
self.mylog.vprint(f"Using historical rates representing data from {frm} to {to}.")
|
|
282
282
|
self._rateMethod = self._histRates
|
|
283
283
|
elif method == "historical average" or method == "means":
|
|
284
|
-
self.mylog.vprint("Using average of rates from
|
|
284
|
+
self.mylog.vprint(f"Using average of rates from {frm} to {to}.")
|
|
285
285
|
self.means, self.stdev, self.corr, self.covar = getRatesDistributions(frm, to, self.mylog)
|
|
286
286
|
self._setFixedRates(self.means)
|
|
287
287
|
elif method == "histochastic":
|
|
288
|
-
self.mylog.vprint("Using histochastic rates derived from years
|
|
288
|
+
self.mylog.vprint(f"Using histochastic rates derived from years {frm} to {to}.")
|
|
289
289
|
self._rateMethod = self._stochRates
|
|
290
290
|
self.means, self.stdev, self.corr, self.covar = getRatesDistributions(frm, to, self.mylog)
|
|
291
291
|
else:
|
|
292
|
-
raise ValueError("Method
|
|
292
|
+
raise ValueError(f"Method {method} not supported.")
|
|
293
293
|
|
|
294
294
|
self.method = method
|
|
295
295
|
|
|
@@ -297,7 +297,7 @@ class Rates(object):
|
|
|
297
297
|
|
|
298
298
|
def _setFixedRates(self, rates):
|
|
299
299
|
Nk = len(self._defRates)
|
|
300
|
-
assert len(rates) == Nk, "Rate list provided must have
|
|
300
|
+
assert len(rates) == Nk, f"Rate list provided must have {Nk} entries."
|
|
301
301
|
self._myRates = np.array(rates)
|
|
302
302
|
self._rateMethod = self._fixedRates
|
|
303
303
|
|
owlplanner/tax2025.py
CHANGED
|
@@ -154,7 +154,7 @@ def taxBrackets(N_i, n_d, N_n):
|
|
|
154
154
|
Return dictionary containing future tax brackets
|
|
155
155
|
unadjusted for inflation for plotting.
|
|
156
156
|
"""
|
|
157
|
-
assert 0 < N_i and N_i <= 2, "Cannot process
|
|
157
|
+
assert 0 < N_i and N_i <= 2, f"Cannot process {N_i} individuals."
|
|
158
158
|
# This 1 is the number of years left in TCJA from 2025.
|
|
159
159
|
ytc = 1
|
|
160
160
|
status = N_i - 1
|
owlplanner/timelists.py
CHANGED
|
@@ -55,12 +55,12 @@ def read(finput, inames, horizons, mylog):
|
|
|
55
55
|
try:
|
|
56
56
|
dfDict = pd.read_excel(finput, sheet_name=None)
|
|
57
57
|
except Exception as e:
|
|
58
|
-
raise Exception("Could not read file
|
|
59
|
-
streamName = "file '
|
|
58
|
+
raise Exception(f"Could not read file {finput}: {e}.")
|
|
59
|
+
streamName = f"file '{finput}'"
|
|
60
60
|
|
|
61
61
|
timeLists = condition(dfDict, inames, horizons, mylog)
|
|
62
62
|
|
|
63
|
-
mylog.vprint("Successfully read time horizons from
|
|
63
|
+
mylog.vprint(f"Successfully read time horizons from {streamName}.")
|
|
64
64
|
|
|
65
65
|
return finput, timeLists
|
|
66
66
|
|
|
@@ -101,10 +101,10 @@ def condition(dfDict, inames, horizons, mylog):
|
|
|
101
101
|
else:
|
|
102
102
|
for item in timeHorizonItems:
|
|
103
103
|
if item != "big-ticket items" and df[item].iloc[n] < 0:
|
|
104
|
-
raise ValueError("Item
|
|
104
|
+
raise ValueError(f"Item {item} for {iname} in year {df['year'].iloc[n]} is < 0.")
|
|
105
105
|
|
|
106
106
|
if len(missing) > 0:
|
|
107
|
-
mylog.vprint("Adding
|
|
107
|
+
mylog.vprint(f"Adding {len(missing)} missing years for {iname}: {missing}.")
|
|
108
108
|
|
|
109
109
|
df.sort_values("year", inplace=True)
|
|
110
110
|
# Replace empty (NaN) cells with 0 value.
|
owlplanner/utils.py
CHANGED
owlplanner/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2025.02.
|
|
1
|
+
__version__ = "2025.02.11"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
owlplanner/__init__.py,sha256=QqrdT0Qks20osBTg7h0vJHAxpP9lL7DA99xb0nYbtw4,254
|
|
2
|
+
owlplanner/abcapi.py,sha256=LbzW_KcNy0IeHp42MUHwGu_H67B2h_e1_vu-c2ACTkQ,6646
|
|
3
|
+
owlplanner/config.py,sha256=XFVcXFVpEuWXzybaijNGSTt72py3cYJ3oq0S1ujivl0,11702
|
|
4
|
+
owlplanner/logging.py,sha256=tYMw04O-XYSzjTj36fmKJGLcE1VkK6k6oJNeqtKXzuc,2530
|
|
5
|
+
owlplanner/plan.py,sha256=f_F7sIka4c_91dV7X5XHJkVvztXbtS_cqi24ncBI9gY,113084
|
|
6
|
+
owlplanner/progress.py,sha256=8jlCvvtgDI89zXVNMBg1-lnEyhpPvKQS2X5oAIpoOVQ,384
|
|
7
|
+
owlplanner/rates.py,sha256=TN407qU4n-bac1oymkQ_n2QKEPwFQxy6JZVGwgIkLQU,15585
|
|
8
|
+
owlplanner/tax2025.py,sha256=PVteko6G9gjAT247GnTzAPUe_RaLnZUArFtdzf1dF3M,7014
|
|
9
|
+
owlplanner/timelists.py,sha256=tYieZU67FT6TCcQQis36JaXGI7dT6NqD7RvdEjgJL4M,4026
|
|
10
|
+
owlplanner/utils.py,sha256=HM70W60qB41zfnbl2LltNwAuLYHyy5XYbwnbNcaa6FE,2351
|
|
11
|
+
owlplanner/version.py,sha256=n8NJ4iSncRuwW7_28_WeKgNkjAmkCzQOs0fAbkRFU5E,28
|
|
12
|
+
owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
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,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
owlplanner/__init__.py,sha256=QqrdT0Qks20osBTg7h0vJHAxpP9lL7DA99xb0nYbtw4,254
|
|
2
|
-
owlplanner/abcapi.py,sha256=J2xlj6LXwIblot0g4UeO5BC-vIYXsrI72duH82H6Di8,6658
|
|
3
|
-
owlplanner/config.py,sha256=tKqqDxzygC283UPyZwa47oq0PBH0uEv_1bwiwAdRm4Q,11731
|
|
4
|
-
owlplanner/logging.py,sha256=0y6Mhj56vmilgnDvw0pA8ur15oz5O3IN7qNCaf8yUIk,2532
|
|
5
|
-
owlplanner/plan.py,sha256=5biGxkIfilBHOfoROpDs7m_BmzywJ6qldm_8aGAXbXU,114092
|
|
6
|
-
owlplanner/progress.py,sha256=uyCUmuvZzvg2UPonjmXmUsWnnP55Mm6n19Xg3WcjwHU,386
|
|
7
|
-
owlplanner/rates.py,sha256=CRQBaAXIB7hkxkLyDxxptBGuwnXwoIvK9_j4qTAc23Y,15627
|
|
8
|
-
owlplanner/tax2025.py,sha256=VNRo1CiskHLEY_m5-wPVBVK-eFoFIaDF2lFRW-bHdc4,7016
|
|
9
|
-
owlplanner/timelists.py,sha256=NgyPifalMBLfKi-2mtkAems4DnOJSkPtPPXR7LmbGTU,4052
|
|
10
|
-
owlplanner/utils.py,sha256=Wy3ngYuVmWB2CeJx-v6vdhlSeSszQTjtuPtbc7hTb_A,2353
|
|
11
|
-
owlplanner/version.py,sha256=WQPMjK8ZbcCw8owMuvICW0qcZSL2-1yIsSwBODMumNs,28
|
|
12
|
-
owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
|
|
14
|
-
owlplanner-2025.2.9.dist-info/METADATA,sha256=cxhHp7ve0Rpmdr2pChd-y21DmNOejUweN_x8tR7N9K8,63946
|
|
15
|
-
owlplanner-2025.2.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
16
|
-
owlplanner-2025.2.9.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
17
|
-
owlplanner-2025.2.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|