owlplanner 2025.5.5__py3-none-any.whl → 2025.5.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- owlplanner/__init__.py +3 -1
- owlplanner/config.py +2 -2
- owlplanner/plan.py +139 -274
- owlplanner/plotting/__init__.py +7 -0
- owlplanner/plotting/base.py +76 -0
- owlplanner/plotting/factory.py +32 -0
- owlplanner/plotting/matplotlib_backend.py +432 -0
- owlplanner/plotting/plotly_backend.py +980 -0
- owlplanner/rates.py +3 -14
- owlplanner/timelists.py +2 -7
- owlplanner/version.py +1 -1
- {owlplanner-2025.5.5.dist-info → owlplanner-2025.5.15.dist-info}/METADATA +10 -5
- owlplanner-2025.5.15.dist-info/RECORD +22 -0
- owlplanner/plots.py +0 -296
- owlplanner-2025.5.5.dist-info/RECORD +0 -18
- /owlplanner/{logging.py → mylogging.py} +0 -0
- {owlplanner-2025.5.5.dist-info → owlplanner-2025.5.15.dist-info}/WHEEL +0 -0
- {owlplanner-2025.5.5.dist-info → owlplanner-2025.5.15.dist-info}/licenses/LICENSE +0 -0
owlplanner/__init__.py
CHANGED
|
@@ -2,5 +2,7 @@ from owlplanner.plan import Plan #
|
|
|
2
2
|
from owlplanner.plan import clone # noqa: F401
|
|
3
3
|
from owlplanner.config import readConfig # noqa: F401
|
|
4
4
|
from owlplanner.rates import getRatesDistributions # noqa: F401
|
|
5
|
-
from owlplanner.rates import showRatesDistributions # noqa: F401
|
|
6
5
|
from owlplanner.version import __version__ # noqa: F401
|
|
6
|
+
|
|
7
|
+
# Make the package importable as 'owlplanner'
|
|
8
|
+
__all__ = ['Plan', 'clone', 'readConfig', 'getRatesDistributions', '__version__']
|
owlplanner/config.py
CHANGED
|
@@ -16,7 +16,7 @@ from datetime import date
|
|
|
16
16
|
import os
|
|
17
17
|
|
|
18
18
|
from owlplanner import plan
|
|
19
|
-
from owlplanner import
|
|
19
|
+
from owlplanner import mylogging as log
|
|
20
20
|
from owlplanner.rates import FROM, TO
|
|
21
21
|
|
|
22
22
|
|
|
@@ -143,7 +143,7 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
|
|
|
143
143
|
A new plan is created and returned.
|
|
144
144
|
Argument file can be a filename, a file, or a stringIO.
|
|
145
145
|
"""
|
|
146
|
-
mylog =
|
|
146
|
+
mylog = log.Logger(verbose, logstreams)
|
|
147
147
|
|
|
148
148
|
accountTypes = ["taxable", "tax-deferred", "tax-free"]
|
|
149
149
|
|
owlplanner/plan.py
CHANGED
|
@@ -16,26 +16,21 @@ Disclaimer: This program comes with no guarantee. Use at your own risk.
|
|
|
16
16
|
###########################################################################
|
|
17
17
|
import numpy as np
|
|
18
18
|
import pandas as pd
|
|
19
|
-
import matplotlib.pyplot as plt
|
|
20
19
|
from datetime import date, datetime
|
|
21
20
|
from functools import wraps
|
|
22
21
|
from openpyxl import Workbook
|
|
23
22
|
from openpyxl.utils.dataframe import dataframe_to_rows
|
|
24
23
|
import time
|
|
25
24
|
|
|
26
|
-
from
|
|
27
|
-
from
|
|
28
|
-
from
|
|
29
|
-
from
|
|
30
|
-
from
|
|
31
|
-
from
|
|
32
|
-
from
|
|
33
|
-
from
|
|
34
|
-
from
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# This makes all graphs to have the same height.
|
|
38
|
-
plt.rcParams.update({'figure.autolayout': True})
|
|
25
|
+
from . import utils as u
|
|
26
|
+
from . import tax2025 as tx
|
|
27
|
+
from . import abcapi as abc
|
|
28
|
+
from . import rates
|
|
29
|
+
from . import config
|
|
30
|
+
from . import timelists
|
|
31
|
+
from . import mylogging as log
|
|
32
|
+
from . import progress
|
|
33
|
+
from .plotting.factory import PlotFactory
|
|
39
34
|
|
|
40
35
|
|
|
41
36
|
def _genGamma_n(tau):
|
|
@@ -246,6 +241,8 @@ class Plan(object):
|
|
|
246
241
|
self._description = ''
|
|
247
242
|
self.defaultPlots = "nominal"
|
|
248
243
|
self.defaultSolver = "HiGHS"
|
|
244
|
+
self._plotterName = None
|
|
245
|
+
self.setPlotBackend("matplotlib")
|
|
249
246
|
|
|
250
247
|
self.N_i = len(yobs)
|
|
251
248
|
if not (0 <= self.N_i <= 2):
|
|
@@ -257,7 +254,7 @@ class Plan(object):
|
|
|
257
254
|
if inames[0] == "" or (self.N_i == 2 and inames[1] == ""):
|
|
258
255
|
raise ValueError("Name for each individual must be provided.")
|
|
259
256
|
|
|
260
|
-
self.filingStatus =
|
|
257
|
+
self.filingStatus = ("single", "married")[self.N_i - 1]
|
|
261
258
|
# Default year TCJA is speculated to expire.
|
|
262
259
|
self.yTCJA = 2026
|
|
263
260
|
self.inames = inames
|
|
@@ -314,7 +311,7 @@ class Plan(object):
|
|
|
314
311
|
self.lambdha = 0
|
|
315
312
|
|
|
316
313
|
# Scenario starts at the beginning of this year and ends at the end of the last year.
|
|
317
|
-
s =
|
|
314
|
+
s = ("", "s")[self.N_i - 1]
|
|
318
315
|
self.mylog.vprint(f"Preparing scenario of {self.N_n} years for {self.N_i} individual{s}.")
|
|
319
316
|
for i in range(self.N_i):
|
|
320
317
|
endyear = thisyear + self.horizons[i] - 1
|
|
@@ -349,7 +346,7 @@ class Plan(object):
|
|
|
349
346
|
self.mylog = logger
|
|
350
347
|
|
|
351
348
|
def setLogstreams(self, verbose, logstreams):
|
|
352
|
-
self.mylog =
|
|
349
|
+
self.mylog = log.Logger(verbose, logstreams)
|
|
353
350
|
# self.mylog.vprint(f"Setting logstreams to {logstreams}.")
|
|
354
351
|
|
|
355
352
|
def logger(self):
|
|
@@ -403,13 +400,11 @@ class Plan(object):
|
|
|
403
400
|
if value is None:
|
|
404
401
|
return self.defaultPlots
|
|
405
402
|
|
|
406
|
-
opts =
|
|
407
|
-
if value in opts:
|
|
408
|
-
|
|
403
|
+
opts = ("nominal", "today")
|
|
404
|
+
if value not in opts:
|
|
405
|
+
raise ValueError(f"Value type must be one of: {opts}")
|
|
409
406
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
return None
|
|
407
|
+
return value
|
|
413
408
|
|
|
414
409
|
def rename(self, newname):
|
|
415
410
|
"""
|
|
@@ -454,6 +449,18 @@ class Plan(object):
|
|
|
454
449
|
self.defaultPlots = self._checkValue(value)
|
|
455
450
|
self.mylog.vprint(f"Setting plots default value to {value}.")
|
|
456
451
|
|
|
452
|
+
def setPlotBackend(self, backend: str):
|
|
453
|
+
"""
|
|
454
|
+
Set plotting backend.
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
if backend not in ("matplotlib", "plotly"):
|
|
458
|
+
raise ValueError(f"Backend {backend} not a valid option.")
|
|
459
|
+
|
|
460
|
+
if backend != self._plotterName:
|
|
461
|
+
self._plotter = PlotFactory.createBackend(backend)
|
|
462
|
+
self.mylog.vprint(f"Setting plotting backend to {backend}.")
|
|
463
|
+
|
|
457
464
|
def setDividendRate(self, mu):
|
|
458
465
|
"""
|
|
459
466
|
Set dividend tax rate. Rate is in percent. Default 2%.
|
|
@@ -1100,7 +1107,7 @@ class Plan(object):
|
|
|
1100
1107
|
return None
|
|
1101
1108
|
|
|
1102
1109
|
def _setup_constraint_shortcuts(self, options):
|
|
1103
|
-
# Set up all the local variables as attributes for use in helpers
|
|
1110
|
+
# Set up all the local variables as attributes for use in helpers.
|
|
1104
1111
|
oppCostX = options.get("oppCostX", 0.)
|
|
1105
1112
|
self.xnet = 1 - oppCostX / 100.
|
|
1106
1113
|
self.optionsUnits = u.getUnits(options.get("units", "k"))
|
|
@@ -1390,11 +1397,15 @@ class Plan(object):
|
|
|
1390
1397
|
"""
|
|
1391
1398
|
Run historical scenarios on plan over a range of years.
|
|
1392
1399
|
"""
|
|
1400
|
+
|
|
1393
1401
|
if yend + self.N_n > self.year_n[0]:
|
|
1394
1402
|
yend = self.year_n[0] - self.N_n - 1
|
|
1395
1403
|
self.mylog.vprint(f"Warning: Upper bound for year range re-adjusted to {yend}.")
|
|
1396
|
-
N = yend - ystart + 1
|
|
1397
1404
|
|
|
1405
|
+
if yend < ystart:
|
|
1406
|
+
raise ValueError(f"Starting year is too large to support a lifespan of {self.N_n} years.")
|
|
1407
|
+
|
|
1408
|
+
N = yend - ystart + 1
|
|
1398
1409
|
self.mylog.vprint(f"Running historical range from {ystart} to {yend}.")
|
|
1399
1410
|
|
|
1400
1411
|
self.mylog.setVerbose(verbose)
|
|
@@ -1405,7 +1416,7 @@ class Plan(object):
|
|
|
1405
1416
|
columns = ["partial", "final"]
|
|
1406
1417
|
else:
|
|
1407
1418
|
self.mylog.print(f"Invalid objective {objective}.")
|
|
1408
|
-
|
|
1419
|
+
raise ValueError(f"Invalid objective {objective}.")
|
|
1409
1420
|
|
|
1410
1421
|
df = pd.DataFrame(columns=columns)
|
|
1411
1422
|
|
|
@@ -1429,10 +1440,11 @@ class Plan(object):
|
|
|
1429
1440
|
progcall.finish()
|
|
1430
1441
|
self.mylog.resetVerbose()
|
|
1431
1442
|
|
|
1443
|
+
fig, description = self._plotter.plot_histogram_results(objective, df, N, self.year_n,
|
|
1444
|
+
self.n_d, self.N_i, self.phi_j)
|
|
1445
|
+
self.mylog.print(description.getvalue())
|
|
1446
|
+
|
|
1432
1447
|
if figure:
|
|
1433
|
-
fig, description = plots.show_histogram_results(objective, df, N, self.year_n,
|
|
1434
|
-
self.n_d, self.N_i, self.phi_j)
|
|
1435
|
-
self.mylog.print(description.getvalue())
|
|
1436
1448
|
return fig, description.getvalue()
|
|
1437
1449
|
|
|
1438
1450
|
return N, df
|
|
@@ -1442,7 +1454,7 @@ class Plan(object):
|
|
|
1442
1454
|
"""
|
|
1443
1455
|
Run Monte Carlo simulations on plan.
|
|
1444
1456
|
"""
|
|
1445
|
-
if self.rateMethod not in
|
|
1457
|
+
if self.rateMethod not in ("stochastic", "histochastic"):
|
|
1446
1458
|
self.mylog.print("It is pointless to run Monte Carlo simulations with fixed rates.")
|
|
1447
1459
|
return
|
|
1448
1460
|
|
|
@@ -1486,10 +1498,11 @@ class Plan(object):
|
|
|
1486
1498
|
progcall.finish()
|
|
1487
1499
|
self.mylog.resetVerbose()
|
|
1488
1500
|
|
|
1501
|
+
fig, description = self._plotter.plot_histogram_results(objective, df, N, self.year_n,
|
|
1502
|
+
self.n_d, self.N_i, self.phi_j)
|
|
1503
|
+
self.mylog.print(description.getvalue())
|
|
1504
|
+
|
|
1489
1505
|
if figure:
|
|
1490
|
-
fig, description = plots.show_histogram_results(objective, df, N, self.year_n,
|
|
1491
|
-
self.n_d, self.N_i, self.phi_j)
|
|
1492
|
-
self.mylog.print(description.getvalue())
|
|
1493
1506
|
return fig, description.getvalue()
|
|
1494
1507
|
|
|
1495
1508
|
return N, df
|
|
@@ -1625,7 +1638,8 @@ class Plan(object):
|
|
|
1625
1638
|
while True:
|
|
1626
1639
|
solution, xx, solverSuccess, solverMsg = solverMethod(objective, options)
|
|
1627
1640
|
|
|
1628
|
-
if not solverSuccess:
|
|
1641
|
+
if not solverSuccess or solution is None:
|
|
1642
|
+
self.mylog.vprint("Solver failed:", solverMsg, solverSuccess)
|
|
1629
1643
|
break
|
|
1630
1644
|
|
|
1631
1645
|
if not withMedicare:
|
|
@@ -1675,8 +1689,13 @@ class Plan(object):
|
|
|
1675
1689
|
"""
|
|
1676
1690
|
from scipy import optimize
|
|
1677
1691
|
|
|
1678
|
-
#
|
|
1679
|
-
milpOptions = {
|
|
1692
|
+
# Optimize solver parameters
|
|
1693
|
+
milpOptions = {
|
|
1694
|
+
"disp": False,
|
|
1695
|
+
"mip_rel_gap": 1e-7,
|
|
1696
|
+
"presolve": True,
|
|
1697
|
+
"node_limit": 10000 # Limit search nodes for faster solutions
|
|
1698
|
+
}
|
|
1680
1699
|
|
|
1681
1700
|
self._buildConstraints(objective, options)
|
|
1682
1701
|
Alu, lbvec, ubvec = self.A.arrays()
|
|
@@ -2047,10 +2066,10 @@ class Plan(object):
|
|
|
2047
2066
|
dic[f"Net spending for year {now}"] = u.d(self.g_n[0] / self.yearFracLeft)
|
|
2048
2067
|
dic[f"Net spending remaining in year {now}"] = u.d(self.g_n[0])
|
|
2049
2068
|
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
dic["Total net spending"] = f"{u.d(
|
|
2053
|
-
dic["[Total net spending]"] = f"{u.d(
|
|
2069
|
+
totSpending = np.sum(self.g_n, axis=0)
|
|
2070
|
+
totSpendingNow = np.sum(self.g_n / self.gamma_n[:-1], axis=0)
|
|
2071
|
+
dic["Total net spending"] = f"{u.d(totSpendingNow)}"
|
|
2072
|
+
dic["[Total net spending]"] = f"{u.d(totSpending)}"
|
|
2054
2073
|
|
|
2055
2074
|
totRoth = np.sum(self.x_in, axis=(0, 1))
|
|
2056
2075
|
totRothNow = np.sum(np.sum(self.x_in, axis=0) / self.gamma_n[:-1], axis=0)
|
|
@@ -2150,13 +2169,25 @@ class Plan(object):
|
|
|
2150
2169
|
self.mylog.vprint(f"Warning: Cannot plot correlations for {self.rateMethod} rate method.")
|
|
2151
2170
|
return None
|
|
2152
2171
|
|
|
2153
|
-
fig =
|
|
2154
|
-
|
|
2172
|
+
fig = self._plotter.plot_rates_correlations(self._name, self.tau_kn, self.N_n, self.rateMethod,
|
|
2173
|
+
self.rateFrm, self.rateTo, tag, shareRange)
|
|
2174
|
+
|
|
2175
|
+
if figure:
|
|
2176
|
+
return fig
|
|
2155
2177
|
|
|
2178
|
+
self._plotter.jupyter_renderer(fig)
|
|
2179
|
+
return None
|
|
2180
|
+
|
|
2181
|
+
def showRatesDistributions(self, frm=rates.FROM, to=rates.TO, figure=False):
|
|
2182
|
+
"""
|
|
2183
|
+
Plot histograms of the rates distributions.
|
|
2184
|
+
"""
|
|
2185
|
+
fig = self._plotter.plot_rates_distributions(frm, to, rates.SP500, rates.BondsBaa,
|
|
2186
|
+
rates.TNotes, rates.Inflation, rates.FROM)
|
|
2156
2187
|
if figure:
|
|
2157
2188
|
return fig
|
|
2158
2189
|
|
|
2159
|
-
|
|
2190
|
+
self._plotter.jupyter_renderer(fig)
|
|
2160
2191
|
return None
|
|
2161
2192
|
|
|
2162
2193
|
def showRates(self, tag="", figure=False):
|
|
@@ -2169,13 +2200,13 @@ class Plan(object):
|
|
|
2169
2200
|
self.mylog.vprint("Warning: Rate method must be selected before plotting.")
|
|
2170
2201
|
return None
|
|
2171
2202
|
|
|
2172
|
-
fig =
|
|
2173
|
-
|
|
2203
|
+
fig = self._plotter.plot_rates(self._name, self.tau_kn, self.year_n, self.yearFracLeft,
|
|
2204
|
+
self.N_k, self.rateMethod, self.rateFrm, self.rateTo, tag)
|
|
2174
2205
|
|
|
2175
2206
|
if figure:
|
|
2176
2207
|
return fig
|
|
2177
2208
|
|
|
2178
|
-
|
|
2209
|
+
self._plotter.jupyter_renderer(fig)
|
|
2179
2210
|
return None
|
|
2180
2211
|
|
|
2181
2212
|
def showProfile(self, tag="", figure=False):
|
|
@@ -2187,19 +2218,15 @@ class Plan(object):
|
|
|
2187
2218
|
if self.xi_n is None:
|
|
2188
2219
|
self.mylog.vprint("Warning: Profile must be selected before plotting.")
|
|
2189
2220
|
return None
|
|
2190
|
-
|
|
2191
2221
|
title = self._name + "\nSpending Profile"
|
|
2192
|
-
if tag
|
|
2222
|
+
if tag:
|
|
2193
2223
|
title += " - " + tag
|
|
2194
|
-
|
|
2195
|
-
style = {"profile": "-"}
|
|
2196
|
-
series = {"profile": self.xi_n}
|
|
2197
|
-
fig, ax = plots.line_income_plot(self.year_n, series, style, title, yformat="$\\xi$")
|
|
2224
|
+
fig = self._plotter.plot_profile(self.year_n, self.xi_n, title, self.inames)
|
|
2198
2225
|
|
|
2199
2226
|
if figure:
|
|
2200
2227
|
return fig
|
|
2201
2228
|
|
|
2202
|
-
|
|
2229
|
+
self._plotter.jupyter_renderer(fig)
|
|
2203
2230
|
return None
|
|
2204
2231
|
|
|
2205
2232
|
@_checkCaseStatus
|
|
@@ -2213,28 +2240,15 @@ class Plan(object):
|
|
|
2213
2240
|
the default behavior of setDefaultPlots().
|
|
2214
2241
|
"""
|
|
2215
2242
|
value = self._checkValue(value)
|
|
2216
|
-
|
|
2217
2243
|
title = self._name + "\nNet Available Spending"
|
|
2218
|
-
if tag
|
|
2244
|
+
if tag:
|
|
2219
2245
|
title += " - " + tag
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
if value == "nominal":
|
|
2223
|
-
series = {"net": self.g_n, "target": (self.g_n[0] / self.xi_n[0]) * self.xiBar_n}
|
|
2224
|
-
yformat = "\\$k (nominal)"
|
|
2225
|
-
else:
|
|
2226
|
-
series = {
|
|
2227
|
-
"net": self.g_n / self.gamma_n[:-1],
|
|
2228
|
-
"target": (self.g_n[0] / self.xi_n[0]) * self.xi_n,
|
|
2229
|
-
}
|
|
2230
|
-
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2231
|
-
|
|
2232
|
-
fig, ax = plots.line_income_plot(self.year_n, series, style, title, yformat)
|
|
2233
|
-
|
|
2246
|
+
fig = self._plotter.plot_net_spending(self.year_n, self.g_n, self.xi_n, self.xiBar_n,
|
|
2247
|
+
self.gamma_n, value, title, self.inames)
|
|
2234
2248
|
if figure:
|
|
2235
2249
|
return fig
|
|
2236
2250
|
|
|
2237
|
-
|
|
2251
|
+
self._plotter.jupyter_renderer(fig)
|
|
2238
2252
|
return None
|
|
2239
2253
|
|
|
2240
2254
|
@_checkCaseStatus
|
|
@@ -2251,41 +2265,37 @@ class Plan(object):
|
|
|
2251
2265
|
the default behavior of setDefaultPlots().
|
|
2252
2266
|
"""
|
|
2253
2267
|
value = self._checkValue(value)
|
|
2268
|
+
figures = self._plotter.plot_asset_distribution(self.year_n, self.inames, self.b_ijkn,
|
|
2269
|
+
self.gamma_n, value, self._name, tag)
|
|
2270
|
+
if figure:
|
|
2271
|
+
return figures
|
|
2254
2272
|
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
else:
|
|
2259
|
-
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2260
|
-
infladjust = self.gamma_n
|
|
2261
|
-
|
|
2262
|
-
years_n = np.array(self.year_n)
|
|
2263
|
-
years_n = np.append(years_n, [years_n[-1] + 1])
|
|
2264
|
-
y2stack = {}
|
|
2265
|
-
jDic = {"taxable": 0, "tax-deferred": 1, "tax-free": 2}
|
|
2266
|
-
kDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
|
|
2267
|
-
figures = []
|
|
2268
|
-
for jkey in jDic:
|
|
2269
|
-
stackNames = []
|
|
2270
|
-
for kkey in kDic:
|
|
2271
|
-
name = kkey + " / " + jkey
|
|
2272
|
-
stackNames.append(name)
|
|
2273
|
-
y2stack[name] = np.zeros((self.N_i, self.N_n + 1))
|
|
2274
|
-
for i in range(self.N_i):
|
|
2275
|
-
y2stack[name][i][:] = self.b_ijkn[i][jDic[jkey]][kDic[kkey]][:] / infladjust
|
|
2273
|
+
for fig in figures:
|
|
2274
|
+
self._plotter.jupyter_renderer(fig)
|
|
2275
|
+
return None
|
|
2276
2276
|
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2277
|
+
@_checkCaseStatus
|
|
2278
|
+
def showGrossIncome(self, tag="", value=None, figure=False):
|
|
2279
|
+
"""
|
|
2280
|
+
Plot income tax and taxable income over time horizon.
|
|
2280
2281
|
|
|
2281
|
-
|
|
2282
|
-
y2stack, stackNames, "upper left", yformat)
|
|
2283
|
-
figures.append(fig)
|
|
2282
|
+
A tag string can be set to add information to the title of the plot.
|
|
2284
2283
|
|
|
2284
|
+
The value parameter can be set to *nominal* or *today*, overriding
|
|
2285
|
+
the default behavior of setDefaultPlots().
|
|
2286
|
+
"""
|
|
2287
|
+
value = self._checkValue(value)
|
|
2288
|
+
tax_brackets = tx.taxBrackets(self.N_i, self.n_d, self.N_n, self.yTCJA)
|
|
2289
|
+
title = self._name + "\nTaxable Ordinary Income vs. Tax Brackets"
|
|
2290
|
+
if tag:
|
|
2291
|
+
title += " - " + tag
|
|
2292
|
+
fig = self._plotter.plot_gross_income(
|
|
2293
|
+
self.year_n, self.G_n, self.gamma_n, value, title, tax_brackets
|
|
2294
|
+
)
|
|
2285
2295
|
if figure:
|
|
2286
|
-
return
|
|
2296
|
+
return fig
|
|
2287
2297
|
|
|
2288
|
-
|
|
2298
|
+
self._plotter.jupyter_renderer(fig)
|
|
2289
2299
|
return None
|
|
2290
2300
|
|
|
2291
2301
|
def showAllocations(self, tag="", figure=False):
|
|
@@ -2296,46 +2306,16 @@ class Plan(object):
|
|
|
2296
2306
|
|
|
2297
2307
|
A tag string can be set to add information to the title of the plot.
|
|
2298
2308
|
"""
|
|
2299
|
-
|
|
2300
|
-
if
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
acList = [self.ARCoord]
|
|
2305
|
-
elif self.ARCoord == "account":
|
|
2306
|
-
acList = ["taxable", "tax-deferred", "tax-free"]
|
|
2307
|
-
else:
|
|
2308
|
-
raise ValueError(f"Unknown coordination {self.ARCoord}.")
|
|
2309
|
-
|
|
2310
|
-
figures = []
|
|
2311
|
-
assetDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
|
|
2312
|
-
for i in range(count):
|
|
2313
|
-
y2stack = {}
|
|
2314
|
-
for acType in acList:
|
|
2315
|
-
stackNames = []
|
|
2316
|
-
for key in assetDic:
|
|
2317
|
-
aname = key + " / " + acType
|
|
2318
|
-
stackNames.append(aname)
|
|
2319
|
-
y2stack[aname] = np.zeros((count, self.N_n))
|
|
2320
|
-
y2stack[aname][i][:] = self.alpha_ijkn[i, acList.index(acType), assetDic[key], : self.N_n]
|
|
2321
|
-
|
|
2322
|
-
title = self._name + "\nAsset Allocation (%) - " + acType
|
|
2323
|
-
if self.ARCoord == "spouses":
|
|
2324
|
-
title += " spouses"
|
|
2325
|
-
else:
|
|
2326
|
-
title += " " + self.inames[i]
|
|
2327
|
-
|
|
2328
|
-
if tag != "":
|
|
2329
|
-
title += " - " + tag
|
|
2330
|
-
|
|
2331
|
-
fig, ax = plots.stack_plot(self.year_n, self.inames, title, [i],
|
|
2332
|
-
y2stack, stackNames, "upper left", "percent")
|
|
2333
|
-
figures.append(fig)
|
|
2334
|
-
|
|
2309
|
+
title = self._name + "\nAsset Allocation"
|
|
2310
|
+
if tag:
|
|
2311
|
+
title += " - " + tag
|
|
2312
|
+
figures = self._plotter.plot_allocations(self.year_n, self.inames, self.alpha_ijkn,
|
|
2313
|
+
self.ARCoord, title)
|
|
2335
2314
|
if figure:
|
|
2336
2315
|
return figures
|
|
2337
2316
|
|
|
2338
|
-
|
|
2317
|
+
for fig in figures:
|
|
2318
|
+
self._plotter.jupyter_renderer(fig)
|
|
2339
2319
|
return None
|
|
2340
2320
|
|
|
2341
2321
|
@_checkCaseStatus
|
|
@@ -2349,31 +2329,15 @@ class Plan(object):
|
|
|
2349
2329
|
the default behavior of setDefaultPlots().
|
|
2350
2330
|
"""
|
|
2351
2331
|
value = self._checkValue(value)
|
|
2352
|
-
|
|
2353
2332
|
title = self._name + "\nSavings Balance"
|
|
2354
|
-
if tag
|
|
2333
|
+
if tag:
|
|
2355
2334
|
title += " - " + tag
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
# Add one year for estate.
|
|
2359
|
-
year_n = np.append(self.year_n, [self.year_n[-1] + 1])
|
|
2360
|
-
|
|
2361
|
-
if value == "nominal":
|
|
2362
|
-
yformat = "\\$k (nominal)"
|
|
2363
|
-
savings_in = self.savings_in
|
|
2364
|
-
else:
|
|
2365
|
-
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2366
|
-
savings_in = {}
|
|
2367
|
-
for key in self.savings_in:
|
|
2368
|
-
savings_in[key] = self.savings_in[key] / self.gamma_n
|
|
2369
|
-
|
|
2370
|
-
fig, ax = plots.stack_plot(year_n, self.inames, title, range(self.N_i),
|
|
2371
|
-
savings_in, stypes, "upper left", yformat)
|
|
2372
|
-
|
|
2335
|
+
fig = self._plotter.plot_accounts(self.year_n, self.savings_in, self.gamma_n,
|
|
2336
|
+
value, title, self.inames)
|
|
2373
2337
|
if figure:
|
|
2374
2338
|
return fig
|
|
2375
2339
|
|
|
2376
|
-
|
|
2340
|
+
self._plotter.jupyter_renderer(fig)
|
|
2377
2341
|
return None
|
|
2378
2342
|
|
|
2379
2343
|
@_checkCaseStatus
|
|
@@ -2387,55 +2351,15 @@ class Plan(object):
|
|
|
2387
2351
|
the default behavior of setDefaultPlots().
|
|
2388
2352
|
"""
|
|
2389
2353
|
value = self._checkValue(value)
|
|
2390
|
-
|
|
2391
2354
|
title = self._name + "\nRaw Income Sources"
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
if tag != "":
|
|
2355
|
+
if tag:
|
|
2395
2356
|
title += " - " + tag
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
yformat = "\\$k (nominal)"
|
|
2399
|
-
sources_in = self.sources_in
|
|
2400
|
-
else:
|
|
2401
|
-
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2402
|
-
sources_in = {}
|
|
2403
|
-
for key in stypes:
|
|
2404
|
-
sources_in[key] = self.sources_in[key] / self.gamma_n[:-1]
|
|
2405
|
-
|
|
2406
|
-
fig, ax = plots.stack_plot(self.year_n, self.inames, title, range(self.N_i),
|
|
2407
|
-
sources_in, stypes, "upper left", yformat)
|
|
2408
|
-
|
|
2357
|
+
fig = self._plotter.plot_sources(self.year_n, self.sources_in, self.gamma_n,
|
|
2358
|
+
value, title, self.inames)
|
|
2409
2359
|
if figure:
|
|
2410
2360
|
return fig
|
|
2411
2361
|
|
|
2412
|
-
|
|
2413
|
-
return None
|
|
2414
|
-
|
|
2415
|
-
@_checkCaseStatus
|
|
2416
|
-
def _showFeff(self, tag=""):
|
|
2417
|
-
"""
|
|
2418
|
-
Plot income tax paid over time.
|
|
2419
|
-
|
|
2420
|
-
A tag string can be set to add information to the title of the plot.
|
|
2421
|
-
"""
|
|
2422
|
-
title = self._name + "\nEff f "
|
|
2423
|
-
if tag != "":
|
|
2424
|
-
title += " - " + tag
|
|
2425
|
-
|
|
2426
|
-
various = ["-", "--", "-.", ":"]
|
|
2427
|
-
style = {}
|
|
2428
|
-
series = {}
|
|
2429
|
-
q = 0
|
|
2430
|
-
for t in range(self.N_t):
|
|
2431
|
-
key = "f " + str(t)
|
|
2432
|
-
series[key] = self.F_tn[t] / self.DeltaBar_tn[t]
|
|
2433
|
-
style[key] = various[q % len(various)]
|
|
2434
|
-
q += 1
|
|
2435
|
-
|
|
2436
|
-
fig, ax = plots.line_income_plot(self.year_n, series, style, title, yformat="")
|
|
2437
|
-
|
|
2438
|
-
plt.show()
|
|
2362
|
+
self._plotter.jupyter_renderer(fig)
|
|
2439
2363
|
return None
|
|
2440
2364
|
|
|
2441
2365
|
@_checkCaseStatus
|
|
@@ -2449,87 +2373,17 @@ class Plan(object):
|
|
|
2449
2373
|
the default behavior of setDefaultPlots().
|
|
2450
2374
|
"""
|
|
2451
2375
|
value = self._checkValue(value)
|
|
2452
|
-
|
|
2453
|
-
style = {"income taxes": "-", "Medicare": "-."}
|
|
2454
|
-
|
|
2455
|
-
if value == "nominal":
|
|
2456
|
-
series = {"income taxes": self.T_n, "Medicare": self.M_n}
|
|
2457
|
-
yformat = "\\$k (nominal)"
|
|
2458
|
-
else:
|
|
2459
|
-
series = {
|
|
2460
|
-
"income taxes": self.T_n / self.gamma_n[:-1],
|
|
2461
|
-
"Medicare": self.M_n / self.gamma_n[:-1],
|
|
2462
|
-
}
|
|
2463
|
-
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2464
|
-
|
|
2465
2376
|
title = self._name + "\nIncome Tax"
|
|
2466
|
-
if tag
|
|
2467
|
-
title += " - " + tag
|
|
2468
|
-
|
|
2469
|
-
fig, ax = plots.line_income_plot(self.year_n, series, style, title, yformat)
|
|
2470
|
-
|
|
2471
|
-
if figure:
|
|
2472
|
-
return fig
|
|
2473
|
-
|
|
2474
|
-
plt.show()
|
|
2475
|
-
return None
|
|
2476
|
-
|
|
2477
|
-
@_checkCaseStatus
|
|
2478
|
-
def showGrossIncome(self, tag="", value=None, figure=False):
|
|
2479
|
-
"""
|
|
2480
|
-
Plot income tax and taxable income over time horizon.
|
|
2481
|
-
|
|
2482
|
-
A tag string can be set to add information to the title of the plot.
|
|
2483
|
-
|
|
2484
|
-
The value parameter can be set to *nominal* or *today*, overriding
|
|
2485
|
-
the default behavior of setDefaultPlots().
|
|
2486
|
-
"""
|
|
2487
|
-
value = self._checkValue(value)
|
|
2488
|
-
|
|
2489
|
-
style = {"taxable income": "-"}
|
|
2490
|
-
|
|
2491
|
-
if value == "nominal":
|
|
2492
|
-
series = {"taxable income": self.G_n}
|
|
2493
|
-
yformat = "\\$k (nominal)"
|
|
2494
|
-
infladjust = self.gamma_n[:-1]
|
|
2495
|
-
else:
|
|
2496
|
-
series = {"taxable income": self.G_n / self.gamma_n[:-1]}
|
|
2497
|
-
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2498
|
-
infladjust = 1
|
|
2499
|
-
|
|
2500
|
-
title = self._name + "\nTaxable Ordinary Income vs. Tax Brackets"
|
|
2501
|
-
if tag != "":
|
|
2377
|
+
if tag:
|
|
2502
2378
|
title += " - " + tag
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
data = tx.taxBrackets(self.N_i, self.n_d, self.N_n, self.yTCJA)
|
|
2507
|
-
for key in data:
|
|
2508
|
-
data_adj = data[key] * infladjust
|
|
2509
|
-
ax.plot(self.year_n, data_adj, label=key, ls=":")
|
|
2510
|
-
|
|
2511
|
-
plt.grid(visible="both")
|
|
2512
|
-
ax.legend(loc="upper left", reverse=True, fontsize=8, framealpha=0.3)
|
|
2513
|
-
|
|
2379
|
+
fig = self._plotter.plot_taxes(self.year_n, self.T_n, self.M_n, self.gamma_n,
|
|
2380
|
+
value, title, self.inames)
|
|
2514
2381
|
if figure:
|
|
2515
2382
|
return fig
|
|
2516
2383
|
|
|
2517
|
-
|
|
2384
|
+
self._plotter.jupyter_renderer(fig)
|
|
2518
2385
|
return None
|
|
2519
2386
|
|
|
2520
|
-
# @_checkCaseStatus
|
|
2521
|
-
def saveConfig(self, basename=None):
|
|
2522
|
-
"""
|
|
2523
|
-
Save parameters in a configuration file.
|
|
2524
|
-
"""
|
|
2525
|
-
if basename is None:
|
|
2526
|
-
basename = "case_" + self._name
|
|
2527
|
-
|
|
2528
|
-
config.saveConfig(self, basename, self.mylog)
|
|
2529
|
-
|
|
2530
|
-
return None
|
|
2531
|
-
|
|
2532
|
-
@_checkCaseStatus
|
|
2533
2387
|
def saveWorkbook(self, overwrite=False, *, basename=None, saveToFile=True):
|
|
2534
2388
|
"""
|
|
2535
2389
|
Save instance in an Excel spreadsheet.
|
|
@@ -2772,6 +2626,17 @@ class Plan(object):
|
|
|
2772
2626
|
|
|
2773
2627
|
return None
|
|
2774
2628
|
|
|
2629
|
+
def saveConfig(self, basename=None):
|
|
2630
|
+
"""
|
|
2631
|
+
Save parameters in a configuration file.
|
|
2632
|
+
"""
|
|
2633
|
+
if basename is None:
|
|
2634
|
+
basename = "case_" + self._name
|
|
2635
|
+
|
|
2636
|
+
config.saveConfig(self, basename, self.mylog)
|
|
2637
|
+
|
|
2638
|
+
return None
|
|
2639
|
+
|
|
2775
2640
|
|
|
2776
2641
|
def _saveWorkbook(wb, basename, overwrite, mylog):
|
|
2777
2642
|
"""
|