owlplanner 2025.2.8__py3-none-any.whl → 2025.2.10__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 +15 -15
- owlplanner/config.py +142 -137
- owlplanner/logging.py +13 -13
- owlplanner/plan.py +632 -673
- owlplanner/progress.py +2 -2
- owlplanner/rates.py +74 -74
- owlplanner/tax2025.py +3 -3
- owlplanner/timelists.py +33 -31
- owlplanner/utils.py +9 -9
- owlplanner/version.py +1 -1
- {owlplanner-2025.2.8.dist-info → owlplanner-2025.2.10.dist-info}/METADATA +1 -1
- owlplanner-2025.2.10.dist-info/RECORD +17 -0
- owlplanner-2025.2.8.dist-info/RECORD +0 -17
- {owlplanner-2025.2.8.dist-info → owlplanner-2025.2.10.dist-info}/WHEEL +0 -0
- {owlplanner-2025.2.8.dist-info → owlplanner-2025.2.10.dist-info}/licenses/LICENSE +0 -0
owlplanner/plan.py
CHANGED
|
@@ -63,10 +63,10 @@ def _genXi_n(profile, fraction, n_d, N_n, a, b, c):
|
|
|
63
63
|
Series is unadjusted for inflation.
|
|
64
64
|
"""
|
|
65
65
|
xi = np.ones(N_n)
|
|
66
|
-
if profile ==
|
|
66
|
+
if profile == "flat":
|
|
67
67
|
if n_d < N_n:
|
|
68
68
|
xi[n_d:] *= fraction
|
|
69
|
-
elif profile ==
|
|
69
|
+
elif profile == "smile":
|
|
70
70
|
span = N_n - 1 - c
|
|
71
71
|
x = np.linspace(0, span, N_n - c)
|
|
72
72
|
a /= 100
|
|
@@ -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(
|
|
85
|
+
raise ValueError(f"Unknown profile type {profile}.")
|
|
86
86
|
|
|
87
87
|
return xi
|
|
88
88
|
|
|
@@ -142,7 +142,7 @@ def clone(plan, newname=None, *, verbose=True, logstreams=None):
|
|
|
142
142
|
newplan.setLogstreams(verbose, logstreams)
|
|
143
143
|
|
|
144
144
|
if newname is None:
|
|
145
|
-
newplan.rename(plan._name +
|
|
145
|
+
newplan.rename(plan._name + " (copy)")
|
|
146
146
|
else:
|
|
147
147
|
newplan.rename(newname)
|
|
148
148
|
|
|
@@ -160,8 +160,8 @@ def _checkCaseStatus(func):
|
|
|
160
160
|
|
|
161
161
|
@wraps(func)
|
|
162
162
|
def wrapper(self, *args, **kwargs):
|
|
163
|
-
if self.caseStatus !=
|
|
164
|
-
self.mylog.vprint(
|
|
163
|
+
if self.caseStatus != "solved":
|
|
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 =
|
|
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 =
|
|
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,8 +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
|
-
% (int(pt / 60), pt % 60, int(rt / 60), rt % 60))
|
|
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.")
|
|
206
205
|
return result
|
|
207
206
|
|
|
208
207
|
return wrapper
|
|
@@ -221,8 +220,8 @@ class Plan(object):
|
|
|
221
220
|
and the third the life expectancy. Last argument is a name for
|
|
222
221
|
the plan.
|
|
223
222
|
"""
|
|
224
|
-
if name ==
|
|
225
|
-
raise ValueError(
|
|
223
|
+
if name == "":
|
|
224
|
+
raise ValueError("Plan must have a name")
|
|
226
225
|
|
|
227
226
|
self._name = name
|
|
228
227
|
self.setLogstreams(verbose, logstreams)
|
|
@@ -235,21 +234,21 @@ class Plan(object):
|
|
|
235
234
|
self.N_z = 2
|
|
236
235
|
|
|
237
236
|
# Default interpolation parameters for allocation ratios.
|
|
238
|
-
self.interpMethod =
|
|
237
|
+
self.interpMethod = "linear"
|
|
239
238
|
self._interpolator = self._linInterp
|
|
240
239
|
self.interpCenter = 15
|
|
241
240
|
self.interpWidth = 5
|
|
242
241
|
|
|
243
|
-
self.defaultPlots =
|
|
244
|
-
self.defaultSolver =
|
|
242
|
+
self.defaultPlots = "nominal"
|
|
243
|
+
self.defaultSolver = "HiGHS"
|
|
245
244
|
|
|
246
245
|
self.N_i = len(yobs)
|
|
247
|
-
assert 0 < self.N_i and self.N_i <= 2,
|
|
248
|
-
assert self.N_i == len(expectancy),
|
|
249
|
-
assert self.N_i == len(inames),
|
|
250
|
-
assert inames[0] !=
|
|
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."
|
|
249
|
+
assert inames[0] != "" or (self.N_i == 2 and inames[1] == ""), "Name for each individual must be provided."
|
|
251
250
|
|
|
252
|
-
self.filingStatus = [
|
|
251
|
+
self.filingStatus = ["single", "married"][self.N_i - 1]
|
|
253
252
|
|
|
254
253
|
self.inames = inames
|
|
255
254
|
self.yobs = np.array(yobs, dtype=np.int32)
|
|
@@ -300,11 +299,11 @@ class Plan(object):
|
|
|
300
299
|
self.prevMAGI = np.zeros((2))
|
|
301
300
|
|
|
302
301
|
# Scenario starts at the beginning of this year and ends at the end of the last year.
|
|
303
|
-
|
|
304
|
-
|
|
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}.")
|
|
305
304
|
for i in range(self.N_i):
|
|
306
|
-
|
|
307
|
-
|
|
305
|
+
endyear = thisyear + self.horizons[i] - 1
|
|
306
|
+
self.mylog.vprint(f"{self.inames[i]:>14}: life horizon from {thisyear} -> {endyear}.")
|
|
308
307
|
|
|
309
308
|
# Prepare income tax and RMD time series.
|
|
310
309
|
self.rho_in = tx.rho_in(self.yobs, self.N_n)
|
|
@@ -320,11 +319,11 @@ class Plan(object):
|
|
|
320
319
|
self.timeListsFileName = "None"
|
|
321
320
|
self.timeLists = {}
|
|
322
321
|
self.zeroContributions()
|
|
323
|
-
self.caseStatus =
|
|
322
|
+
self.caseStatus = "unsolved"
|
|
324
323
|
self.rateMethod = None
|
|
325
324
|
|
|
326
325
|
self.ARCoord = None
|
|
327
|
-
self.objective =
|
|
326
|
+
self.objective = "unknown"
|
|
328
327
|
|
|
329
328
|
# Placeholders to check if properly configured.
|
|
330
329
|
self.xi_n = None
|
|
@@ -337,7 +336,7 @@ class Plan(object):
|
|
|
337
336
|
|
|
338
337
|
def setLogstreams(self, verbose, logstreams):
|
|
339
338
|
self.mylog = logging.Logger(verbose, logstreams)
|
|
340
|
-
# self.mylog.vprint("Setting logstreams to
|
|
339
|
+
# self.mylog.vprint(f"Setting logstreams to {logstreams}.")
|
|
341
340
|
|
|
342
341
|
def logger(self):
|
|
343
342
|
return self.mylog
|
|
@@ -361,13 +360,13 @@ class Plan(object):
|
|
|
361
360
|
thisyear = date.today().year
|
|
362
361
|
|
|
363
362
|
if isinstance(mydate, date):
|
|
364
|
-
mydate = mydate.strftime(
|
|
363
|
+
mydate = mydate.strftime("%Y-%m-%d")
|
|
365
364
|
|
|
366
|
-
if mydate is None or mydate ==
|
|
365
|
+
if mydate is None or mydate == "today":
|
|
367
366
|
refdate = date.today()
|
|
368
|
-
self.startDate = refdate.strftime(
|
|
367
|
+
self.startDate = refdate.strftime("%Y-%m-%d")
|
|
369
368
|
else:
|
|
370
|
-
mydatelist = mydate.split(
|
|
369
|
+
mydatelist = mydate.split("-")
|
|
371
370
|
if len(mydatelist) == 2 or len(mydatelist) == 3:
|
|
372
371
|
self.startDate = mydate
|
|
373
372
|
# Ignore the year provided.
|
|
@@ -379,7 +378,7 @@ class Plan(object):
|
|
|
379
378
|
# Take midnight as the reference.
|
|
380
379
|
self.yearFracLeft = 1 - (refdate.timetuple().tm_yday - 1) / (365 + lp)
|
|
381
380
|
|
|
382
|
-
self.mylog.vprint(
|
|
381
|
+
self.mylog.vprint(f"Setting 1st-year starting date to {self.startDate}.")
|
|
383
382
|
|
|
384
383
|
return None
|
|
385
384
|
|
|
@@ -390,11 +389,11 @@ class Plan(object):
|
|
|
390
389
|
if value is None:
|
|
391
390
|
return self.defaultPlots
|
|
392
391
|
|
|
393
|
-
opts = [
|
|
392
|
+
opts = ["nominal", "today"]
|
|
394
393
|
if value in opts:
|
|
395
394
|
return value
|
|
396
395
|
|
|
397
|
-
raise ValueError(
|
|
396
|
+
raise ValueError(f"Value type must be one of: {opts}")
|
|
398
397
|
|
|
399
398
|
return None
|
|
400
399
|
|
|
@@ -404,7 +403,7 @@ class Plan(object):
|
|
|
404
403
|
to distinguish graph outputs and as base name for
|
|
405
404
|
saving configurations and workbooks.
|
|
406
405
|
"""
|
|
407
|
-
self.mylog.vprint(
|
|
406
|
+
self.mylog.vprint(f"Renaming plan {self._name} -> {newname}.")
|
|
408
407
|
self._name = newname
|
|
409
408
|
|
|
410
409
|
def setSpousalDepositFraction(self, eta):
|
|
@@ -417,13 +416,13 @@ class Plan(object):
|
|
|
417
416
|
where s_n is the surplus amount. Here d_0n is the taxable account
|
|
418
417
|
deposit for the first spouse while d_1n is for the second spouse.
|
|
419
418
|
"""
|
|
420
|
-
assert 0 <= eta and eta <= 1,
|
|
419
|
+
assert 0 <= eta and eta <= 1, "Fraction must be between 0 and 1."
|
|
421
420
|
if self.N_i != 2:
|
|
422
|
-
self.mylog.vprint(
|
|
421
|
+
self.mylog.vprint("Deposit fraction can only be 0 for single individuals.")
|
|
423
422
|
eta = 0
|
|
424
423
|
else:
|
|
425
|
-
self.mylog.vprint(
|
|
426
|
-
self.mylog.vprint(
|
|
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}")
|
|
427
426
|
self.eta = eta
|
|
428
427
|
|
|
429
428
|
def setDefaultPlots(self, value):
|
|
@@ -432,69 +431,69 @@ class Plan(object):
|
|
|
432
431
|
"""
|
|
433
432
|
|
|
434
433
|
self.defaultPlots = self._checkValue(value)
|
|
435
|
-
self.mylog.vprint(
|
|
434
|
+
self.mylog.vprint(f"Setting plots default value to {value}.")
|
|
436
435
|
|
|
437
436
|
def setDividendRate(self, mu):
|
|
438
437
|
"""
|
|
439
438
|
Set dividend rate on equities. Rate is in percent. Default 2%.
|
|
440
439
|
"""
|
|
441
|
-
assert 0 <= mu and mu <= 100,
|
|
440
|
+
assert 0 <= mu and mu <= 100, "Rate must be between 0 and 100."
|
|
442
441
|
mu /= 100
|
|
443
|
-
self.mylog.vprint(
|
|
442
|
+
self.mylog.vprint(f"Dividend return rate on equities set to {u.pc(mu, f=1)}.")
|
|
444
443
|
self.mu = mu
|
|
445
|
-
self.caseStatus =
|
|
444
|
+
self.caseStatus = "modified"
|
|
446
445
|
|
|
447
446
|
def setLongTermCapitalTaxRate(self, psi):
|
|
448
447
|
"""
|
|
449
448
|
Set long-term income tax rate. Rate is in percent. Default 15%.
|
|
450
449
|
"""
|
|
451
|
-
assert 0 <= psi and psi <= 100,
|
|
450
|
+
assert 0 <= psi and psi <= 100, "Rate must be between 0 and 100."
|
|
452
451
|
psi /= 100
|
|
453
|
-
self.mylog.vprint(
|
|
452
|
+
self.mylog.vprint(f"Long-term capital gain income tax set to {u.pc(psi, f=0)}.")
|
|
454
453
|
self.psi = psi
|
|
455
|
-
self.caseStatus =
|
|
454
|
+
self.caseStatus = "modified"
|
|
456
455
|
|
|
457
456
|
def setBeneficiaryFractions(self, phi):
|
|
458
457
|
"""
|
|
459
458
|
Set fractions of savings accounts that is left to surviving spouse.
|
|
460
459
|
Default is [1, 1, 1] for taxable, tax-deferred, adn tax-exempt accounts.
|
|
461
460
|
"""
|
|
462
|
-
assert len(phi) == self.N_j,
|
|
461
|
+
assert len(phi) == self.N_j, f"Fractions must have {self.N_j} entries."
|
|
463
462
|
for j in range(self.N_j):
|
|
464
|
-
assert 0 <= phi[j] <= 1,
|
|
463
|
+
assert 0 <= phi[j] <= 1, "Fractions must be between 0 and 1."
|
|
465
464
|
|
|
466
465
|
self.phi_j = np.array(phi, dtype=np.float32)
|
|
467
|
-
self.mylog.vprint(
|
|
468
|
-
self.caseStatus =
|
|
466
|
+
self.mylog.vprint(f"Spousal beneficiary fractions set to {phi}.")
|
|
467
|
+
self.caseStatus = "modified"
|
|
469
468
|
|
|
470
469
|
if np.any(self.phi_j != 1):
|
|
471
|
-
self.mylog.vprint(
|
|
472
|
-
self.mylog.vprint(
|
|
470
|
+
self.mylog.vprint("Consider changing spousal deposit fraction for better convergence.")
|
|
471
|
+
self.mylog.vprint(f"\tRecommended: setSpousalDepositFraction({self.i_d}.)")
|
|
473
472
|
|
|
474
473
|
def setHeirsTaxRate(self, nu):
|
|
475
474
|
"""
|
|
476
475
|
Set the heirs tax rate on the tax-deferred portion of the estate.
|
|
477
476
|
Rate is in percent. Default is 30%.
|
|
478
477
|
"""
|
|
479
|
-
assert 0 <= nu and nu <= 100,
|
|
478
|
+
assert 0 <= nu and nu <= 100, "Rate must be between 0 and 100."
|
|
480
479
|
nu /= 100
|
|
481
|
-
self.mylog.vprint(
|
|
480
|
+
self.mylog.vprint(f"Heirs tax rate on tax-deferred portion of estate set to {u.pc(nu, f=0)}.")
|
|
482
481
|
self.nu = nu
|
|
483
|
-
self.caseStatus =
|
|
482
|
+
self.caseStatus = "modified"
|
|
484
483
|
|
|
485
|
-
def setPension(self, amounts, ages, indexed=[False, False], units=
|
|
484
|
+
def setPension(self, amounts, ages, indexed=[False, False], units="k"):
|
|
486
485
|
"""
|
|
487
486
|
Set value of pension for each individual and commencement age.
|
|
488
487
|
Units are in $k, unless specified otherwise: 'k', 'M', or '1'.
|
|
489
488
|
"""
|
|
490
|
-
assert len(amounts) == self.N_i,
|
|
491
|
-
assert len(ages) == self.N_i,
|
|
492
|
-
assert len(indexed) >= self.N_i,
|
|
489
|
+
assert len(amounts) == self.N_i, f"Amounts must have {self.N_i} entries."
|
|
490
|
+
assert len(ages) == self.N_i, f"Ages must have {self.N_i} entries."
|
|
491
|
+
assert len(indexed) >= self.N_i, f"Indexed list must have at least {self.N_i} entries."
|
|
493
492
|
|
|
494
493
|
fac = u.getUnits(units)
|
|
495
494
|
amounts = u.rescale(amounts, fac)
|
|
496
495
|
|
|
497
|
-
self.mylog.vprint(
|
|
496
|
+
self.mylog.vprint("Setting pension of", [u.d(amounts[i]) for i in range(self.N_i)], "at age(s)", ages)
|
|
498
497
|
|
|
499
498
|
thisyear = date.today().year
|
|
500
499
|
# Use zero array freshly initialized.
|
|
@@ -511,24 +510,24 @@ class Plan(object):
|
|
|
511
510
|
self.pensionAmounts = np.array(amounts)
|
|
512
511
|
self.pensionAges = np.array(ages, dtype=np.int32)
|
|
513
512
|
self.pensionIndexed = indexed
|
|
514
|
-
self.caseStatus =
|
|
513
|
+
self.caseStatus = "modified"
|
|
515
514
|
self._adjustedParameters = False
|
|
516
515
|
|
|
517
|
-
def setSocialSecurity(self, amounts, ages, units=
|
|
516
|
+
def setSocialSecurity(self, amounts, ages, units="k"):
|
|
518
517
|
"""
|
|
519
518
|
Set value of social security for each individual and commencement age.
|
|
520
519
|
Units are in $k, unless specified otherwise: 'k', 'M', or '1'.
|
|
521
520
|
"""
|
|
522
|
-
assert len(amounts) == self.N_i,
|
|
523
|
-
assert len(ages) == self.N_i,
|
|
521
|
+
assert len(amounts) == self.N_i, f"Amounts must have {self.N_i} entries."
|
|
522
|
+
assert len(ages) == self.N_i, f"Ages must have {self.N_i} entries."
|
|
524
523
|
|
|
525
524
|
fac = u.getUnits(units)
|
|
526
525
|
amounts = u.rescale(amounts, fac)
|
|
527
526
|
|
|
528
527
|
self.mylog.vprint(
|
|
529
|
-
|
|
528
|
+
"Setting social security benefits of",
|
|
530
529
|
[u.d(amounts[i]) for i in range(self.N_i)],
|
|
531
|
-
|
|
530
|
+
"at age(s)",
|
|
532
531
|
ages,
|
|
533
532
|
)
|
|
534
533
|
|
|
@@ -544,11 +543,11 @@ class Plan(object):
|
|
|
544
543
|
|
|
545
544
|
if self.N_i == 2:
|
|
546
545
|
# Approximate calculation for spousal benefit (only valid at FRA).
|
|
547
|
-
self.zeta_in[self.i_s, self.n_d:] = max(amounts[self.i_s], amounts[self.i_d])
|
|
546
|
+
self.zeta_in[self.i_s, self.n_d :] = max(amounts[self.i_s], amounts[self.i_d])
|
|
548
547
|
|
|
549
548
|
self.ssecAmounts = np.array(amounts)
|
|
550
549
|
self.ssecAges = np.array(ages, dtype=np.int32)
|
|
551
|
-
self.caseStatus =
|
|
550
|
+
self.caseStatus = "modified"
|
|
552
551
|
self._adjustedParameters = False
|
|
553
552
|
|
|
554
553
|
def setSpendingProfile(self, profile, percent=60, dip=15, increase=12, delay=0):
|
|
@@ -557,16 +556,16 @@ class Plan(object):
|
|
|
557
556
|
as a second argument. Default value is 60%.
|
|
558
557
|
Dip and increase are percent changes in the smile profile.
|
|
559
558
|
"""
|
|
560
|
-
assert 0 <= percent and percent <= 100,
|
|
561
|
-
assert 0 <= dip and dip <= 100,
|
|
562
|
-
assert -100 <= increase and increase <= 100,
|
|
563
|
-
assert 0 <= delay and delay <= self.N_n - 2
|
|
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."
|
|
564
563
|
|
|
565
564
|
self.chi = percent / 100
|
|
566
565
|
|
|
567
|
-
self.mylog.vprint(
|
|
566
|
+
self.mylog.vprint("Setting", profile, "spending profile.")
|
|
568
567
|
if self.N_i == 2:
|
|
569
|
-
self.mylog.vprint(
|
|
568
|
+
self.mylog.vprint("Securing", u.pc(self.chi, f=0), "of spending amount for surviving spouse.")
|
|
570
569
|
|
|
571
570
|
self.xi_n = _genXi_n(profile, self.chi, self.n_d, self.N_n, dip, increase, delay)
|
|
572
571
|
# Account for time elapsed in the current year.
|
|
@@ -576,7 +575,7 @@ class Plan(object):
|
|
|
576
575
|
self.smileDip = dip
|
|
577
576
|
self.smileIncrease = increase
|
|
578
577
|
self.smileDelay = delay
|
|
579
|
-
self.caseStatus =
|
|
578
|
+
self.caseStatus = "modified"
|
|
580
579
|
|
|
581
580
|
def setRates(self, method, frm=None, to=None, values=None, stdev=None, corr=None):
|
|
582
581
|
"""
|
|
@@ -603,13 +602,7 @@ class Plan(object):
|
|
|
603
602
|
self.rateFrm = frm
|
|
604
603
|
self.rateTo = to
|
|
605
604
|
self.tau_kn = dr.genSeries(self.N_n).transpose()
|
|
606
|
-
self.mylog.vprint(
|
|
607
|
-
'Generating rate series of',
|
|
608
|
-
len(self.tau_kn[0]),
|
|
609
|
-
'years using',
|
|
610
|
-
method,
|
|
611
|
-
'method.',
|
|
612
|
-
)
|
|
605
|
+
self.mylog.vprint(f"Generating rate series of {len(self.tau_kn[0])} years using {method} method.")
|
|
613
606
|
|
|
614
607
|
# Account for how late we are now in the first year and reduce rate accordingly.
|
|
615
608
|
self.tau_kn[:, 0] *= self.yearFracLeft
|
|
@@ -617,7 +610,7 @@ class Plan(object):
|
|
|
617
610
|
# Once rates are selected, (re)build cumulative inflation multipliers.
|
|
618
611
|
self.gamma_n = _genGamma_n(self.tau_kn)
|
|
619
612
|
self._adjustedParameters = False
|
|
620
|
-
self.caseStatus =
|
|
613
|
+
self.caseStatus = "modified"
|
|
621
614
|
|
|
622
615
|
def regenRates(self):
|
|
623
616
|
"""
|
|
@@ -654,24 +647,24 @@ class Plan(object):
|
|
|
654
647
|
to the beginning of the year provided.
|
|
655
648
|
"""
|
|
656
649
|
if self.rateMethod is None:
|
|
657
|
-
raise RuntimeError(
|
|
650
|
+
raise RuntimeError("A rate method needs to be first selected using setRates(...).")
|
|
658
651
|
|
|
659
652
|
thisyear = date.today().year
|
|
660
|
-
assert year > thisyear,
|
|
653
|
+
assert year > thisyear, "Internal error in forwardValue()."
|
|
661
654
|
span = year - thisyear
|
|
662
655
|
|
|
663
656
|
return amount * self.gamma_n[span]
|
|
664
657
|
|
|
665
|
-
def setAccountBalances(self, *, taxable, taxDeferred, taxFree, units=
|
|
658
|
+
def setAccountBalances(self, *, taxable, taxDeferred, taxFree, units="k"):
|
|
666
659
|
"""
|
|
667
660
|
Three lists containing the balance of all assets in each category for
|
|
668
661
|
each spouse. For single individuals, these lists will contain only
|
|
669
662
|
one entry. Units are in $k, unless specified otherwise: 'k', 'M', or '1'.
|
|
670
663
|
"""
|
|
671
|
-
plurals = [
|
|
672
|
-
assert len(taxable) == self.N_i,
|
|
673
|
-
assert len(taxDeferred) == self.N_i,
|
|
674
|
-
assert len(taxFree) == self.N_i,
|
|
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}."
|
|
675
668
|
|
|
676
669
|
fac = u.getUnits(units)
|
|
677
670
|
taxable = u.rescale(taxable, fac)
|
|
@@ -683,14 +676,14 @@ class Plan(object):
|
|
|
683
676
|
self.b_ji[1][:] = taxDeferred
|
|
684
677
|
self.b_ji[2][:] = taxFree
|
|
685
678
|
self.beta_ij = self.b_ji.transpose()
|
|
686
|
-
self.caseStatus =
|
|
679
|
+
self.caseStatus = "modified"
|
|
687
680
|
|
|
688
|
-
self.mylog.vprint(
|
|
689
|
-
self.mylog.vprint(
|
|
690
|
-
self.mylog.vprint(
|
|
691
|
-
self.mylog.vprint(
|
|
681
|
+
self.mylog.vprint("Taxable balances:", *[u.d(taxable[i]) for i in range(self.N_i)])
|
|
682
|
+
self.mylog.vprint("Tax-deferred balances:", *[u.d(taxDeferred[i]) for i in range(self.N_i)])
|
|
683
|
+
self.mylog.vprint("Tax-free balances:", *[u.d(taxFree[i]) for i in range(self.N_i)])
|
|
684
|
+
self.mylog.vprint("Sum of all savings accounts:", u.d(np.sum(taxable) + np.sum(taxDeferred) + np.sum(taxFree)))
|
|
692
685
|
self.mylog.vprint(
|
|
693
|
-
|
|
686
|
+
"Post-tax total wealth of approximately",
|
|
694
687
|
u.d(np.sum(taxable) + 0.7 * np.sum(taxDeferred) + np.sum(taxFree)),
|
|
695
688
|
)
|
|
696
689
|
|
|
@@ -706,19 +699,19 @@ class Plan(object):
|
|
|
706
699
|
5 years. This means that the transition from initial to final
|
|
707
700
|
will start occuring in 10 years (15-5) and will end in 20 years (15+5).
|
|
708
701
|
"""
|
|
709
|
-
if method ==
|
|
702
|
+
if method == "linear":
|
|
710
703
|
self._interpolator = self._linInterp
|
|
711
|
-
elif method ==
|
|
704
|
+
elif method == "s-curve":
|
|
712
705
|
self._interpolator = self._tanhInterp
|
|
713
706
|
self.interpCenter = center
|
|
714
707
|
self.interpWidth = width
|
|
715
708
|
else:
|
|
716
|
-
raise ValueError(
|
|
709
|
+
raise ValueError(f"Method {method} not supported.")
|
|
717
710
|
|
|
718
711
|
self.interpMethod = method
|
|
719
|
-
self.caseStatus =
|
|
712
|
+
self.caseStatus = "modified"
|
|
720
713
|
|
|
721
|
-
self.mylog.vprint(
|
|
714
|
+
self.mylog.vprint(f"Asset allocation interpolation method set to {method}.")
|
|
722
715
|
|
|
723
716
|
def setAllocationRatios(self, allocType, taxable=None, taxDeferred=None, taxFree=None, generic=None):
|
|
724
717
|
"""
|
|
@@ -744,34 +737,22 @@ class Plan(object):
|
|
|
744
737
|
"""
|
|
745
738
|
self.boundsAR = {}
|
|
746
739
|
self.alpha_ijkn = np.zeros((self.N_i, self.N_j, self.N_k, self.N_n + 1))
|
|
747
|
-
if allocType ==
|
|
740
|
+
if allocType == "account":
|
|
748
741
|
# Make sure we have proper input.
|
|
749
742
|
for item in [taxable, taxDeferred, taxFree]:
|
|
750
|
-
assert len(item) == self.N_i,
|
|
743
|
+
assert len(item) == self.N_i, f"{item} must have one entry per individual."
|
|
751
744
|
for i in range(self.N_i):
|
|
752
745
|
# Initial and final.
|
|
753
|
-
assert len(item[i]) == 2,
|
|
754
|
-
item,
|
|
755
|
-
i,
|
|
756
|
-
)
|
|
746
|
+
assert len(item[i]) == 2, f"{item}[{i}] must have 2 lists (initial and final)."
|
|
757
747
|
for z in range(2):
|
|
758
|
-
assert len(item[i][z]) == self.N_k,
|
|
759
|
-
|
|
760
|
-
i,
|
|
761
|
-
z,
|
|
762
|
-
self.N_k,
|
|
763
|
-
)
|
|
764
|
-
assert abs(sum(item[i][z]) - 100) < 0.01, 'Sum of percentages must add to 100.'
|
|
748
|
+
assert len(item[i][z]) == self.N_k, f"{item}[{i}][{z}] must have {self.N_k} entries."
|
|
749
|
+
assert abs(sum(item[i][z]) - 100) < 0.01, "Sum of percentages must add to 100."
|
|
765
750
|
|
|
766
751
|
for i in range(self.N_i):
|
|
767
|
-
self.mylog.vprint(
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
)
|
|
772
|
-
self.mylog.vprint(' taxable:', taxable[i][0], '->', taxable[i][1])
|
|
773
|
-
self.mylog.vprint(' taxDeferred:', taxDeferred[i][0], '->', taxDeferred[i][1])
|
|
774
|
-
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]}")
|
|
775
756
|
|
|
776
757
|
# Order in alpha is j, i, 0/1, k.
|
|
777
758
|
alpha = {}
|
|
@@ -787,30 +768,22 @@ class Plan(object):
|
|
|
787
768
|
dat = self._interpolator(start, end, Nin)
|
|
788
769
|
self.alpha_ijkn[i, j, k, :Nin] = dat[:]
|
|
789
770
|
|
|
790
|
-
self.boundsAR[
|
|
791
|
-
self.boundsAR[
|
|
792
|
-
self.boundsAR[
|
|
771
|
+
self.boundsAR["taxable"] = taxable
|
|
772
|
+
self.boundsAR["tax-deferred"] = taxDeferred
|
|
773
|
+
self.boundsAR["tax-free"] = taxFree
|
|
793
774
|
|
|
794
|
-
elif allocType ==
|
|
795
|
-
assert len(generic) == self.N_i,
|
|
775
|
+
elif allocType == "individual":
|
|
776
|
+
assert len(generic) == self.N_i, "generic must have one list per individual."
|
|
796
777
|
for i in range(self.N_i):
|
|
797
778
|
# Initial and final.
|
|
798
|
-
assert len(generic[i]) == 2,
|
|
779
|
+
assert len(generic[i]) == 2, f"generic[{i}] must have 2 lists (initial and final)."
|
|
799
780
|
for z in range(2):
|
|
800
|
-
assert len(generic[i][z]) == self.N_k,
|
|
801
|
-
|
|
802
|
-
z,
|
|
803
|
-
self.N_k,
|
|
804
|
-
)
|
|
805
|
-
assert abs(sum(generic[i][z]) - 100) < 0.01, 'Sum of percentages must add to 100.'
|
|
781
|
+
assert len(generic[i][z]) == self.N_k, f"generic[{i}][{z}] must have {self.N_k} entries."
|
|
782
|
+
assert abs(sum(generic[i][z]) - 100) < 0.01, "Sum of percentages must add to 100."
|
|
806
783
|
|
|
807
784
|
for i in range(self.N_i):
|
|
808
|
-
self.mylog.vprint(
|
|
809
|
-
|
|
810
|
-
': Setting gliding allocation ratios (%) to',
|
|
811
|
-
allocType,
|
|
812
|
-
)
|
|
813
|
-
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]}")
|
|
814
787
|
|
|
815
788
|
for i in range(self.N_i):
|
|
816
789
|
Nin = self.horizons[i] + 1
|
|
@@ -821,19 +794,16 @@ class Plan(object):
|
|
|
821
794
|
for j in range(self.N_j):
|
|
822
795
|
self.alpha_ijkn[i, j, k, :Nin] = dat[:]
|
|
823
796
|
|
|
824
|
-
self.boundsAR[
|
|
797
|
+
self.boundsAR["generic"] = generic
|
|
825
798
|
|
|
826
|
-
elif allocType ==
|
|
827
|
-
assert len(generic) == 2,
|
|
799
|
+
elif allocType == "spouses":
|
|
800
|
+
assert len(generic) == 2, "generic must have 2 entries (initial and final)."
|
|
828
801
|
for z in range(2):
|
|
829
|
-
assert len(generic[z]) == self.N_k,
|
|
830
|
-
|
|
831
|
-
self.N_k,
|
|
832
|
-
)
|
|
833
|
-
assert abs(sum(generic[z]) - 100) < 0.01, 'Sum of percentages must add to 100.'
|
|
802
|
+
assert len(generic[z]) == self.N_k, f"generic[{z}] must have {self.N_k} entries."
|
|
803
|
+
assert abs(sum(generic[z]) - 100) < 0.01, "Sum of percentages must add to 100."
|
|
834
804
|
|
|
835
|
-
self.mylog.vprint(
|
|
836
|
-
self.mylog.vprint(
|
|
805
|
+
self.mylog.vprint(f"Setting gliding allocation ratios (%) to {allocType}.")
|
|
806
|
+
self.mylog.vprint(f"\t{generic[0]} -> {generic[1]}")
|
|
837
807
|
|
|
838
808
|
# Use longest-lived spouse for both time scales.
|
|
839
809
|
Nxn = max(self.horizons) + 1
|
|
@@ -846,12 +816,12 @@ class Plan(object):
|
|
|
846
816
|
for j in range(self.N_j):
|
|
847
817
|
self.alpha_ijkn[i, j, k, :Nxn] = dat[:]
|
|
848
818
|
|
|
849
|
-
self.boundsAR[
|
|
819
|
+
self.boundsAR["generic"] = generic
|
|
850
820
|
|
|
851
821
|
self.ARCoord = allocType
|
|
852
|
-
self.caseStatus =
|
|
822
|
+
self.caseStatus = "modified"
|
|
853
823
|
|
|
854
|
-
self.mylog.vprint(
|
|
824
|
+
self.mylog.vprint(f"Interpolating assets allocation ratios using {self.interpMethod} method.")
|
|
855
825
|
|
|
856
826
|
def readContributions(self, filename):
|
|
857
827
|
"""
|
|
@@ -877,7 +847,7 @@ class Plan(object):
|
|
|
877
847
|
try:
|
|
878
848
|
filename, self.timeLists = timelists.read(filename, self.inames, self.horizons, self.mylog)
|
|
879
849
|
except Exception as e:
|
|
880
|
-
raise Exception(
|
|
850
|
+
raise Exception(f"Unsuccessful read of contributions: {e}")
|
|
881
851
|
return False
|
|
882
852
|
|
|
883
853
|
self.timeListsFileName = filename
|
|
@@ -893,20 +863,20 @@ class Plan(object):
|
|
|
893
863
|
# Now fill in parameters which are in $.
|
|
894
864
|
for i, iname in enumerate(self.inames):
|
|
895
865
|
h = self.horizons[i]
|
|
896
|
-
self.omega_in[i, :h] = self.timeLists[iname][
|
|
897
|
-
self.kappa_ijn[i, 0, :h] = self.timeLists[iname][
|
|
898
|
-
self.kappa_ijn[i, 1, :h] = self.timeLists[iname][
|
|
899
|
-
self.kappa_ijn[i, 2, :h] = self.timeLists[iname][
|
|
900
|
-
self.kappa_ijn[i, 1, :h] += self.timeLists[iname][
|
|
901
|
-
self.kappa_ijn[i, 2, :h] += self.timeLists[iname][
|
|
902
|
-
self.myRothX_in[i, :h] = self.timeLists[iname][
|
|
903
|
-
self.Lambda_in[i, :h] = self.timeLists[iname][
|
|
866
|
+
self.omega_in[i, :h] = self.timeLists[iname]["anticipated wages"].iloc[:h]
|
|
867
|
+
self.kappa_ijn[i, 0, :h] = self.timeLists[iname]["taxable ctrb"].iloc[:h]
|
|
868
|
+
self.kappa_ijn[i, 1, :h] = self.timeLists[iname]["401k ctrb"].iloc[:h]
|
|
869
|
+
self.kappa_ijn[i, 2, :h] = self.timeLists[iname]["Roth 401k ctrb"].iloc[:h]
|
|
870
|
+
self.kappa_ijn[i, 1, :h] += self.timeLists[iname]["IRA ctrb"].iloc[:h]
|
|
871
|
+
self.kappa_ijn[i, 2, :h] += self.timeLists[iname]["Roth IRA ctrb"].iloc[:h]
|
|
872
|
+
self.myRothX_in[i, :h] = self.timeLists[iname]["Roth conv"].iloc[:h]
|
|
873
|
+
self.Lambda_in[i, :h] = self.timeLists[iname]["big-ticket items"].iloc[:h]
|
|
904
874
|
|
|
905
875
|
# In 1st year, reduce wages and contributions depending on starting date.
|
|
906
876
|
self.omega_in[:, 0] *= self.yearFracLeft
|
|
907
877
|
self.kappa_ijn[:, :, 0] *= self.yearFracLeft
|
|
908
878
|
|
|
909
|
-
self.caseStatus =
|
|
879
|
+
self.caseStatus = "modified"
|
|
910
880
|
|
|
911
881
|
return self.timeLists
|
|
912
882
|
|
|
@@ -917,14 +887,14 @@ class Plan(object):
|
|
|
917
887
|
if self.timeLists is None:
|
|
918
888
|
return None
|
|
919
889
|
|
|
920
|
-
self.mylog.vprint(
|
|
890
|
+
self.mylog.vprint("Preparing wages and contributions workbook.")
|
|
921
891
|
|
|
922
892
|
def fillsheet(sheet, i):
|
|
923
893
|
sheet.title = self.inames[i]
|
|
924
894
|
df = self.timeLists[self.inames[i]]
|
|
925
895
|
for row in dataframe_to_rows(df, index=False, header=True):
|
|
926
896
|
sheet.append(row)
|
|
927
|
-
_formatSpreadsheet(sheet,
|
|
897
|
+
_formatSpreadsheet(sheet, "currency")
|
|
928
898
|
|
|
929
899
|
wb = Workbook()
|
|
930
900
|
ws = wb.active
|
|
@@ -940,23 +910,32 @@ class Plan(object):
|
|
|
940
910
|
"""
|
|
941
911
|
Reset all contributions variables to zero.
|
|
942
912
|
"""
|
|
943
|
-
self.mylog.vprint(
|
|
913
|
+
self.mylog.vprint("Resetting wages and contributions to zero.")
|
|
944
914
|
|
|
945
915
|
# Reset parameters with zeros.
|
|
946
|
-
self.omega_in[:, :] = 0.
|
|
947
|
-
self.Lambda_in[:, :] = 0.
|
|
948
|
-
self.myRothX_in[:, :] = 0.
|
|
949
|
-
self.kappa_ijn[:, :, :] = 0.
|
|
950
|
-
|
|
951
|
-
cols = [
|
|
952
|
-
|
|
916
|
+
self.omega_in[:, :] = 0.0
|
|
917
|
+
self.Lambda_in[:, :] = 0.0
|
|
918
|
+
self.myRothX_in[:, :] = 0.0
|
|
919
|
+
self.kappa_ijn[:, :, :] = 0.0
|
|
920
|
+
|
|
921
|
+
cols = [
|
|
922
|
+
"year",
|
|
923
|
+
"anticipated wages",
|
|
924
|
+
"taxable ctrb",
|
|
925
|
+
"401k ctrb",
|
|
926
|
+
"Roth 401k ctrb",
|
|
927
|
+
"IRA ctrb",
|
|
928
|
+
"Roth IRA ctrb",
|
|
929
|
+
"Roth conv",
|
|
930
|
+
"big-ticket items",
|
|
931
|
+
]
|
|
953
932
|
for i, iname in enumerate(self.inames):
|
|
954
933
|
h = self.horizons[i]
|
|
955
934
|
df = pd.DataFrame(0, index=np.arange(h), columns=cols)
|
|
956
|
-
df[
|
|
935
|
+
df["year"] = self.year_n[:h]
|
|
957
936
|
self.timeLists[iname] = df
|
|
958
937
|
|
|
959
|
-
self.caseStatus =
|
|
938
|
+
self.caseStatus = "modified"
|
|
960
939
|
|
|
961
940
|
return self.timeLists
|
|
962
941
|
|
|
@@ -977,7 +956,7 @@ class Plan(object):
|
|
|
977
956
|
is happening, and "w" is the width of the transition.
|
|
978
957
|
"""
|
|
979
958
|
c = self.interpCenter
|
|
980
|
-
w = self.interpWidth + 0.0001
|
|
959
|
+
w = self.interpWidth + 0.0001 # Avoid division by zero.
|
|
981
960
|
t = np.linspace(0, numPoints, numPoints)
|
|
982
961
|
# Solve 2x2 system to match end points exactly.
|
|
983
962
|
th0 = np.tanh((t[0] - c) / w)
|
|
@@ -997,10 +976,10 @@ class Plan(object):
|
|
|
997
976
|
Adjust parameters that follow inflation.
|
|
998
977
|
"""
|
|
999
978
|
if self.rateMethod is None:
|
|
1000
|
-
raise RuntimeError(
|
|
979
|
+
raise RuntimeError("A rate method needs to be first selected using setRates(...).")
|
|
1001
980
|
|
|
1002
981
|
if not self._adjustedParameters:
|
|
1003
|
-
self.mylog.vprint(
|
|
982
|
+
self.mylog.vprint("Adjusting parameters for inflation.")
|
|
1004
983
|
self.DeltaBar_tn = self.Delta_tn * self.gamma_n[:-1]
|
|
1005
984
|
self.zetaBar_in = self.zeta_in * self.gamma_n[:-1]
|
|
1006
985
|
self.sigmaBar_n = self.sigma_n * self.gamma_n[:-1]
|
|
@@ -1021,25 +1000,19 @@ class Plan(object):
|
|
|
1021
1000
|
"""
|
|
1022
1001
|
# Stack all variables in a single block vector.
|
|
1023
1002
|
C = {}
|
|
1024
|
-
C[
|
|
1025
|
-
C[
|
|
1026
|
-
C[
|
|
1027
|
-
C[
|
|
1028
|
-
C[
|
|
1029
|
-
C[
|
|
1030
|
-
C[
|
|
1031
|
-
C[
|
|
1032
|
-
C[
|
|
1033
|
-
self.nvars = _qC(C[
|
|
1003
|
+
C["b"] = 0
|
|
1004
|
+
C["d"] = _qC(C["b"], self.N_i, self.N_j, self.N_n + 1)
|
|
1005
|
+
C["e"] = _qC(C["d"], self.N_i, self.N_n)
|
|
1006
|
+
C["F"] = _qC(C["e"], self.N_n)
|
|
1007
|
+
C["g"] = _qC(C["F"], self.N_t, self.N_n)
|
|
1008
|
+
C["s"] = _qC(C["g"], self.N_n)
|
|
1009
|
+
C["w"] = _qC(C["s"], self.N_n)
|
|
1010
|
+
C["x"] = _qC(C["w"], self.N_i, self.N_j, self.N_n)
|
|
1011
|
+
C["z"] = _qC(C["x"], self.N_i, self.N_n)
|
|
1012
|
+
self.nvars = _qC(C["z"], self.N_i, self.N_n, self.N_z)
|
|
1034
1013
|
|
|
1035
1014
|
self.C = C
|
|
1036
|
-
self.mylog.vprint(
|
|
1037
|
-
'Problem has',
|
|
1038
|
-
len(C),
|
|
1039
|
-
'distinct time series forming',
|
|
1040
|
-
self.nvars,
|
|
1041
|
-
'decision variables.',
|
|
1042
|
-
)
|
|
1015
|
+
self.mylog.vprint(f"Problem has {len(C)} distinct time series forming {self.nvars} decision variables.")
|
|
1043
1016
|
|
|
1044
1017
|
return None
|
|
1045
1018
|
|
|
@@ -1063,15 +1036,15 @@ class Plan(object):
|
|
|
1063
1036
|
i_s = self.i_s
|
|
1064
1037
|
n_d = self.n_d
|
|
1065
1038
|
|
|
1066
|
-
Cb = self.C[
|
|
1067
|
-
Cd = self.C[
|
|
1068
|
-
Ce = self.C[
|
|
1069
|
-
CF = self.C[
|
|
1070
|
-
Cg = self.C[
|
|
1071
|
-
Cs = self.C[
|
|
1072
|
-
Cw = self.C[
|
|
1073
|
-
Cx = self.C[
|
|
1074
|
-
Cz = self.C[
|
|
1039
|
+
Cb = self.C["b"]
|
|
1040
|
+
Cd = self.C["d"]
|
|
1041
|
+
Ce = self.C["e"]
|
|
1042
|
+
CF = self.C["F"]
|
|
1043
|
+
Cg = self.C["g"]
|
|
1044
|
+
Cs = self.C["s"]
|
|
1045
|
+
Cw = self.C["w"]
|
|
1046
|
+
Cx = self.C["x"]
|
|
1047
|
+
Cz = self.C["z"]
|
|
1075
1048
|
|
|
1076
1049
|
tau_ijn = np.zeros((Ni, Nj, Nn))
|
|
1077
1050
|
for i in range(Ni):
|
|
@@ -1083,15 +1056,15 @@ class Plan(object):
|
|
|
1083
1056
|
Tau1_ijn = 1 + tau_ijn
|
|
1084
1057
|
Tauh_ijn = 1 + tau_ijn / 2
|
|
1085
1058
|
|
|
1086
|
-
if
|
|
1087
|
-
units = u.getUnits(options[
|
|
1059
|
+
if "units" in options:
|
|
1060
|
+
units = u.getUnits(options["units"])
|
|
1088
1061
|
else:
|
|
1089
1062
|
units = 1000
|
|
1090
1063
|
|
|
1091
1064
|
bigM = 5e6
|
|
1092
|
-
if
|
|
1065
|
+
if "bigM" in options:
|
|
1093
1066
|
# No units for bigM.
|
|
1094
|
-
bigM = options[
|
|
1067
|
+
bigM = options["bigM"]
|
|
1095
1068
|
|
|
1096
1069
|
###################################################################
|
|
1097
1070
|
# Inequality constraint matrix with upper and lower bound vectors.
|
|
@@ -1118,16 +1091,16 @@ class Plan(object):
|
|
|
1118
1091
|
B.set0_Ub(_q1(Ce, n, Nn), self.sigmaBar_n[n])
|
|
1119
1092
|
|
|
1120
1093
|
# Roth conversions equalities/inequalities.
|
|
1121
|
-
if
|
|
1122
|
-
if options[
|
|
1123
|
-
# self.mylog.vprint(
|
|
1094
|
+
if "maxRothConversion" in options:
|
|
1095
|
+
if options["maxRothConversion"] == "file":
|
|
1096
|
+
# self.mylog.vprint(f"Fixing Roth conversions to those from file {self.timeListsFileName}.")
|
|
1124
1097
|
for i in range(Ni):
|
|
1125
1098
|
for n in range(self.horizons[i]):
|
|
1126
1099
|
rhs = self.myRothX_in[i][n]
|
|
1127
1100
|
B.setRange(_q2(Cx, i, n, Ni, Nn), rhs, rhs)
|
|
1128
1101
|
else:
|
|
1129
|
-
rhsopt = options[
|
|
1130
|
-
assert isinstance(rhsopt, (int, float)),
|
|
1102
|
+
rhsopt = options["maxRothConversion"]
|
|
1103
|
+
assert isinstance(rhsopt, (int, float)), "Specified maxConversion is not a number."
|
|
1131
1104
|
rhsopt *= units
|
|
1132
1105
|
if rhsopt < 0:
|
|
1133
1106
|
# self.mylog.vprint('Unlimited Roth conversions (<0)')
|
|
@@ -1140,12 +1113,12 @@ class Plan(object):
|
|
|
1140
1113
|
B.set0_Ub(_q2(Cx, i, n, Ni, Nn), rhsopt)
|
|
1141
1114
|
|
|
1142
1115
|
# Process noRothConversions option. Also valid when N_i == 1, why not?
|
|
1143
|
-
if
|
|
1144
|
-
rhsopt = options[
|
|
1116
|
+
if "noRothConversions" in options and options["noRothConversions"] != "None":
|
|
1117
|
+
rhsopt = options["noRothConversions"]
|
|
1145
1118
|
try:
|
|
1146
1119
|
i_x = self.inames.index(rhsopt)
|
|
1147
1120
|
except ValueError:
|
|
1148
|
-
raise ValueError(
|
|
1121
|
+
raise ValueError(f"Unknown individual {rhsopt} for noRothConversions:")
|
|
1149
1122
|
|
|
1150
1123
|
for n in range(Nn):
|
|
1151
1124
|
B.set0_Ub(_q2(Cx, i_x, n, Ni, Nn), zero)
|
|
@@ -1168,11 +1141,11 @@ class Plan(object):
|
|
|
1168
1141
|
A.addNewRow(rowDic, zero, inf)
|
|
1169
1142
|
|
|
1170
1143
|
# Constraints depending on objective function.
|
|
1171
|
-
if objective ==
|
|
1144
|
+
if objective == "maxSpending":
|
|
1172
1145
|
# Impose optional constraint on final bequest requested in today's $.
|
|
1173
|
-
if
|
|
1174
|
-
bequest = options[
|
|
1175
|
-
assert isinstance(bequest, (int, float)),
|
|
1146
|
+
if "bequest" in options:
|
|
1147
|
+
bequest = options["bequest"]
|
|
1148
|
+
assert isinstance(bequest, (int, float)), "Desired bequest is not a number."
|
|
1176
1149
|
bequest *= units * self.gamma_n[-1]
|
|
1177
1150
|
else:
|
|
1178
1151
|
# If not specified, defaults to $1 (nominal $).
|
|
@@ -1187,9 +1160,9 @@ class Plan(object):
|
|
|
1187
1160
|
row.addElem(_q3(Cb, i, 2, Nn, Ni, Nj, Nn + 1), 1)
|
|
1188
1161
|
A.addRow(row, bequest, bequest)
|
|
1189
1162
|
# self.mylog.vprint('Adding bequest constraint of:', u.d(bequest))
|
|
1190
|
-
elif objective ==
|
|
1191
|
-
spending = options[
|
|
1192
|
-
assert isinstance(spending, (int, float)),
|
|
1163
|
+
elif objective == "maxBequest":
|
|
1164
|
+
spending = options["netSpending"]
|
|
1165
|
+
assert isinstance(spending, (int, float)), "Desired spending provided is not a number."
|
|
1193
1166
|
# Account for time elapsed in the current year.
|
|
1194
1167
|
spending *= units * self.yearFracLeft
|
|
1195
1168
|
# self.mylog.vprint('Maximizing bequest with desired net spending of:', u.d(spending))
|
|
@@ -1362,15 +1335,15 @@ class Plan(object):
|
|
|
1362
1335
|
|
|
1363
1336
|
# Now build a solver-neutral objective vector.
|
|
1364
1337
|
c = abc.Objective(self.nvars)
|
|
1365
|
-
if objective ==
|
|
1338
|
+
if objective == "maxSpending":
|
|
1366
1339
|
c.setElem(_q1(Cg, 0, Nn), -1)
|
|
1367
|
-
elif objective ==
|
|
1340
|
+
elif objective == "maxBequest":
|
|
1368
1341
|
for i in range(Ni):
|
|
1369
1342
|
c.setElem(_q3(Cb, i, 0, Nn, Ni, Nj, Nn + 1), -1)
|
|
1370
1343
|
c.setElem(_q3(Cb, i, 1, Nn, Ni, Nj, Nn + 1), -(1 - self.nu))
|
|
1371
1344
|
c.setElem(_q3(Cb, i, 2, Nn, Ni, Nj, Nn + 1), -1)
|
|
1372
1345
|
else:
|
|
1373
|
-
raise RuntimeError(
|
|
1346
|
+
raise RuntimeError("Internal error in objective function.")
|
|
1374
1347
|
|
|
1375
1348
|
self.A = A
|
|
1376
1349
|
self.B = B
|
|
@@ -1385,19 +1358,19 @@ class Plan(object):
|
|
|
1385
1358
|
"""
|
|
1386
1359
|
if yend + self.N_n > self.year_n[0]:
|
|
1387
1360
|
yend = self.year_n[0] - self.N_n - 1
|
|
1388
|
-
self.mylog.vprint(
|
|
1361
|
+
self.mylog.vprint(f"Warning: Upper bound for year range re-adjusted to {yend}.")
|
|
1389
1362
|
N = yend - ystart + 1
|
|
1390
1363
|
|
|
1391
|
-
self.mylog.vprint(
|
|
1364
|
+
self.mylog.vprint(f"Running historical range from {ystart} to {yend}.")
|
|
1392
1365
|
|
|
1393
1366
|
self.mylog.setVerbose(verbose)
|
|
1394
1367
|
|
|
1395
|
-
if objective ==
|
|
1396
|
-
columns = [
|
|
1397
|
-
elif objective ==
|
|
1398
|
-
columns = [
|
|
1368
|
+
if objective == "maxSpending":
|
|
1369
|
+
columns = ["partial", objective]
|
|
1370
|
+
elif objective == "maxBequest":
|
|
1371
|
+
columns = ["partial", "final"]
|
|
1399
1372
|
else:
|
|
1400
|
-
self.mylog.print(
|
|
1373
|
+
self.mylog.print(f"Invalid objective {objective}.")
|
|
1401
1374
|
return None
|
|
1402
1375
|
|
|
1403
1376
|
df = pd.DataFrame(columns=columns)
|
|
@@ -1409,14 +1382,14 @@ class Plan(object):
|
|
|
1409
1382
|
progcall.start()
|
|
1410
1383
|
|
|
1411
1384
|
for year in range(ystart, yend + 1):
|
|
1412
|
-
self.setRates(
|
|
1385
|
+
self.setRates("historical", year)
|
|
1413
1386
|
self.solve(objective, options)
|
|
1414
1387
|
if not verbose:
|
|
1415
|
-
progcall.show((year - ystart + 1)/N)
|
|
1416
|
-
if self.caseStatus ==
|
|
1417
|
-
if objective ==
|
|
1388
|
+
progcall.show((year - ystart + 1) / N)
|
|
1389
|
+
if self.caseStatus == "solved":
|
|
1390
|
+
if objective == "maxSpending":
|
|
1418
1391
|
df.loc[len(df)] = [self.partialBequest, self.basis]
|
|
1419
|
-
elif objective ==
|
|
1392
|
+
elif objective == "maxBequest":
|
|
1420
1393
|
df.loc[len(df)] = [self.partialBequest, self.bequest]
|
|
1421
1394
|
|
|
1422
1395
|
progcall.finish()
|
|
@@ -1434,26 +1407,26 @@ class Plan(object):
|
|
|
1434
1407
|
"""
|
|
1435
1408
|
Run Monte Carlo simulations on plan.
|
|
1436
1409
|
"""
|
|
1437
|
-
if self.rateMethod not in [
|
|
1438
|
-
self.mylog.print(
|
|
1410
|
+
if self.rateMethod not in ["stochastic", "histochastic"]:
|
|
1411
|
+
self.mylog.print("It is pointless to run Monte Carlo simulations with fixed rates.")
|
|
1439
1412
|
return
|
|
1440
1413
|
|
|
1441
|
-
self.mylog.vprint(
|
|
1414
|
+
self.mylog.vprint(f"Running {N} Monte Carlo simulations.")
|
|
1442
1415
|
self.mylog.setVerbose(verbose)
|
|
1443
1416
|
|
|
1444
1417
|
# Turn off Medicare by default, unless specified in options.
|
|
1445
|
-
if
|
|
1418
|
+
if "withMedicare" not in options:
|
|
1446
1419
|
myoptions = dict(options)
|
|
1447
|
-
myoptions[
|
|
1420
|
+
myoptions["withMedicare"] = False
|
|
1448
1421
|
else:
|
|
1449
1422
|
myoptions = options
|
|
1450
1423
|
|
|
1451
|
-
if objective ==
|
|
1452
|
-
columns = [
|
|
1453
|
-
elif objective ==
|
|
1454
|
-
columns = [
|
|
1424
|
+
if objective == "maxSpending":
|
|
1425
|
+
columns = ["partial", objective]
|
|
1426
|
+
elif objective == "maxBequest":
|
|
1427
|
+
columns = ["partial", "final"]
|
|
1455
1428
|
else:
|
|
1456
|
-
self.mylog.print(
|
|
1429
|
+
self.mylog.print(f"Invalid objective {objective}.")
|
|
1457
1430
|
return None
|
|
1458
1431
|
|
|
1459
1432
|
df = pd.DataFrame(columns=columns)
|
|
@@ -1468,11 +1441,11 @@ class Plan(object):
|
|
|
1468
1441
|
self.regenRates()
|
|
1469
1442
|
self.solve(objective, myoptions)
|
|
1470
1443
|
if not verbose:
|
|
1471
|
-
progcall.show((n+1)/N)
|
|
1472
|
-
if self.caseStatus ==
|
|
1473
|
-
if objective ==
|
|
1444
|
+
progcall.show((n + 1) / N)
|
|
1445
|
+
if self.caseStatus == "solved":
|
|
1446
|
+
if objective == "maxSpending":
|
|
1474
1447
|
df.loc[len(df)] = [self.partialBequest, self.basis]
|
|
1475
|
-
elif objective ==
|
|
1448
|
+
elif objective == "maxBequest":
|
|
1476
1449
|
df.loc[len(df)] = [self.partialBequest, self.bequest]
|
|
1477
1450
|
|
|
1478
1451
|
progcall.finish()
|
|
@@ -1493,8 +1466,9 @@ class Plan(object):
|
|
|
1493
1466
|
|
|
1494
1467
|
description = io.StringIO()
|
|
1495
1468
|
|
|
1496
|
-
|
|
1497
|
-
|
|
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}"
|
|
1498
1472
|
means = df.mean(axis=0, numeric_only=True)
|
|
1499
1473
|
medians = df.median(axis=0, numeric_only=True)
|
|
1500
1474
|
|
|
@@ -1506,51 +1480,51 @@ class Plan(object):
|
|
|
1506
1480
|
# or if solution led to empty accounts at the end of first spouse's life.
|
|
1507
1481
|
if np.all(self.phi_j == 1) or medians.iloc[0] < 1:
|
|
1508
1482
|
if medians.iloc[0] < 1:
|
|
1509
|
-
print(
|
|
1510
|
-
df.drop(
|
|
1483
|
+
print(f"Optimized solutions all have null partial bequest in year {my[0]}.", file=description)
|
|
1484
|
+
df.drop("partial", axis=1, inplace=True)
|
|
1511
1485
|
means = df.mean(axis=0, numeric_only=True)
|
|
1512
1486
|
medians = df.median(axis=0, numeric_only=True)
|
|
1513
1487
|
|
|
1514
1488
|
df /= 1000
|
|
1515
1489
|
if len(df) > 0:
|
|
1516
|
-
|
|
1490
|
+
thisyear = self.year_n[0]
|
|
1491
|
+
if objective == "maxBequest":
|
|
1517
1492
|
fig, axes = plt.subplots()
|
|
1518
1493
|
# Show both partial and final bequests in the same histogram.
|
|
1519
|
-
sbn.histplot(df, multiple=
|
|
1494
|
+
sbn.histplot(df, multiple="dodge", kde=True, ax=axes)
|
|
1520
1495
|
legend = []
|
|
1521
1496
|
# Don't know why but legend is reversed from df.
|
|
1522
1497
|
for q in range(len(means) - 1, -1, -1):
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
)
|
|
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}")
|
|
1527
1501
|
plt.legend(legend, shadow=True)
|
|
1528
|
-
plt.xlabel(
|
|
1502
|
+
plt.xlabel(f"{thisyear} $k")
|
|
1529
1503
|
plt.title(objective)
|
|
1530
|
-
leads = [
|
|
1504
|
+
leads = [f"partial {my[0]}", f" final {my[1]}"]
|
|
1531
1505
|
elif len(means) == 2:
|
|
1532
1506
|
# Show partial bequest and net spending as two separate histograms.
|
|
1533
1507
|
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
|
|
1534
|
-
cols = [
|
|
1535
|
-
leads = [
|
|
1508
|
+
cols = ["partial", objective]
|
|
1509
|
+
leads = [f"partial {my[0]}", objective]
|
|
1536
1510
|
for q in range(2):
|
|
1537
1511
|
sbn.histplot(df[cols[q]], kde=True, ax=axes[q])
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
]
|
|
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}"]
|
|
1541
1515
|
axes[q].set_label(legend)
|
|
1542
1516
|
axes[q].legend(labels=legend)
|
|
1543
1517
|
axes[q].set_title(leads[q])
|
|
1544
|
-
axes[q].set_xlabel(
|
|
1518
|
+
axes[q].set_xlabel(f"{thisyear} $k")
|
|
1545
1519
|
else:
|
|
1546
1520
|
# Show net spending as single histogram.
|
|
1547
1521
|
fig, axes = plt.subplots()
|
|
1548
1522
|
sbn.histplot(df[objective], kde=True, ax=axes)
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
]
|
|
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}"]
|
|
1552
1526
|
plt.legend(legend, shadow=True)
|
|
1553
|
-
plt.xlabel(
|
|
1527
|
+
plt.xlabel(f"{thisyear} $k")
|
|
1554
1528
|
plt.title(objective)
|
|
1555
1529
|
leads = [objective]
|
|
1556
1530
|
|
|
@@ -1558,14 +1532,13 @@ class Plan(object):
|
|
|
1558
1532
|
# plt.show()
|
|
1559
1533
|
|
|
1560
1534
|
for q in range(len(means)):
|
|
1561
|
-
print(
|
|
1562
|
-
print(
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
file=description)
|
|
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)
|
|
1567
1540
|
nzeros = len(df.iloc[:, q][df.iloc[:, q] < 0.001])
|
|
1568
|
-
print(
|
|
1541
|
+
print(f"{leads[q]:>12}: N zero solns: {nzeros}", file=description)
|
|
1569
1542
|
|
|
1570
1543
|
return fig, description
|
|
1571
1544
|
|
|
@@ -1597,24 +1570,24 @@ class Plan(object):
|
|
|
1597
1570
|
Refer to companion document for implementation details.
|
|
1598
1571
|
"""
|
|
1599
1572
|
if self.rateMethod is None:
|
|
1600
|
-
raise RuntimeError(
|
|
1573
|
+
raise RuntimeError("Rate method must be selected before solving.")
|
|
1601
1574
|
|
|
1602
1575
|
# Assume unsuccessful until problem solved.
|
|
1603
|
-
self.caseStatus =
|
|
1576
|
+
self.caseStatus = "unsuccessful"
|
|
1604
1577
|
|
|
1605
1578
|
# Check objective and required options.
|
|
1606
|
-
knownObjectives = [
|
|
1607
|
-
knownSolvers = [
|
|
1579
|
+
knownObjectives = ["maxBequest", "maxSpending"]
|
|
1580
|
+
knownSolvers = ["HiGHS", "MOSEK"]
|
|
1608
1581
|
knownOptions = [
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1582
|
+
"units",
|
|
1583
|
+
"maxRothConversion",
|
|
1584
|
+
"netSpending",
|
|
1585
|
+
"bequest",
|
|
1586
|
+
"bigM",
|
|
1587
|
+
"noRothConversions",
|
|
1588
|
+
"withMedicare",
|
|
1589
|
+
"solver",
|
|
1590
|
+
"previousMAGIs",
|
|
1618
1591
|
]
|
|
1619
1592
|
# We will modify options if required.
|
|
1620
1593
|
if options is None:
|
|
@@ -1624,48 +1597,48 @@ class Plan(object):
|
|
|
1624
1597
|
|
|
1625
1598
|
for opt in myoptions:
|
|
1626
1599
|
if opt not in knownOptions:
|
|
1627
|
-
raise ValueError(
|
|
1600
|
+
raise ValueError(f"Option {opt} is not one of {knownOptions}.")
|
|
1628
1601
|
|
|
1629
1602
|
if objective not in knownObjectives:
|
|
1630
|
-
raise ValueError(
|
|
1603
|
+
raise ValueError(f"Objective {objective} is not one of {knownObjectives}.")
|
|
1631
1604
|
|
|
1632
|
-
if objective ==
|
|
1633
|
-
raise RuntimeError(
|
|
1605
|
+
if objective == "maxBequest" and "netSpending" not in myoptions:
|
|
1606
|
+
raise RuntimeError(f"Objective {objective} needs netSpending option.")
|
|
1634
1607
|
|
|
1635
|
-
if objective ==
|
|
1636
|
-
self.mylog.vprint(
|
|
1637
|
-
myoptions.pop(
|
|
1608
|
+
if objective == "maxBequest" and "bequest" in myoptions:
|
|
1609
|
+
self.mylog.vprint("Ignoring bequest option provided.")
|
|
1610
|
+
myoptions.pop("bequest")
|
|
1638
1611
|
|
|
1639
|
-
if objective ==
|
|
1640
|
-
self.mylog.vprint(
|
|
1641
|
-
myoptions.pop(
|
|
1612
|
+
if objective == "maxSpending" and "netSpending" in myoptions:
|
|
1613
|
+
self.mylog.vprint("Ignoring netSpending option provided.")
|
|
1614
|
+
myoptions.pop("netSpending")
|
|
1642
1615
|
|
|
1643
|
-
if objective ==
|
|
1644
|
-
self.mylog.vprint(
|
|
1616
|
+
if objective == "maxSpending" and "bequest" not in myoptions:
|
|
1617
|
+
self.mylog.vprint("Using bequest of $1.")
|
|
1645
1618
|
|
|
1646
|
-
if
|
|
1647
|
-
magi = myoptions[
|
|
1619
|
+
if "previousMAGIs" in myoptions:
|
|
1620
|
+
magi = myoptions["previousMAGIs"]
|
|
1648
1621
|
if len(magi) != 2:
|
|
1649
1622
|
raise ValueError("previousMAGIs must have two values.")
|
|
1650
1623
|
|
|
1651
|
-
if
|
|
1652
|
-
units = u.getUnits(options[
|
|
1624
|
+
if "units" in options:
|
|
1625
|
+
units = u.getUnits(options["units"])
|
|
1653
1626
|
else:
|
|
1654
1627
|
units = 1000
|
|
1655
1628
|
self.prevMAGI = units * np.array(magi)
|
|
1656
1629
|
|
|
1657
1630
|
self._adjustParameters()
|
|
1658
1631
|
|
|
1659
|
-
if
|
|
1660
|
-
solver = myoptions[
|
|
1632
|
+
if "solver" in options:
|
|
1633
|
+
solver = myoptions["solver"]
|
|
1661
1634
|
if solver not in knownSolvers:
|
|
1662
|
-
raise ValueError(
|
|
1635
|
+
raise ValueError(f"Unknown solver {solver}.")
|
|
1663
1636
|
else:
|
|
1664
1637
|
solver = self.defaultSolver
|
|
1665
1638
|
|
|
1666
|
-
if solver ==
|
|
1639
|
+
if solver == "HiGHS":
|
|
1667
1640
|
self._milpSolve(objective, myoptions)
|
|
1668
|
-
elif solver ==
|
|
1641
|
+
elif solver == "MOSEK":
|
|
1669
1642
|
self._mosekSolve(objective, myoptions)
|
|
1670
1643
|
|
|
1671
1644
|
self.objective = objective
|
|
@@ -1680,16 +1653,16 @@ class Plan(object):
|
|
|
1680
1653
|
from scipy import optimize
|
|
1681
1654
|
|
|
1682
1655
|
withMedicare = True
|
|
1683
|
-
if
|
|
1656
|
+
if "withMedicare" in options and options["withMedicare"] is False:
|
|
1684
1657
|
withMedicare = False
|
|
1685
1658
|
|
|
1686
|
-
if objective ==
|
|
1659
|
+
if objective == "maxSpending":
|
|
1687
1660
|
objFac = -1 / self.xi_n[0]
|
|
1688
1661
|
else:
|
|
1689
1662
|
objFac = -1 / self.gamma_n[-1]
|
|
1690
1663
|
|
|
1691
1664
|
# mip_rel_gap smaller than 1e-6 can lead to oscillatory solutions.
|
|
1692
|
-
milpOptions = {
|
|
1665
|
+
milpOptions = {"disp": False, "mip_rel_gap": 1e-6}
|
|
1693
1666
|
|
|
1694
1667
|
it = 0
|
|
1695
1668
|
absdiff = np.inf
|
|
@@ -1722,38 +1695,38 @@ class Plan(object):
|
|
|
1722
1695
|
|
|
1723
1696
|
self._estimateMedicare(solution.x)
|
|
1724
1697
|
|
|
1725
|
-
self.mylog.vprint(
|
|
1698
|
+
self.mylog.vprint(f"Iteration: {it} objective: {u.d(solution.fun * objFac, f=2)}")
|
|
1726
1699
|
|
|
1727
1700
|
delta = solution.x - old_x
|
|
1728
1701
|
absdiff = np.sum(np.abs(delta), axis=0)
|
|
1729
1702
|
if absdiff < 1:
|
|
1730
|
-
self.mylog.vprint(
|
|
1703
|
+
self.mylog.vprint("Converged on full solution.")
|
|
1731
1704
|
break
|
|
1732
1705
|
|
|
1733
1706
|
# Avoid oscillatory solutions. Look only at most recent solutions. Within $10.
|
|
1734
|
-
isclosenough = abs(-solution.fun - min(old_solutions[int(it / 2):])) < 10 * self.xi_n[0]
|
|
1707
|
+
isclosenough = abs(-solution.fun - min(old_solutions[int(it / 2) :])) < 10 * self.xi_n[0]
|
|
1735
1708
|
if isclosenough:
|
|
1736
|
-
self.mylog.vprint(
|
|
1709
|
+
self.mylog.vprint("Converged through selecting minimum oscillating objective.")
|
|
1737
1710
|
break
|
|
1738
1711
|
|
|
1739
1712
|
if it > 59:
|
|
1740
|
-
self.mylog.vprint(
|
|
1713
|
+
self.mylog.vprint("WARNING: Exiting loop on maximum iterations.")
|
|
1741
1714
|
break
|
|
1742
1715
|
|
|
1743
1716
|
old_solutions.append(-solution.fun)
|
|
1744
1717
|
old_x = solution.x
|
|
1745
1718
|
|
|
1746
1719
|
if solution.success:
|
|
1747
|
-
self.mylog.vprint(
|
|
1720
|
+
self.mylog.vprint(f"Self-consistent Medicare loop returned after {it} iterations.")
|
|
1748
1721
|
self.mylog.vprint(solution.message)
|
|
1749
|
-
self.mylog.vprint(
|
|
1722
|
+
self.mylog.vprint(f"Objective: {u.d(solution.fun * objFac)}")
|
|
1750
1723
|
# self.mylog.vprint('Upper bound:', u.d(-solution.mip_dual_bound))
|
|
1751
1724
|
self._aggregateResults(solution.x)
|
|
1752
|
-
self._timestamp = datetime.now().strftime(
|
|
1753
|
-
self.caseStatus =
|
|
1725
|
+
self._timestamp = datetime.now().strftime("%Y-%m-%d at %H:%M:%S")
|
|
1726
|
+
self.caseStatus = "solved"
|
|
1754
1727
|
else:
|
|
1755
|
-
self.mylog.vprint(
|
|
1756
|
-
self.caseStatus =
|
|
1728
|
+
self.mylog.vprint("WARNING: Optimization failed:", solution.message, solution.success)
|
|
1729
|
+
self.caseStatus = "unsuccessful"
|
|
1757
1730
|
|
|
1758
1731
|
return None
|
|
1759
1732
|
|
|
@@ -1764,10 +1737,10 @@ class Plan(object):
|
|
|
1764
1737
|
import mosek
|
|
1765
1738
|
|
|
1766
1739
|
withMedicare = True
|
|
1767
|
-
if
|
|
1740
|
+
if "withMedicare" in options and options["withMedicare"] is False:
|
|
1768
1741
|
withMedicare = False
|
|
1769
1742
|
|
|
1770
|
-
if objective ==
|
|
1743
|
+
if objective == "maxSpending":
|
|
1771
1744
|
objFac = -1 / self.xi_n[0]
|
|
1772
1745
|
else:
|
|
1773
1746
|
objFac = -1 / self.gamma_n[-1]
|
|
@@ -1775,11 +1748,11 @@ class Plan(object):
|
|
|
1775
1748
|
# mip_rel_gap smaller than 1e-6 can lead to oscillatory solutions.
|
|
1776
1749
|
|
|
1777
1750
|
bdic = {
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1751
|
+
"fx": mosek.boundkey.fx,
|
|
1752
|
+
"fr": mosek.boundkey.fr,
|
|
1753
|
+
"lo": mosek.boundkey.lo,
|
|
1754
|
+
"ra": mosek.boundkey.ra,
|
|
1755
|
+
"up": mosek.boundkey.up,
|
|
1783
1756
|
}
|
|
1784
1757
|
|
|
1785
1758
|
it = 0
|
|
@@ -1834,22 +1807,22 @@ class Plan(object):
|
|
|
1834
1807
|
|
|
1835
1808
|
self._estimateMedicare(xx)
|
|
1836
1809
|
|
|
1837
|
-
self.mylog.vprint(
|
|
1810
|
+
self.mylog.vprint("Iteration:", it, "objective:", u.d(solution * objFac, f=2))
|
|
1838
1811
|
|
|
1839
1812
|
delta = xx - old_x
|
|
1840
1813
|
absdiff = np.sum(np.abs(delta), axis=0)
|
|
1841
1814
|
if absdiff < 1:
|
|
1842
|
-
self.mylog.vprint(
|
|
1815
|
+
self.mylog.vprint("Converged on full solution.")
|
|
1843
1816
|
break
|
|
1844
1817
|
|
|
1845
1818
|
# Avoid oscillatory solutions. Look only at most recent solutions. Within $10.
|
|
1846
|
-
isclosenough = abs(-solution - min(old_solutions[int(it / 2):])) < 10 * self.xi_n[0]
|
|
1819
|
+
isclosenough = abs(-solution - min(old_solutions[int(it / 2) :])) < 10 * self.xi_n[0]
|
|
1847
1820
|
if isclosenough:
|
|
1848
|
-
self.mylog.vprint(
|
|
1821
|
+
self.mylog.vprint("Converged through selecting minimum oscillating objective.")
|
|
1849
1822
|
break
|
|
1850
1823
|
|
|
1851
1824
|
if it > 59:
|
|
1852
|
-
self.mylog.vprint(
|
|
1825
|
+
self.mylog.vprint("WARNING: Exiting loop on maximum iterations.")
|
|
1853
1826
|
break
|
|
1854
1827
|
|
|
1855
1828
|
old_solutions.append(-solution)
|
|
@@ -1858,17 +1831,17 @@ class Plan(object):
|
|
|
1858
1831
|
task.set_Stream(mosek.streamtype.wrn, _streamPrinter)
|
|
1859
1832
|
# task.writedata(self._name+'.ptf')
|
|
1860
1833
|
if solsta == mosek.solsta.integer_optimal:
|
|
1861
|
-
self.mylog.vprint(
|
|
1834
|
+
self.mylog.vprint(f"Self-consistent Medicare loop returned after {it} iterations.")
|
|
1862
1835
|
task.solutionsummary(mosek.streamtype.msg)
|
|
1863
|
-
self.mylog.vprint(
|
|
1864
|
-
self.caseStatus =
|
|
1836
|
+
self.mylog.vprint("Objective:", u.d(solution * objFac))
|
|
1837
|
+
self.caseStatus = "solved"
|
|
1865
1838
|
# self.mylog.vprint('Upper bound:', u.d(-solution.mip_dual_bound))
|
|
1866
1839
|
self._aggregateResults(xx)
|
|
1867
|
-
self._timestamp = datetime.now().strftime(
|
|
1840
|
+
self._timestamp = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
|
|
1868
1841
|
else:
|
|
1869
|
-
self.mylog.vprint(
|
|
1842
|
+
self.mylog.vprint("WARNING: Optimization failed:", "Infeasible or unbounded.")
|
|
1870
1843
|
task.solutionsummary(mosek.streamtype.msg)
|
|
1871
|
-
self.caseStatus =
|
|
1844
|
+
self.caseStatus = "unsuccessful"
|
|
1872
1845
|
|
|
1873
1846
|
return None
|
|
1874
1847
|
|
|
@@ -1883,9 +1856,9 @@ class Plan(object):
|
|
|
1883
1856
|
if x is None:
|
|
1884
1857
|
MAGI_n = np.zeros(self.N_n)
|
|
1885
1858
|
else:
|
|
1886
|
-
self.F_tn = np.array(x[self.C[
|
|
1859
|
+
self.F_tn = np.array(x[self.C["F"] : self.C["g"]])
|
|
1887
1860
|
self.F_tn = self.F_tn.reshape((self.N_t, self.N_n))
|
|
1888
|
-
MAGI_n = np.sum(self.F_tn, axis=0) + np.array(x[self.C[
|
|
1861
|
+
MAGI_n = np.sum(self.F_tn, axis=0) + np.array(x[self.C["e"] : self.C["F"]])
|
|
1889
1862
|
|
|
1890
1863
|
self.M_n = tx.mediCosts(self.yobs, self.horizons, MAGI_n, self.prevMAGI, self.gamma_n[:-1], self.N_n)
|
|
1891
1864
|
|
|
@@ -1905,15 +1878,15 @@ class Plan(object):
|
|
|
1905
1878
|
# Nz = self.N_z
|
|
1906
1879
|
n_d = self.n_d
|
|
1907
1880
|
|
|
1908
|
-
Cb = self.C[
|
|
1909
|
-
Cd = self.C[
|
|
1910
|
-
Ce = self.C[
|
|
1911
|
-
CF = self.C[
|
|
1912
|
-
Cg = self.C[
|
|
1913
|
-
Cs = self.C[
|
|
1914
|
-
Cw = self.C[
|
|
1915
|
-
Cx = self.C[
|
|
1916
|
-
Cz = self.C[
|
|
1881
|
+
Cb = self.C["b"]
|
|
1882
|
+
Cd = self.C["d"]
|
|
1883
|
+
Ce = self.C["e"]
|
|
1884
|
+
CF = self.C["F"]
|
|
1885
|
+
Cg = self.C["g"]
|
|
1886
|
+
Cs = self.C["s"]
|
|
1887
|
+
Cw = self.C["w"]
|
|
1888
|
+
Cx = self.C["x"]
|
|
1889
|
+
Cz = self.C["z"]
|
|
1917
1890
|
|
|
1918
1891
|
x = u.roundCents(x)
|
|
1919
1892
|
|
|
@@ -2006,20 +1979,20 @@ class Plan(object):
|
|
|
2006
1979
|
]
|
|
2007
1980
|
"""
|
|
2008
1981
|
sources = {}
|
|
2009
|
-
sources[
|
|
2010
|
-
sources[
|
|
2011
|
-
sources[
|
|
2012
|
-
sources[
|
|
2013
|
-
sources[
|
|
2014
|
-
sources[
|
|
2015
|
-
sources[
|
|
2016
|
-
sources[
|
|
2017
|
-
sources[
|
|
1982
|
+
sources["wages"] = self.omega_in
|
|
1983
|
+
sources["ssec"] = self.zetaBar_in
|
|
1984
|
+
sources["pension"] = self.piBar_in
|
|
1985
|
+
sources["txbl acc wdrwl"] = self.w_ijn[:, 0, :]
|
|
1986
|
+
sources["RMD"] = self.rmd_in
|
|
1987
|
+
sources["+dist"] = self.dist_in
|
|
1988
|
+
sources["RothX"] = self.x_in
|
|
1989
|
+
sources["tax-free wdrwl"] = self.w_ijn[:, 2, :]
|
|
1990
|
+
sources["BTI"] = self.Lambda_in
|
|
2018
1991
|
|
|
2019
1992
|
savings = {}
|
|
2020
|
-
savings[
|
|
2021
|
-
savings[
|
|
2022
|
-
savings[
|
|
1993
|
+
savings["taxable"] = self.b_ijn[:, 0, :]
|
|
1994
|
+
savings["tax-deferred"] = self.b_ijn[:, 1, :]
|
|
1995
|
+
savings["tax-free"] = self.b_ijn[:, 2, :]
|
|
2023
1996
|
|
|
2024
1997
|
self.sources_in = sources
|
|
2025
1998
|
self.savings_in = savings
|
|
@@ -2039,7 +2012,7 @@ class Plan(object):
|
|
|
2039
2012
|
"""
|
|
2040
2013
|
_estate = np.sum(self.b_ijn[:, :, :, self.N_n], axis=(0, 2))
|
|
2041
2014
|
_estate[1] *= 1 - self.nu
|
|
2042
|
-
self.mylog.vprint(
|
|
2015
|
+
self.mylog.vprint(f"Estate value of {u.d(sum(_estate))} at the end of year {self.year_n[-1]}.")
|
|
2043
2016
|
|
|
2044
2017
|
return None
|
|
2045
2018
|
|
|
@@ -2048,11 +2021,11 @@ class Plan(object):
|
|
|
2048
2021
|
"""
|
|
2049
2022
|
Print summary in logs.
|
|
2050
2023
|
"""
|
|
2051
|
-
self.mylog.print(
|
|
2024
|
+
self.mylog.print("SUMMARY ================================================================")
|
|
2052
2025
|
dic = self.summaryDic()
|
|
2053
2026
|
for key, value in dic.items():
|
|
2054
2027
|
self.mylog.print(f"{key}: {value}")
|
|
2055
|
-
self.mylog.print(
|
|
2028
|
+
self.mylog.print("------------------------------------------------------------------------")
|
|
2056
2029
|
|
|
2057
2030
|
return None
|
|
2058
2031
|
|
|
@@ -2071,11 +2044,10 @@ class Plan(object):
|
|
|
2071
2044
|
"""
|
|
2072
2045
|
Return summary as a string.
|
|
2073
2046
|
"""
|
|
2074
|
-
string =
|
|
2047
|
+
string = "Synopsis\n"
|
|
2075
2048
|
dic = self.summaryDic()
|
|
2076
2049
|
for key, value in dic.items():
|
|
2077
2050
|
string += f"{key:>70}: {value}\n"
|
|
2078
|
-
# string += "%60s: %s\n" % (key, value)
|
|
2079
2051
|
|
|
2080
2052
|
return string
|
|
2081
2053
|
|
|
@@ -2086,86 +2058,69 @@ class Plan(object):
|
|
|
2086
2058
|
now = self.year_n[0]
|
|
2087
2059
|
dic = {}
|
|
2088
2060
|
# Results
|
|
2089
|
-
dic[f"Net yearly spending basis in {now}$"] =
|
|
2090
|
-
dic[f"Net yearly spending for year {now}"] =
|
|
2061
|
+
dic[f"Net yearly spending basis in {now}$"] = u.d(self.g_n[0] / self.xi_n[0])
|
|
2062
|
+
dic[f"Net yearly spending for year {now}"] = u.d(self.g_n[0] / self.yearFracLeft)
|
|
2091
2063
|
dic[f"Net spending remaining in year {now}"] = u.d(self.g_n[0])
|
|
2092
2064
|
|
|
2093
2065
|
totIncome = np.sum(self.g_n, axis=0)
|
|
2094
2066
|
totIncomeNow = np.sum(self.g_n / self.gamma_n[:-1], axis=0)
|
|
2095
|
-
dic[f"Total net spending in {now}$"] = (
|
|
2096
|
-
"%s (%s nominal)" % (u.d(totIncomeNow), u.d(totIncome))
|
|
2097
|
-
)
|
|
2067
|
+
dic[f"Total net spending in {now}$"] = f"{u.d(totIncomeNow)} ({u.d(totIncome)} nominal)"
|
|
2098
2068
|
|
|
2099
2069
|
totRoth = np.sum(self.x_in, axis=(0, 1))
|
|
2100
2070
|
totRothNow = np.sum(np.sum(self.x_in, axis=0) / self.gamma_n[:-1], axis=0)
|
|
2101
|
-
dic[f"Total Roth conversions in {now}$"] = (
|
|
2102
|
-
"%s (%s nominal)" % (u.d(totRothNow), u.d(totRoth))
|
|
2103
|
-
)
|
|
2071
|
+
dic[f"Total Roth conversions in {now}$"] = f"{u.d(totRothNow)} ({u.d(totRoth)} nominal)"
|
|
2104
2072
|
|
|
2105
2073
|
taxPaid = np.sum(self.T_n, axis=0)
|
|
2106
2074
|
taxPaidNow = np.sum(self.T_n / self.gamma_n[:-1], axis=0)
|
|
2107
|
-
dic[f"Total income tax paid on ordinary income in {now}$"] = (
|
|
2108
|
-
"%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
|
|
2109
|
-
)
|
|
2075
|
+
dic[f"Total income tax paid on ordinary income in {now}$"] = f"{u.d(taxPaidNow)} ({u.d(taxPaid)} nominal)"
|
|
2110
2076
|
|
|
2111
2077
|
taxPaid = np.sum(self.U_n, axis=0)
|
|
2112
2078
|
taxPaidNow = np.sum(self.U_n / self.gamma_n[:-1], axis=0)
|
|
2113
|
-
dic[f"Total tax paid on gains and dividends in {now}$"] = (
|
|
2114
|
-
"%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
|
|
2115
|
-
)
|
|
2079
|
+
dic[f"Total tax paid on gains and dividends in {now}$"] = f"{u.d(taxPaidNow)} ({u.d(taxPaid)} nominal)"
|
|
2116
2080
|
|
|
2117
2081
|
taxPaid = np.sum(self.M_n, axis=0)
|
|
2118
2082
|
taxPaidNow = np.sum(self.M_n / self.gamma_n[:-1], axis=0)
|
|
2119
|
-
dic[f"Total Medicare premiums paid in {now}$"] = (
|
|
2120
|
-
"%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
|
|
2121
|
-
)
|
|
2083
|
+
dic[f"Total Medicare premiums paid in {now}$"] = f"{u.d(taxPaidNow)} ({u.d(taxPaid)} nominal)"
|
|
2122
2084
|
|
|
2123
2085
|
if self.N_i == 2 and self.n_d < self.N_n:
|
|
2124
2086
|
p_j = self.partialEstate_j * (1 - self.phi_j)
|
|
2125
2087
|
p_j[1] *= 1 - self.nu
|
|
2126
2088
|
nx = self.n_d - 1
|
|
2089
|
+
ynx = self.year_n[nx]
|
|
2127
2090
|
totOthers = np.sum(p_j)
|
|
2128
2091
|
totOthersNow = totOthers / self.gamma_n[nx + 1]
|
|
2129
2092
|
q_j = self.partialEstate_j * self.phi_j
|
|
2130
2093
|
totSpousal = np.sum(q_j)
|
|
2131
2094
|
totSpousalNow = totSpousal / self.gamma_n[nx + 1]
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
dic["Sum of spousal bequests to
|
|
2138
|
-
(
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
"
|
|
2144
|
-
|
|
2145
|
-
dic["Sum of post-tax non-spousal bequests from %s in year %d in %d$" %
|
|
2146
|
-
(self.inames[self.i_d], self.year_n[nx], now)] = (
|
|
2147
|
-
"%s (%s nominal)" % (u.d(totOthersNow), u.d(totOthers))
|
|
2148
|
-
)
|
|
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])}")
|
|
2099
|
+
|
|
2100
|
+
dic[f"Sum of spousal bequests to {iname_s} in year {ynx} in {now}$"] = (
|
|
2101
|
+
f"{u.d(totSpousalNow)} ({u.d(totSpousal)} nominal)")
|
|
2102
|
+
dic[
|
|
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])}")
|
|
2105
|
+
dic[
|
|
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"] =
|
|
2163
|
-
"%.2f" % (self.gamma_n[-1])
|
|
2164
|
-
)
|
|
2120
|
+
dic["Cumulative inflation factor from start date to end of plan"] = f"{self.gamma_n[-1]:.2f}"
|
|
2165
2121
|
for i in range(self.N_i):
|
|
2166
|
-
dic["
|
|
2167
|
-
"
|
|
2168
|
-
)
|
|
2122
|
+
dic[f"{self.inames[i]:>12}'s {self.horizons[i]:02}-year life horizon"] = (
|
|
2123
|
+
f"{now} -> {now + self.horizons[i] - 1}")
|
|
2169
2124
|
|
|
2170
2125
|
dic["Plan name"] = self._name
|
|
2171
2126
|
dic["Number of decision variables"] = str(self.A.nvars)
|
|
@@ -2174,7 +2129,7 @@ class Plan(object):
|
|
|
2174
2129
|
|
|
2175
2130
|
return dic
|
|
2176
2131
|
|
|
2177
|
-
def showRatesCorrelations(self, tag=
|
|
2132
|
+
def showRatesCorrelations(self, tag="", shareRange=False, figure=False):
|
|
2178
2133
|
"""
|
|
2179
2134
|
Plot correlations between various rates.
|
|
2180
2135
|
|
|
@@ -2182,15 +2137,15 @@ class Plan(object):
|
|
|
2182
2137
|
"""
|
|
2183
2138
|
import seaborn as sbn
|
|
2184
2139
|
|
|
2185
|
-
if self.rateMethod in [None,
|
|
2186
|
-
self.mylog.vprint(
|
|
2140
|
+
if self.rateMethod in [None, "user", "historical average", "conservative"]:
|
|
2141
|
+
self.mylog.vprint(f"Warning: Cannot plot correlations for {self.rateMethod} rate method.")
|
|
2187
2142
|
return None
|
|
2188
2143
|
|
|
2189
2144
|
rateNames = [
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2145
|
+
"S&P500 (incl. div.)",
|
|
2146
|
+
"Baa Corp. Bonds",
|
|
2147
|
+
"10-y T-Notes",
|
|
2148
|
+
"Inflation",
|
|
2194
2149
|
]
|
|
2195
2150
|
|
|
2196
2151
|
df = pd.DataFrame()
|
|
@@ -2206,25 +2161,25 @@ class Plan(object):
|
|
|
2206
2161
|
g.map_upper(sbn.scatterplot)
|
|
2207
2162
|
g.map_lower(sbn.kdeplot)
|
|
2208
2163
|
# g.map_diag(sbn.kdeplot)
|
|
2209
|
-
g.map_diag(sbn.histplot, color=
|
|
2164
|
+
g.map_diag(sbn.histplot, color="orange")
|
|
2210
2165
|
|
|
2211
2166
|
# Put zero axes on off-diagonal plots.
|
|
2212
2167
|
imod = len(rateNames) + 1
|
|
2213
2168
|
for i, ax in enumerate(g.axes.flat):
|
|
2214
|
-
ax.axvline(x=0, color=
|
|
2169
|
+
ax.axvline(x=0, color="grey", linewidth=1, linestyle=":")
|
|
2215
2170
|
if i % imod != 0:
|
|
2216
|
-
ax.axhline(y=0, color=
|
|
2171
|
+
ax.axhline(y=0, color="grey", linewidth=1, linestyle=":")
|
|
2217
2172
|
# ax.tick_params(axis='both', labelleft=True, labelbottom=True)
|
|
2218
2173
|
|
|
2219
2174
|
# plt.subplots_adjust(wspace=0.3, hspace=0.3)
|
|
2220
2175
|
|
|
2221
|
-
title = self._name +
|
|
2222
|
-
title +=
|
|
2223
|
-
if self.rateMethod in [
|
|
2224
|
-
title +=
|
|
2176
|
+
title = self._name + "\n"
|
|
2177
|
+
title += f"Rates Correlations (N={self.N_n}) {self.rateMethod}"
|
|
2178
|
+
if self.rateMethod in ["historical", "histochastic"]:
|
|
2179
|
+
title += " (" + str(self.rateFrm) + "-" + str(self.rateTo) + ")"
|
|
2225
2180
|
|
|
2226
|
-
if tag !=
|
|
2227
|
-
title +=
|
|
2181
|
+
if tag != "":
|
|
2182
|
+
title += " - " + tag
|
|
2228
2183
|
|
|
2229
2184
|
g.fig.suptitle(title, y=1.08)
|
|
2230
2185
|
|
|
@@ -2234,7 +2189,7 @@ class Plan(object):
|
|
|
2234
2189
|
plt.show()
|
|
2235
2190
|
return None
|
|
2236
2191
|
|
|
2237
|
-
def showRates(self, tag=
|
|
2192
|
+
def showRates(self, tag="", figure=False):
|
|
2238
2193
|
"""
|
|
2239
2194
|
Plot rate values used over the time horizon.
|
|
2240
2195
|
|
|
@@ -2243,26 +2198,26 @@ class Plan(object):
|
|
|
2243
2198
|
import matplotlib.ticker as tk
|
|
2244
2199
|
|
|
2245
2200
|
if self.rateMethod is None:
|
|
2246
|
-
self.mylog.vprint(
|
|
2201
|
+
self.mylog.vprint("Warning: Rate method must be selected before plotting.")
|
|
2247
2202
|
return None
|
|
2248
2203
|
|
|
2249
2204
|
fig, ax = plt.subplots(figsize=(6, 4))
|
|
2250
|
-
plt.grid(visible=
|
|
2251
|
-
title = self._name +
|
|
2252
|
-
if self.rateMethod in [
|
|
2253
|
-
title +=
|
|
2254
|
-
title +=
|
|
2205
|
+
plt.grid(visible="both")
|
|
2206
|
+
title = self._name + "\nReturn & Inflation Rates (" + str(self.rateMethod)
|
|
2207
|
+
if self.rateMethod in ["historical", "histochastic", "historical average"]:
|
|
2208
|
+
title += " " + str(self.rateFrm) + "-" + str(self.rateTo)
|
|
2209
|
+
title += ")"
|
|
2255
2210
|
|
|
2256
|
-
if tag !=
|
|
2257
|
-
title +=
|
|
2211
|
+
if tag != "":
|
|
2212
|
+
title += " - " + tag
|
|
2258
2213
|
|
|
2259
2214
|
rateName = [
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2215
|
+
"S&P500 (incl. div.)",
|
|
2216
|
+
"Baa Corp. Bonds",
|
|
2217
|
+
"10-y T-Notes",
|
|
2218
|
+
"Inflation",
|
|
2264
2219
|
]
|
|
2265
|
-
ltype = [
|
|
2220
|
+
ltype = ["-", "-.", ":", "--"]
|
|
2266
2221
|
for k in range(self.N_k):
|
|
2267
2222
|
if self.yearFracLeft == 1:
|
|
2268
2223
|
data = 100 * self.tau_kn[k]
|
|
@@ -2272,16 +2227,17 @@ class Plan(object):
|
|
|
2272
2227
|
years = self.year_n[1:]
|
|
2273
2228
|
|
|
2274
2229
|
# Use ddof=1 to match pandas.
|
|
2275
|
-
label = (
|
|
2276
|
-
|
|
2230
|
+
label = (
|
|
2231
|
+
rateName[k] + " <" + "{:.1f}".format(np.mean(data)) + " +/- {:.1f}".format(np.std(data, ddof=1)) + "%>"
|
|
2232
|
+
)
|
|
2277
2233
|
ax.plot(years, data, label=label, ls=ltype[k % self.N_k])
|
|
2278
2234
|
|
|
2279
2235
|
ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
|
|
2280
|
-
ax.legend(loc=
|
|
2236
|
+
ax.legend(loc="best", reverse=False, fontsize=8, framealpha=0.7)
|
|
2281
2237
|
# ax.legend(loc='upper left')
|
|
2282
2238
|
ax.set_title(title)
|
|
2283
|
-
ax.set_xlabel(
|
|
2284
|
-
ax.set_ylabel(
|
|
2239
|
+
ax.set_xlabel("year")
|
|
2240
|
+
ax.set_ylabel("%")
|
|
2285
2241
|
|
|
2286
2242
|
if figure:
|
|
2287
2243
|
return fig
|
|
@@ -2289,24 +2245,24 @@ class Plan(object):
|
|
|
2289
2245
|
plt.show()
|
|
2290
2246
|
return None
|
|
2291
2247
|
|
|
2292
|
-
def showProfile(self, tag=
|
|
2248
|
+
def showProfile(self, tag="", figure=False):
|
|
2293
2249
|
"""
|
|
2294
2250
|
Plot spending profile over time.
|
|
2295
2251
|
|
|
2296
2252
|
A tag string can be set to add information to the title of the plot.
|
|
2297
2253
|
"""
|
|
2298
2254
|
if self.xi_n is None:
|
|
2299
|
-
self.mylog.vprint(
|
|
2255
|
+
self.mylog.vprint("Warning: Profile must be selected before plotting.")
|
|
2300
2256
|
return None
|
|
2301
2257
|
|
|
2302
|
-
title = self._name +
|
|
2303
|
-
if tag !=
|
|
2304
|
-
title +=
|
|
2258
|
+
title = self._name + "\nSpending Profile"
|
|
2259
|
+
if tag != "":
|
|
2260
|
+
title += " - " + tag
|
|
2305
2261
|
|
|
2306
2262
|
# style = {'net': '-', 'target': ':'}
|
|
2307
|
-
style = {
|
|
2308
|
-
series = {
|
|
2309
|
-
fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat=
|
|
2263
|
+
style = {"profile": "-"}
|
|
2264
|
+
series = {"profile": self.xi_n}
|
|
2265
|
+
fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat="xi")
|
|
2310
2266
|
|
|
2311
2267
|
if figure:
|
|
2312
2268
|
return fig
|
|
@@ -2315,7 +2271,7 @@ class Plan(object):
|
|
|
2315
2271
|
return None
|
|
2316
2272
|
|
|
2317
2273
|
@_checkCaseStatus
|
|
2318
|
-
def showNetSpending(self, tag=
|
|
2274
|
+
def showNetSpending(self, tag="", value=None, figure=False):
|
|
2319
2275
|
"""
|
|
2320
2276
|
Plot net available spending and target over time.
|
|
2321
2277
|
|
|
@@ -2326,20 +2282,20 @@ class Plan(object):
|
|
|
2326
2282
|
"""
|
|
2327
2283
|
value = self._checkValue(value)
|
|
2328
2284
|
|
|
2329
|
-
title = self._name +
|
|
2330
|
-
if tag !=
|
|
2331
|
-
title +=
|
|
2285
|
+
title = self._name + "\nNet Available Spending"
|
|
2286
|
+
if tag != "":
|
|
2287
|
+
title += " - " + tag
|
|
2332
2288
|
|
|
2333
|
-
style = {
|
|
2334
|
-
if value ==
|
|
2335
|
-
series = {
|
|
2336
|
-
yformat =
|
|
2289
|
+
style = {"net": "-", "target": ":"}
|
|
2290
|
+
if value == "nominal":
|
|
2291
|
+
series = {"net": self.g_n, "target": (self.g_n[0] / self.xi_n[0]) * self.xiBar_n}
|
|
2292
|
+
yformat = "\\$k (nominal)"
|
|
2337
2293
|
else:
|
|
2338
2294
|
series = {
|
|
2339
|
-
|
|
2340
|
-
|
|
2295
|
+
"net": self.g_n / self.gamma_n[:-1],
|
|
2296
|
+
"target": (self.g_n[0] / self.xi_n[0]) * self.xi_n,
|
|
2341
2297
|
}
|
|
2342
|
-
yformat =
|
|
2298
|
+
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2343
2299
|
|
|
2344
2300
|
fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat)
|
|
2345
2301
|
|
|
@@ -2350,7 +2306,7 @@ class Plan(object):
|
|
|
2350
2306
|
return None
|
|
2351
2307
|
|
|
2352
2308
|
@_checkCaseStatus
|
|
2353
|
-
def showAssetDistribution(self, tag=
|
|
2309
|
+
def showAssetDistribution(self, tag="", value=None, figure=False):
|
|
2354
2310
|
"""
|
|
2355
2311
|
Plot the distribution of each savings account in thousands of dollars
|
|
2356
2312
|
during the simulation time. This function will generate three
|
|
@@ -2364,34 +2320,35 @@ class Plan(object):
|
|
|
2364
2320
|
"""
|
|
2365
2321
|
value = self._checkValue(value)
|
|
2366
2322
|
|
|
2367
|
-
if value ==
|
|
2368
|
-
yformat =
|
|
2323
|
+
if value == "nominal":
|
|
2324
|
+
yformat = "\\$k (nominal)"
|
|
2369
2325
|
infladjust = 1
|
|
2370
2326
|
else:
|
|
2371
|
-
yformat =
|
|
2327
|
+
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2372
2328
|
infladjust = self.gamma_n
|
|
2373
2329
|
|
|
2374
2330
|
years_n = np.array(self.year_n)
|
|
2375
2331
|
years_n = np.append(years_n, [years_n[-1] + 1])
|
|
2376
2332
|
y2stack = {}
|
|
2377
|
-
jDic = {
|
|
2378
|
-
kDic = {
|
|
2333
|
+
jDic = {"taxable": 0, "tax-deferred": 1, "tax-free": 2}
|
|
2334
|
+
kDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
|
|
2379
2335
|
figures = []
|
|
2380
2336
|
for jkey in jDic:
|
|
2381
2337
|
stackNames = []
|
|
2382
2338
|
for kkey in kDic:
|
|
2383
|
-
name = kkey +
|
|
2339
|
+
name = kkey + " / " + jkey
|
|
2384
2340
|
stackNames.append(name)
|
|
2385
2341
|
y2stack[name] = np.zeros((self.N_i, self.N_n + 1))
|
|
2386
2342
|
for i in range(self.N_i):
|
|
2387
2343
|
y2stack[name][i][:] = self.b_ijkn[i][jDic[jkey]][kDic[kkey]][:] / infladjust
|
|
2388
2344
|
|
|
2389
|
-
title = self._name +
|
|
2390
|
-
if tag !=
|
|
2391
|
-
title +=
|
|
2345
|
+
title = self._name + "\nAssets Distribution - " + jkey
|
|
2346
|
+
if tag != "":
|
|
2347
|
+
title += " - " + tag
|
|
2392
2348
|
|
|
2393
|
-
fig, ax = _stackPlot(
|
|
2394
|
-
|
|
2349
|
+
fig, ax = _stackPlot(
|
|
2350
|
+
years_n, self.inames, title, range(self.N_i), y2stack, stackNames, "upper left", yformat
|
|
2351
|
+
)
|
|
2395
2352
|
figures.append(fig)
|
|
2396
2353
|
|
|
2397
2354
|
if figure:
|
|
@@ -2400,7 +2357,7 @@ class Plan(object):
|
|
|
2400
2357
|
plt.show()
|
|
2401
2358
|
return None
|
|
2402
2359
|
|
|
2403
|
-
def showAllocations(self, tag=
|
|
2360
|
+
def showAllocations(self, tag="", figure=False):
|
|
2404
2361
|
"""
|
|
2405
2362
|
Plot desired allocation of savings accounts in percentage
|
|
2406
2363
|
over simulation time and interpolated by the selected method
|
|
@@ -2409,39 +2366,38 @@ class Plan(object):
|
|
|
2409
2366
|
A tag string can be set to add information to the title of the plot.
|
|
2410
2367
|
"""
|
|
2411
2368
|
count = self.N_i
|
|
2412
|
-
if self.ARCoord ==
|
|
2369
|
+
if self.ARCoord == "spouses":
|
|
2413
2370
|
acList = [self.ARCoord]
|
|
2414
2371
|
count = 1
|
|
2415
|
-
elif self.ARCoord ==
|
|
2372
|
+
elif self.ARCoord == "individual":
|
|
2416
2373
|
acList = [self.ARCoord]
|
|
2417
|
-
elif self.ARCoord ==
|
|
2418
|
-
acList = [
|
|
2374
|
+
elif self.ARCoord == "account":
|
|
2375
|
+
acList = ["taxable", "tax-deferred", "tax-free"]
|
|
2419
2376
|
else:
|
|
2420
|
-
raise ValueError(
|
|
2377
|
+
raise ValueError(f"Unknown coordination {self.ARCoord}.")
|
|
2421
2378
|
|
|
2422
2379
|
figures = []
|
|
2423
|
-
assetDic = {
|
|
2380
|
+
assetDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
|
|
2424
2381
|
for i in range(count):
|
|
2425
2382
|
y2stack = {}
|
|
2426
2383
|
for acType in acList:
|
|
2427
2384
|
stackNames = []
|
|
2428
2385
|
for key in assetDic:
|
|
2429
|
-
aname = key +
|
|
2386
|
+
aname = key + " / " + acType
|
|
2430
2387
|
stackNames.append(aname)
|
|
2431
2388
|
y2stack[aname] = np.zeros((count, self.N_n))
|
|
2432
2389
|
y2stack[aname][i][:] = self.alpha_ijkn[i, acList.index(acType), assetDic[key], : self.N_n]
|
|
2433
2390
|
|
|
2434
|
-
title = self._name +
|
|
2435
|
-
if self.ARCoord ==
|
|
2436
|
-
title +=
|
|
2391
|
+
title = self._name + "\nAsset Allocation (%) - " + acType
|
|
2392
|
+
if self.ARCoord == "spouses":
|
|
2393
|
+
title += " spouses"
|
|
2437
2394
|
else:
|
|
2438
|
-
title +=
|
|
2395
|
+
title += " " + self.inames[i]
|
|
2439
2396
|
|
|
2440
|
-
if tag !=
|
|
2441
|
-
title +=
|
|
2397
|
+
if tag != "":
|
|
2398
|
+
title += " - " + tag
|
|
2442
2399
|
|
|
2443
|
-
fig, ax = _stackPlot(self.year_n, self.inames, title, [i], y2stack,
|
|
2444
|
-
stackNames, 'upper left', 'percent')
|
|
2400
|
+
fig, ax = _stackPlot(self.year_n, self.inames, title, [i], y2stack, stackNames, "upper left", "percent")
|
|
2445
2401
|
figures.append(fig)
|
|
2446
2402
|
|
|
2447
2403
|
if figure:
|
|
@@ -2451,7 +2407,7 @@ class Plan(object):
|
|
|
2451
2407
|
return None
|
|
2452
2408
|
|
|
2453
2409
|
@_checkCaseStatus
|
|
2454
|
-
def showAccounts(self, tag=
|
|
2410
|
+
def showAccounts(self, tag="", value=None, figure=False):
|
|
2455
2411
|
"""
|
|
2456
2412
|
Plot values of savings accounts over time.
|
|
2457
2413
|
|
|
@@ -2462,25 +2418,24 @@ class Plan(object):
|
|
|
2462
2418
|
"""
|
|
2463
2419
|
value = self._checkValue(value)
|
|
2464
2420
|
|
|
2465
|
-
title = self._name +
|
|
2466
|
-
if tag !=
|
|
2467
|
-
title +=
|
|
2421
|
+
title = self._name + "\nSavings Balance"
|
|
2422
|
+
if tag != "":
|
|
2423
|
+
title += " - " + tag
|
|
2468
2424
|
|
|
2469
2425
|
stypes = self.savings_in.keys()
|
|
2470
2426
|
# Add one year for estate.
|
|
2471
2427
|
year_n = np.append(self.year_n, [self.year_n[-1] + 1])
|
|
2472
2428
|
|
|
2473
|
-
if value ==
|
|
2474
|
-
yformat =
|
|
2429
|
+
if value == "nominal":
|
|
2430
|
+
yformat = "\\$k (nominal)"
|
|
2475
2431
|
savings_in = self.savings_in
|
|
2476
2432
|
else:
|
|
2477
|
-
yformat =
|
|
2433
|
+
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2478
2434
|
savings_in = {}
|
|
2479
2435
|
for key in self.savings_in:
|
|
2480
2436
|
savings_in[key] = self.savings_in[key] / self.gamma_n
|
|
2481
2437
|
|
|
2482
|
-
fig, ax = _stackPlot(year_n, self.inames, title, range(self.N_i),
|
|
2483
|
-
savings_in, stypes, 'upper left', yformat)
|
|
2438
|
+
fig, ax = _stackPlot(year_n, self.inames, title, range(self.N_i), savings_in, stypes, "upper left", yformat)
|
|
2484
2439
|
|
|
2485
2440
|
if figure:
|
|
2486
2441
|
return fig
|
|
@@ -2489,7 +2444,7 @@ class Plan(object):
|
|
|
2489
2444
|
return None
|
|
2490
2445
|
|
|
2491
2446
|
@_checkCaseStatus
|
|
2492
|
-
def showSources(self, tag=
|
|
2447
|
+
def showSources(self, tag="", value=None, figure=False):
|
|
2493
2448
|
"""
|
|
2494
2449
|
Plot income over time.
|
|
2495
2450
|
|
|
@@ -2500,23 +2455,24 @@ class Plan(object):
|
|
|
2500
2455
|
"""
|
|
2501
2456
|
value = self._checkValue(value)
|
|
2502
2457
|
|
|
2503
|
-
title = self._name +
|
|
2458
|
+
title = self._name + "\nRaw Income Sources"
|
|
2504
2459
|
stypes = self.sources_in.keys()
|
|
2505
2460
|
|
|
2506
|
-
if tag !=
|
|
2507
|
-
title +=
|
|
2461
|
+
if tag != "":
|
|
2462
|
+
title += " - " + tag
|
|
2508
2463
|
|
|
2509
|
-
if value ==
|
|
2510
|
-
yformat =
|
|
2464
|
+
if value == "nominal":
|
|
2465
|
+
yformat = "\\$k (nominal)"
|
|
2511
2466
|
sources_in = self.sources_in
|
|
2512
2467
|
else:
|
|
2513
|
-
yformat =
|
|
2468
|
+
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2514
2469
|
sources_in = {}
|
|
2515
2470
|
for key in self.sources_in:
|
|
2516
2471
|
sources_in[key] = self.sources_in[key] / self.gamma_n[:-1]
|
|
2517
2472
|
|
|
2518
|
-
fig, ax = _stackPlot(
|
|
2519
|
-
|
|
2473
|
+
fig, ax = _stackPlot(
|
|
2474
|
+
self.year_n, self.inames, title, range(self.N_i), sources_in, stypes, "upper left", yformat
|
|
2475
|
+
)
|
|
2520
2476
|
|
|
2521
2477
|
if figure:
|
|
2522
2478
|
return fig
|
|
@@ -2525,34 +2481,34 @@ class Plan(object):
|
|
|
2525
2481
|
return None
|
|
2526
2482
|
|
|
2527
2483
|
@_checkCaseStatus
|
|
2528
|
-
def _showFeff(self, tag=
|
|
2484
|
+
def _showFeff(self, tag=""):
|
|
2529
2485
|
"""
|
|
2530
2486
|
Plot income tax paid over time.
|
|
2531
2487
|
|
|
2532
2488
|
A tag string can be set to add information to the title of the plot.
|
|
2533
2489
|
"""
|
|
2534
|
-
title = self._name +
|
|
2535
|
-
if tag !=
|
|
2536
|
-
title +=
|
|
2490
|
+
title = self._name + "\nEff f "
|
|
2491
|
+
if tag != "":
|
|
2492
|
+
title += " - " + tag
|
|
2537
2493
|
|
|
2538
|
-
various = [
|
|
2494
|
+
various = ["-", "--", "-.", ":"]
|
|
2539
2495
|
style = {}
|
|
2540
2496
|
series = {}
|
|
2541
2497
|
q = 0
|
|
2542
2498
|
for t in range(self.N_t):
|
|
2543
|
-
key =
|
|
2499
|
+
key = "f " + str(t)
|
|
2544
2500
|
series[key] = self.F_tn[t] / self.DeltaBar_tn[t]
|
|
2545
2501
|
# print(key, series[key])
|
|
2546
2502
|
style[key] = various[q % len(various)]
|
|
2547
2503
|
q += 1
|
|
2548
2504
|
|
|
2549
|
-
fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat=
|
|
2505
|
+
fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat="")
|
|
2550
2506
|
|
|
2551
2507
|
plt.show()
|
|
2552
2508
|
return None
|
|
2553
2509
|
|
|
2554
2510
|
@_checkCaseStatus
|
|
2555
|
-
def showTaxes(self, tag=
|
|
2511
|
+
def showTaxes(self, tag="", value=None, figure=False):
|
|
2556
2512
|
"""
|
|
2557
2513
|
Plot income tax paid over time.
|
|
2558
2514
|
|
|
@@ -2563,21 +2519,21 @@ class Plan(object):
|
|
|
2563
2519
|
"""
|
|
2564
2520
|
value = self._checkValue(value)
|
|
2565
2521
|
|
|
2566
|
-
style = {
|
|
2522
|
+
style = {"income taxes": "-", "Medicare": "-."}
|
|
2567
2523
|
|
|
2568
|
-
if value ==
|
|
2569
|
-
series = {
|
|
2570
|
-
yformat =
|
|
2524
|
+
if value == "nominal":
|
|
2525
|
+
series = {"income taxes": self.T_n, "Medicare": self.M_n}
|
|
2526
|
+
yformat = "\\$k (nominal)"
|
|
2571
2527
|
else:
|
|
2572
2528
|
series = {
|
|
2573
|
-
|
|
2574
|
-
|
|
2529
|
+
"income taxes": self.T_n / self.gamma_n[:-1],
|
|
2530
|
+
"Medicare": self.M_n / self.gamma_n[:-1],
|
|
2575
2531
|
}
|
|
2576
|
-
yformat =
|
|
2532
|
+
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2577
2533
|
|
|
2578
|
-
title = self._name +
|
|
2579
|
-
if tag !=
|
|
2580
|
-
title +=
|
|
2534
|
+
title = self._name + "\nIncome Tax"
|
|
2535
|
+
if tag != "":
|
|
2536
|
+
title += " - " + tag
|
|
2581
2537
|
|
|
2582
2538
|
fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat)
|
|
2583
2539
|
|
|
@@ -2588,7 +2544,7 @@ class Plan(object):
|
|
|
2588
2544
|
return None
|
|
2589
2545
|
|
|
2590
2546
|
@_checkCaseStatus
|
|
2591
|
-
def showGrossIncome(self, tag=
|
|
2547
|
+
def showGrossIncome(self, tag="", value=None, figure=False):
|
|
2592
2548
|
"""
|
|
2593
2549
|
Plot income tax and taxable income over time horizon.
|
|
2594
2550
|
|
|
@@ -2599,30 +2555,30 @@ class Plan(object):
|
|
|
2599
2555
|
"""
|
|
2600
2556
|
value = self._checkValue(value)
|
|
2601
2557
|
|
|
2602
|
-
style = {
|
|
2558
|
+
style = {"taxable income": "-"}
|
|
2603
2559
|
|
|
2604
|
-
if value ==
|
|
2605
|
-
series = {
|
|
2606
|
-
yformat =
|
|
2560
|
+
if value == "nominal":
|
|
2561
|
+
series = {"taxable income": self.G_n}
|
|
2562
|
+
yformat = "\\$k (nominal)"
|
|
2607
2563
|
infladjust = self.gamma_n[:-1]
|
|
2608
2564
|
else:
|
|
2609
|
-
series = {
|
|
2610
|
-
yformat =
|
|
2565
|
+
series = {"taxable income": self.G_n / self.gamma_n[:-1]}
|
|
2566
|
+
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2611
2567
|
infladjust = 1
|
|
2612
2568
|
|
|
2613
|
-
title = self._name +
|
|
2614
|
-
if tag !=
|
|
2615
|
-
title +=
|
|
2569
|
+
title = self._name + "\nTaxable Ordinary Income vs. Tax Brackets"
|
|
2570
|
+
if tag != "":
|
|
2571
|
+
title += " - " + tag
|
|
2616
2572
|
|
|
2617
2573
|
fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat)
|
|
2618
2574
|
|
|
2619
2575
|
data = tx.taxBrackets(self.N_i, self.n_d, self.N_n)
|
|
2620
2576
|
for key in data:
|
|
2621
2577
|
data_adj = data[key] * infladjust
|
|
2622
|
-
ax.plot(self.year_n, data_adj, label=key, ls=
|
|
2578
|
+
ax.plot(self.year_n, data_adj, label=key, ls=":")
|
|
2623
2579
|
|
|
2624
|
-
plt.grid(visible=
|
|
2625
|
-
ax.legend(loc=
|
|
2580
|
+
plt.grid(visible="both")
|
|
2581
|
+
ax.legend(loc="upper left", reverse=True, fontsize=8, framealpha=0.3)
|
|
2626
2582
|
|
|
2627
2583
|
if figure:
|
|
2628
2584
|
return fig
|
|
@@ -2636,7 +2592,7 @@ class Plan(object):
|
|
|
2636
2592
|
Save parameters in a configuration file.
|
|
2637
2593
|
"""
|
|
2638
2594
|
if basename is None:
|
|
2639
|
-
basename =
|
|
2595
|
+
basename = "case_" + self._name
|
|
2640
2596
|
|
|
2641
2597
|
config.saveConfig(self, basename, self.mylog)
|
|
2642
2598
|
|
|
@@ -2678,10 +2634,11 @@ class Plan(object):
|
|
|
2678
2634
|
|
|
2679
2635
|
Last worksheet contains summary.
|
|
2680
2636
|
"""
|
|
2637
|
+
|
|
2681
2638
|
def fillsheet(sheet, dic, datatype, op=lambda x: x):
|
|
2682
2639
|
rawData = {}
|
|
2683
|
-
rawData[
|
|
2684
|
-
if datatype ==
|
|
2640
|
+
rawData["year"] = self.year_n
|
|
2641
|
+
if datatype == "currency":
|
|
2685
2642
|
for key in dic:
|
|
2686
2643
|
rawData[key] = u.roundCents(op(dic[key]))
|
|
2687
2644
|
else:
|
|
@@ -2699,71 +2656,71 @@ class Plan(object):
|
|
|
2699
2656
|
|
|
2700
2657
|
# Income.
|
|
2701
2658
|
ws = wb.active
|
|
2702
|
-
ws.title =
|
|
2659
|
+
ws.title = "Income"
|
|
2703
2660
|
|
|
2704
2661
|
incomeDic = {
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2662
|
+
"net spending": self.g_n,
|
|
2663
|
+
"taxable ord. income": self.G_n,
|
|
2664
|
+
"taxable gains/divs": self.Q_n,
|
|
2665
|
+
"Tax bills + Med.": self.T_n + self.U_n + self.M_n,
|
|
2709
2666
|
}
|
|
2710
2667
|
|
|
2711
|
-
fillsheet(ws, incomeDic,
|
|
2668
|
+
fillsheet(ws, incomeDic, "currency")
|
|
2712
2669
|
|
|
2713
2670
|
# Cash flow - sum over both individuals for some.
|
|
2714
2671
|
cashFlowDic = {
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2672
|
+
"net spending": self.g_n,
|
|
2673
|
+
"all wages": np.sum(self.omega_in, axis=0),
|
|
2674
|
+
"all pensions": np.sum(self.piBar_in, axis=0),
|
|
2675
|
+
"all soc sec": np.sum(self.zetaBar_in, axis=0),
|
|
2719
2676
|
"all BTI's": np.sum(self.Lambda_in, axis=0),
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2677
|
+
"all wdrwls": np.sum(self.w_ijn, axis=(0, 1)),
|
|
2678
|
+
"all deposits": -np.sum(self.d_in, axis=0),
|
|
2679
|
+
"ord taxes": -self.T_n,
|
|
2680
|
+
"div taxes": -self.U_n,
|
|
2681
|
+
"Medicare": -self.M_n,
|
|
2725
2682
|
}
|
|
2726
|
-
sname =
|
|
2683
|
+
sname = "Cash Flow"
|
|
2727
2684
|
ws = wb.create_sheet(sname)
|
|
2728
|
-
fillsheet(ws, cashFlowDic,
|
|
2685
|
+
fillsheet(ws, cashFlowDic, "currency")
|
|
2729
2686
|
|
|
2730
2687
|
# Sources are handled separately.
|
|
2731
2688
|
srcDic = {
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2689
|
+
"wages": self.sources_in["wages"],
|
|
2690
|
+
"social sec": self.sources_in["ssec"],
|
|
2691
|
+
"pension": self.sources_in["pension"],
|
|
2692
|
+
"txbl acc wdrwl": self.sources_in["txbl acc wdrwl"],
|
|
2693
|
+
"RMDs": self.sources_in["RMD"],
|
|
2694
|
+
"+distributions": self.sources_in["+dist"],
|
|
2695
|
+
"Roth conv": self.sources_in["RothX"],
|
|
2696
|
+
"tax-free wdrwl": self.sources_in["tax-free wdrwl"],
|
|
2697
|
+
"big-ticket items": self.sources_in["BTI"],
|
|
2741
2698
|
}
|
|
2742
2699
|
|
|
2743
2700
|
for i in range(self.N_i):
|
|
2744
2701
|
sname = self.inames[i] + "'s Sources"
|
|
2745
2702
|
ws = wb.create_sheet(sname)
|
|
2746
|
-
fillsheet(ws, srcDic,
|
|
2703
|
+
fillsheet(ws, srcDic, "currency", op=lambda x: x[i])
|
|
2747
2704
|
|
|
2748
2705
|
# Account balances except final year.
|
|
2749
2706
|
accDic = {
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2707
|
+
"taxable bal": self.b_ijn[:, 0, :-1],
|
|
2708
|
+
"taxable ctrb": self.kappa_ijn[:, 0, :],
|
|
2709
|
+
"taxable dep": self.d_in,
|
|
2710
|
+
"taxable wdrwl": self.w_ijn[:, 0, :],
|
|
2711
|
+
"tax-deferred bal": self.b_ijn[:, 1, :-1],
|
|
2712
|
+
"tax-deferred ctrb": self.kappa_ijn[:, 1, :],
|
|
2713
|
+
"tax-deferred wdrwl": self.w_ijn[:, 1, :],
|
|
2714
|
+
"(included RMDs)": self.rmd_in[:, :],
|
|
2715
|
+
"Roth conv": self.x_in,
|
|
2716
|
+
"tax-free bal": self.b_ijn[:, 2, :-1],
|
|
2717
|
+
"tax-free ctrb": self.kappa_ijn[:, 2, :],
|
|
2718
|
+
"tax-free wdrwl": self.w_ijn[:, 2, :],
|
|
2762
2719
|
}
|
|
2763
2720
|
for i in range(self.N_i):
|
|
2764
2721
|
sname = self.inames[i] + "'s Accounts"
|
|
2765
2722
|
ws = wb.create_sheet(sname)
|
|
2766
|
-
fillsheet(ws, accDic,
|
|
2723
|
+
fillsheet(ws, accDic, "currency", op=lambda x: x[i])
|
|
2767
2724
|
# Add final balances.
|
|
2768
2725
|
lastRow = [
|
|
2769
2726
|
self.year_n[-1] + 1,
|
|
@@ -2781,11 +2738,11 @@ class Plan(object):
|
|
|
2781
2738
|
0,
|
|
2782
2739
|
]
|
|
2783
2740
|
ws.append(lastRow)
|
|
2784
|
-
_formatSpreadsheet(ws,
|
|
2741
|
+
_formatSpreadsheet(ws, "currency")
|
|
2785
2742
|
|
|
2786
2743
|
# Allocations.
|
|
2787
|
-
jDic = {
|
|
2788
|
-
kDic = {
|
|
2744
|
+
jDic = {"taxable": 0, "tax-deferred": 1, "tax-free": 2}
|
|
2745
|
+
kDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
|
|
2789
2746
|
|
|
2790
2747
|
# Add one year for estate.
|
|
2791
2748
|
year_n = np.append(self.year_n, [self.year_n[-1] + 1])
|
|
@@ -2793,28 +2750,30 @@ class Plan(object):
|
|
|
2793
2750
|
sname = self.inames[i] + "'s Allocations"
|
|
2794
2751
|
ws = wb.create_sheet(sname)
|
|
2795
2752
|
rawData = {}
|
|
2796
|
-
rawData[
|
|
2753
|
+
rawData["year"] = year_n
|
|
2797
2754
|
for jkey in jDic:
|
|
2798
2755
|
for kkey in kDic:
|
|
2799
|
-
rawData[jkey +
|
|
2756
|
+
rawData[jkey + "/" + kkey] = self.alpha_ijkn[i, jDic[jkey], kDic[kkey], :]
|
|
2800
2757
|
df = pd.DataFrame(rawData)
|
|
2801
2758
|
for row in dataframe_to_rows(df, index=False, header=True):
|
|
2802
2759
|
ws.append(row)
|
|
2803
2760
|
|
|
2804
|
-
_formatSpreadsheet(ws,
|
|
2761
|
+
_formatSpreadsheet(ws, "percent1")
|
|
2805
2762
|
|
|
2806
2763
|
# Rates on penultimate sheet.
|
|
2807
|
-
ratesDic = {
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2764
|
+
ratesDic = {
|
|
2765
|
+
"S&P 500": self.tau_kn[0],
|
|
2766
|
+
"Corporate Baa": self.tau_kn[1],
|
|
2767
|
+
"T Bonds": self.tau_kn[2],
|
|
2768
|
+
"inflation": self.tau_kn[3],
|
|
2769
|
+
}
|
|
2770
|
+
ws = wb.create_sheet("Rates")
|
|
2771
|
+
fillsheet(ws, ratesDic, "percent2")
|
|
2813
2772
|
|
|
2814
2773
|
# Summary on last sheet.
|
|
2815
|
-
ws = wb.create_sheet(
|
|
2774
|
+
ws = wb.create_sheet("Summary")
|
|
2816
2775
|
rawData = {}
|
|
2817
|
-
rawData[
|
|
2776
|
+
rawData["SUMMARY ==========================================================================="] = (
|
|
2818
2777
|
self.summaryList()
|
|
2819
2778
|
)
|
|
2820
2779
|
|
|
@@ -2822,7 +2781,7 @@ class Plan(object):
|
|
|
2822
2781
|
for row in dataframe_to_rows(df, index=False, header=True):
|
|
2823
2782
|
ws.append(row)
|
|
2824
2783
|
|
|
2825
|
-
_formatSpreadsheet(ws,
|
|
2784
|
+
_formatSpreadsheet(ws, "summary")
|
|
2826
2785
|
|
|
2827
2786
|
if saveToFile:
|
|
2828
2787
|
if basename is None:
|
|
@@ -2841,27 +2800,27 @@ class Plan(object):
|
|
|
2841
2800
|
"""
|
|
2842
2801
|
|
|
2843
2802
|
planData = {}
|
|
2844
|
-
planData[
|
|
2845
|
-
planData[
|
|
2846
|
-
planData[
|
|
2847
|
-
planData[
|
|
2848
|
-
planData[
|
|
2803
|
+
planData["year"] = self.year_n
|
|
2804
|
+
planData["net spending"] = self.g_n
|
|
2805
|
+
planData["taxable ord. income"] = self.G_n
|
|
2806
|
+
planData["taxable gains/divs"] = self.Q_n
|
|
2807
|
+
planData["tax bill"] = self.T_n
|
|
2849
2808
|
|
|
2850
2809
|
for i in range(self.N_i):
|
|
2851
|
-
planData[self.inames[i] +
|
|
2852
|
-
planData[self.inames[i] +
|
|
2853
|
-
planData[self.inames[i] +
|
|
2854
|
-
planData[self.inames[i] +
|
|
2855
|
-
planData[self.inames[i] +
|
|
2856
|
-
planData[self.inames[i] +
|
|
2857
|
-
planData[self.inames[i] +
|
|
2858
|
-
planData[self.inames[i] +
|
|
2859
|
-
planData[self.inames[i] +
|
|
2860
|
-
planData[self.inames[i] +
|
|
2861
|
-
planData[self.inames[i] +
|
|
2862
|
-
planData[self.inames[i] +
|
|
2863
|
-
|
|
2864
|
-
ratesDic = {
|
|
2810
|
+
planData[self.inames[i] + " txbl bal"] = self.b_ijn[i, 0, :-1]
|
|
2811
|
+
planData[self.inames[i] + " txbl dep"] = self.d_in[i, :]
|
|
2812
|
+
planData[self.inames[i] + " txbl wrdwl"] = self.w_ijn[i, 0, :]
|
|
2813
|
+
planData[self.inames[i] + " tx-def bal"] = self.b_ijn[i, 1, :-1]
|
|
2814
|
+
planData[self.inames[i] + " tx-def ctrb"] = self.kappa_ijn[i, 1, :]
|
|
2815
|
+
planData[self.inames[i] + " tx-def wdrl"] = self.w_ijn[i, 1, :]
|
|
2816
|
+
planData[self.inames[i] + " (RMD)"] = self.rmd_in[i, :]
|
|
2817
|
+
planData[self.inames[i] + " Roth conv"] = self.x_in[i, :]
|
|
2818
|
+
planData[self.inames[i] + " tx-free bal"] = self.b_ijn[i, 2, :-1]
|
|
2819
|
+
planData[self.inames[i] + " tx-free ctrb"] = self.kappa_ijn[i, 2, :]
|
|
2820
|
+
planData[self.inames[i] + " tax-free wdrwl"] = self.w_ijn[i, 2, :]
|
|
2821
|
+
planData[self.inames[i] + " big-ticket items"] = self.Lambda_in[i, :]
|
|
2822
|
+
|
|
2823
|
+
ratesDic = {"S&P 500": 0, "Corporate Baa": 1, "T Bonds": 2, "inflation": 3}
|
|
2865
2824
|
for key in ratesDic:
|
|
2866
2825
|
planData[key] = self.tau_kn[ratesDic[key]]
|
|
2867
2826
|
|
|
@@ -2869,39 +2828,39 @@ class Plan(object):
|
|
|
2869
2828
|
|
|
2870
2829
|
while True:
|
|
2871
2830
|
try:
|
|
2872
|
-
fname =
|
|
2831
|
+
fname = "worksheet" + "_" + basename + ".csv"
|
|
2873
2832
|
df.to_csv(fname)
|
|
2874
2833
|
break
|
|
2875
2834
|
except PermissionError:
|
|
2876
|
-
self.mylog.print('Failed to save "
|
|
2877
|
-
key = input(
|
|
2878
|
-
if key ==
|
|
2835
|
+
self.mylog.print(f'Failed to save "{fname}": Permission denied.')
|
|
2836
|
+
key = input("Close file and try again? [Yn] ")
|
|
2837
|
+
if key == "n":
|
|
2879
2838
|
break
|
|
2880
2839
|
except Exception as e:
|
|
2881
|
-
raise Exception(
|
|
2840
|
+
raise Exception(f"Unanticipated exception: {e}.")
|
|
2882
2841
|
|
|
2883
2842
|
return None
|
|
2884
2843
|
|
|
2885
2844
|
|
|
2886
|
-
def _lineIncomePlot(x, series, style, title, yformat=
|
|
2845
|
+
def _lineIncomePlot(x, series, style, title, yformat="\\$k"):
|
|
2887
2846
|
"""
|
|
2888
2847
|
Core line plotter function.
|
|
2889
2848
|
"""
|
|
2890
2849
|
import matplotlib.ticker as tk
|
|
2891
2850
|
|
|
2892
2851
|
fig, ax = plt.subplots(figsize=(6, 4))
|
|
2893
|
-
plt.grid(visible=
|
|
2852
|
+
plt.grid(visible="both")
|
|
2894
2853
|
|
|
2895
2854
|
for sname in series:
|
|
2896
2855
|
ax.plot(x, series[sname], label=sname, ls=style[sname])
|
|
2897
2856
|
|
|
2898
|
-
ax.legend(loc=
|
|
2857
|
+
ax.legend(loc="upper left", reverse=True, fontsize=8, framealpha=0.3)
|
|
2899
2858
|
ax.set_title(title)
|
|
2900
|
-
ax.set_xlabel(
|
|
2859
|
+
ax.set_xlabel("year")
|
|
2901
2860
|
ax.set_ylabel(yformat)
|
|
2902
2861
|
ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
|
|
2903
|
-
if
|
|
2904
|
-
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000),
|
|
2862
|
+
if "k" in yformat:
|
|
2863
|
+
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000), ",")))
|
|
2905
2864
|
# Give range to y values in unindexed flat profiles.
|
|
2906
2865
|
ymin, ymax = ax.get_ylim()
|
|
2907
2866
|
if ymax - ymin < 5000:
|
|
@@ -2910,7 +2869,7 @@ def _lineIncomePlot(x, series, style, title, yformat='\\$k'):
|
|
|
2910
2869
|
return fig, ax
|
|
2911
2870
|
|
|
2912
2871
|
|
|
2913
|
-
def _stackPlot(x, inames, title, irange, series, snames, location, yformat=
|
|
2872
|
+
def _stackPlot(x, inames, title, irange, series, snames, location, yformat="\\$k"):
|
|
2914
2873
|
"""
|
|
2915
2874
|
Core function for stacked plots.
|
|
2916
2875
|
"""
|
|
@@ -2921,28 +2880,28 @@ def _stackPlot(x, inames, title, irange, series, snames, location, yformat='\\$k
|
|
|
2921
2880
|
for i in irange:
|
|
2922
2881
|
tmp = series[sname][i]
|
|
2923
2882
|
if sum(tmp) > 1.0:
|
|
2924
|
-
nonzeroSeries[sname +
|
|
2883
|
+
nonzeroSeries[sname + " " + inames[i]] = tmp
|
|
2925
2884
|
|
|
2926
2885
|
if len(nonzeroSeries) == 0:
|
|
2927
2886
|
# print('Nothing to plot for', title)
|
|
2928
2887
|
return None
|
|
2929
2888
|
|
|
2930
2889
|
fig, ax = plt.subplots(figsize=(6, 4))
|
|
2931
|
-
plt.grid(visible=
|
|
2890
|
+
plt.grid(visible="both")
|
|
2932
2891
|
|
|
2933
2892
|
ax.stackplot(x, nonzeroSeries.values(), labels=nonzeroSeries.keys(), alpha=0.6)
|
|
2934
2893
|
ax.legend(loc=location, reverse=True, fontsize=8, ncol=2, framealpha=0.5)
|
|
2935
2894
|
ax.set_title(title)
|
|
2936
|
-
ax.set_xlabel(
|
|
2895
|
+
ax.set_xlabel("year")
|
|
2937
2896
|
ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
|
|
2938
|
-
if
|
|
2897
|
+
if "k" in yformat:
|
|
2939
2898
|
ax.set_ylabel(yformat)
|
|
2940
|
-
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000),
|
|
2941
|
-
elif yformat ==
|
|
2942
|
-
ax.set_ylabel(
|
|
2943
|
-
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(100 * x),
|
|
2899
|
+
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000), ",")))
|
|
2900
|
+
elif yformat == "percent":
|
|
2901
|
+
ax.set_ylabel("%")
|
|
2902
|
+
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(100 * x), ",")))
|
|
2944
2903
|
else:
|
|
2945
|
-
raise RuntimeError(
|
|
2904
|
+
raise RuntimeError(f"Unknown yformat: {yformat}.")
|
|
2946
2905
|
|
|
2947
2906
|
return fig, ax
|
|
2948
2907
|
|
|
@@ -2955,29 +2914,29 @@ def _saveWorkbook(wb, basename, overwrite, mylog):
|
|
|
2955
2914
|
from pathlib import Path
|
|
2956
2915
|
|
|
2957
2916
|
if Path(basename).suffixes == []:
|
|
2958
|
-
fname =
|
|
2917
|
+
fname = "workbook" + "_" + basename + ".xlsx"
|
|
2959
2918
|
else:
|
|
2960
2919
|
fname = basename
|
|
2961
2920
|
|
|
2962
2921
|
if overwrite is False and isfile(fname):
|
|
2963
|
-
mylog.print('File "
|
|
2964
|
-
key = input(
|
|
2965
|
-
if key !=
|
|
2966
|
-
mylog.vprint(
|
|
2922
|
+
mylog.print(f'File "{fname}" already exists.')
|
|
2923
|
+
key = input("Overwrite? [Ny] ")
|
|
2924
|
+
if key != "y":
|
|
2925
|
+
mylog.vprint("Skipping save and returning.")
|
|
2967
2926
|
return None
|
|
2968
2927
|
|
|
2969
2928
|
while True:
|
|
2970
2929
|
try:
|
|
2971
|
-
mylog.vprint('Saving plan as "
|
|
2930
|
+
mylog.vprint(f'Saving plan as "{fname}".')
|
|
2972
2931
|
wb.save(fname)
|
|
2973
2932
|
break
|
|
2974
2933
|
except PermissionError:
|
|
2975
|
-
mylog.print('Failed to save "
|
|
2976
|
-
key = input(
|
|
2977
|
-
if key ==
|
|
2934
|
+
mylog.print(f'Failed to save "{fname}": Permission denied.')
|
|
2935
|
+
key = input("Close file and try again? [Yn] ")
|
|
2936
|
+
if key == "n":
|
|
2978
2937
|
break
|
|
2979
2938
|
except Exception as e:
|
|
2980
|
-
raise Exception(
|
|
2939
|
+
raise Exception(f"Unanticipated exception {e}.")
|
|
2981
2940
|
|
|
2982
2941
|
return None
|
|
2983
2942
|
|
|
@@ -2986,31 +2945,31 @@ def _formatSpreadsheet(ws, ftype):
|
|
|
2986
2945
|
"""
|
|
2987
2946
|
Utility function to beautify spreadsheet.
|
|
2988
2947
|
"""
|
|
2989
|
-
if ftype ==
|
|
2990
|
-
fstring =
|
|
2991
|
-
elif ftype ==
|
|
2992
|
-
fstring =
|
|
2993
|
-
elif ftype ==
|
|
2994
|
-
fstring =
|
|
2995
|
-
elif ftype ==
|
|
2996
|
-
fstring =
|
|
2997
|
-
elif ftype ==
|
|
2948
|
+
if ftype == "currency":
|
|
2949
|
+
fstring = "$#,##0_);[Red]($#,##0)"
|
|
2950
|
+
elif ftype == "percent2":
|
|
2951
|
+
fstring = "#.00%"
|
|
2952
|
+
elif ftype == "percent1":
|
|
2953
|
+
fstring = "#.0%"
|
|
2954
|
+
elif ftype == "percent0":
|
|
2955
|
+
fstring = "#0%"
|
|
2956
|
+
elif ftype == "summary":
|
|
2998
2957
|
for col in ws.columns:
|
|
2999
2958
|
column = col[0].column_letter
|
|
3000
2959
|
width = max(len(str(col[0].value)) + 20, 40)
|
|
3001
2960
|
ws.column_dimensions[column].width = width
|
|
3002
2961
|
return None
|
|
3003
2962
|
else:
|
|
3004
|
-
raise RuntimeError(
|
|
2963
|
+
raise RuntimeError(f"Unknown format: {ftype}.")
|
|
3005
2964
|
|
|
3006
|
-
for cell in ws[1] + ws[
|
|
3007
|
-
cell.style =
|
|
2965
|
+
for cell in ws[1] + ws["A"]:
|
|
2966
|
+
cell.style = "Pandas"
|
|
3008
2967
|
for col in ws.columns:
|
|
3009
2968
|
column = col[0].column_letter
|
|
3010
2969
|
# col[0].style = 'Title'
|
|
3011
2970
|
width = max(len(str(col[0].value)) + 4, 10)
|
|
3012
2971
|
ws.column_dimensions[column].width = width
|
|
3013
|
-
if column !=
|
|
2972
|
+
if column != "A":
|
|
3014
2973
|
for cell in col:
|
|
3015
2974
|
cell.number_format = fstring
|
|
3016
2975
|
|