owlplanner 2025.2.8__py3-none-any.whl → 2025.2.9__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 +138 -133
- owlplanner/logging.py +13 -13
- owlplanner/plan.py +622 -619
- owlplanner/progress.py +2 -2
- owlplanner/rates.py +72 -72
- owlplanner/tax2025.py +3 -3
- owlplanner/timelists.py +31 -29
- owlplanner/utils.py +9 -9
- owlplanner/version.py +1 -1
- {owlplanner-2025.2.8.dist-info → owlplanner-2025.2.9.dist-info}/METADATA +1 -1
- owlplanner-2025.2.9.dist-info/RECORD +17 -0
- owlplanner-2025.2.8.dist-info/RECORD +0 -17
- {owlplanner-2025.2.8.dist-info → owlplanner-2025.2.9.dist-info}/WHEEL +0 -0
- {owlplanner-2025.2.8.dist-info → owlplanner-2025.2.9.dist-info}/licenses/LICENSE +0 -0
owlplanner/progress.py
CHANGED
|
@@ -11,10 +11,10 @@ class Progress(object):
|
|
|
11
11
|
self.mylog = mylog
|
|
12
12
|
|
|
13
13
|
def start(self):
|
|
14
|
-
self.mylog.print(
|
|
14
|
+
self.mylog.print("|--- progress ---|")
|
|
15
15
|
|
|
16
16
|
def show(self, x):
|
|
17
|
-
self.mylog.print(
|
|
17
|
+
self.mylog.print("\r\r%s" % u.pc(x, f=0), end="")
|
|
18
18
|
|
|
19
19
|
def finish(self):
|
|
20
20
|
self.mylog.print()
|
owlplanner/rates.py
CHANGED
|
@@ -43,31 +43,31 @@ from owlplanner import utils as u
|
|
|
43
43
|
FROM = 1928
|
|
44
44
|
TO = 2024
|
|
45
45
|
|
|
46
|
-
where = os.path.dirname(sys.modules[
|
|
47
|
-
file = os.path.join(where,
|
|
46
|
+
where = os.path.dirname(sys.modules["owlplanner"].__file__)
|
|
47
|
+
file = os.path.join(where, "data/rates.csv")
|
|
48
48
|
try:
|
|
49
49
|
df = pd.read_csv(file)
|
|
50
50
|
except Exception as e:
|
|
51
|
-
raise RuntimeError(f
|
|
51
|
+
raise RuntimeError(f"Could not find rates data file: {e}")
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
# Annual rate of return (%) of S&P 500 since 1928, including dividends.
|
|
55
|
-
SP500 = df[
|
|
55
|
+
SP500 = df["S&P 500"]
|
|
56
56
|
|
|
57
57
|
# Annual rate of return (%) of Baa Corporate Bonds since 1928.
|
|
58
|
-
BondsBaa = df[
|
|
58
|
+
BondsBaa = df["Bonds Baa"]
|
|
59
59
|
|
|
60
60
|
# Annual rate of return (%) of Aaa Corporate Bonds since 1928.
|
|
61
|
-
BondsAaa = df[
|
|
61
|
+
BondsAaa = df["Bonds Aaa"]
|
|
62
62
|
|
|
63
63
|
# Annual rate of return (%) for 10-y Treasury notes since 1928.
|
|
64
|
-
TNotes = df[
|
|
64
|
+
TNotes = df["TNotes"]
|
|
65
65
|
|
|
66
66
|
# Annual rates of return for 3-month Treasury bills since 1928.
|
|
67
|
-
TBills = df[
|
|
67
|
+
TBills = df["TBills"]
|
|
68
68
|
|
|
69
69
|
# Inflation rate as U.S. CPI index (%) since 1928.
|
|
70
|
-
Inflation = df[
|
|
70
|
+
Inflation = df["Inflation"]
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
def getRatesDistributions(frm, to, mylog=None):
|
|
@@ -87,10 +87,10 @@ def getRatesDistributions(frm, to, mylog=None):
|
|
|
87
87
|
assert frm <= to, '"from" must be smaller than "to".'
|
|
88
88
|
|
|
89
89
|
series = {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
"SP500": SP500,
|
|
91
|
+
"BondsBaa": BondsBaa,
|
|
92
|
+
"T. Notes": TNotes,
|
|
93
|
+
"Inflation": Inflation,
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
df = pd.DataFrame(series)
|
|
@@ -100,8 +100,8 @@ def getRatesDistributions(frm, to, mylog=None):
|
|
|
100
100
|
stdev = df.std()
|
|
101
101
|
covar = df.cov()
|
|
102
102
|
|
|
103
|
-
mylog.print(
|
|
104
|
-
mylog.print(
|
|
103
|
+
mylog.print("means: (%)\n", means)
|
|
104
|
+
mylog.print("standard deviation: (%)\n", stdev)
|
|
105
105
|
|
|
106
106
|
# Convert to NumPy array and from percent to decimal.
|
|
107
107
|
means = np.array(means) / 100.0
|
|
@@ -110,7 +110,7 @@ def getRatesDistributions(frm, to, mylog=None):
|
|
|
110
110
|
# Build correlation matrix by dividing by the stdev for each column and row.
|
|
111
111
|
corr = covar / stdev[:, None]
|
|
112
112
|
corr = corr.T / stdev[:, None]
|
|
113
|
-
mylog.print(
|
|
113
|
+
mylog.print("correlation matrix: \n\t\t%s" % str(corr).replace("\n", "\n\t\t"))
|
|
114
114
|
|
|
115
115
|
return means, stdev, corr, covar
|
|
116
116
|
|
|
@@ -121,9 +121,9 @@ def historicalValue(amount, year):
|
|
|
121
121
|
valued at the beginning of the year specified.
|
|
122
122
|
"""
|
|
123
123
|
thisyear = date.today().year
|
|
124
|
-
assert TO == thisyear - 1,
|
|
125
|
-
assert year >= FROM,
|
|
126
|
-
assert year <= thisyear,
|
|
124
|
+
assert TO == thisyear - 1, "Rates file needs to be updated to be current to %d." % thisyear
|
|
125
|
+
assert year >= FROM, "Only data from %d is available." % FROM
|
|
126
|
+
assert year <= thisyear, "Year must be < %d for historical data." % thisyear
|
|
127
127
|
|
|
128
128
|
span = thisyear - year
|
|
129
129
|
ub = len(Inflation)
|
|
@@ -173,7 +173,7 @@ class Rates(object):
|
|
|
173
173
|
self.to = TO
|
|
174
174
|
|
|
175
175
|
# Default values for rates.
|
|
176
|
-
self.setMethod(
|
|
176
|
+
self.setMethod("default")
|
|
177
177
|
|
|
178
178
|
def setMethod(self, method, frm=None, to=TO, values=None, stdev=None, corr=None):
|
|
179
179
|
"""
|
|
@@ -193,45 +193,45 @@ class Rates(object):
|
|
|
193
193
|
For 4 assets, this represents a list of 6 off-diagonal values.
|
|
194
194
|
"""
|
|
195
195
|
if method not in [
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
196
|
+
"default",
|
|
197
|
+
"optimistic",
|
|
198
|
+
"conservative",
|
|
199
|
+
"user",
|
|
200
|
+
"historical",
|
|
201
|
+
"historical average",
|
|
202
|
+
"mean",
|
|
203
|
+
"stochastic",
|
|
204
|
+
"histochastic",
|
|
205
205
|
]:
|
|
206
|
-
raise ValueError(
|
|
206
|
+
raise ValueError("Unknown rate selection method %s." % method)
|
|
207
207
|
|
|
208
208
|
Nk = len(self._defRates)
|
|
209
209
|
# First process fixed methods relying on values.
|
|
210
|
-
if method ==
|
|
210
|
+
if method == "default":
|
|
211
211
|
self.means = self._defRates
|
|
212
212
|
# self.mylog.vprint('Using default fixed rates values:', *[u.pc(k) for k in values])
|
|
213
213
|
self._setFixedRates(self._defRates)
|
|
214
|
-
elif method ==
|
|
214
|
+
elif method == "optimistic":
|
|
215
215
|
self.means = self._defRates
|
|
216
|
-
self.mylog.vprint(
|
|
216
|
+
self.mylog.vprint("Using optimistic fixed rates values:", *[u.pc(k) for k in self.means])
|
|
217
217
|
self._setFixedRates(self._optimisticRates)
|
|
218
|
-
elif method ==
|
|
218
|
+
elif method == "conservative":
|
|
219
219
|
self.means = self._conservRates
|
|
220
|
-
self.mylog.vprint(
|
|
220
|
+
self.mylog.vprint("Using conservative fixed rates values:", *[u.pc(k) for k in self.means])
|
|
221
221
|
self._setFixedRates(self._conservRates)
|
|
222
|
-
elif method ==
|
|
223
|
-
assert values is not None,
|
|
224
|
-
assert len(values) == Nk,
|
|
222
|
+
elif method == "user":
|
|
223
|
+
assert values is not None, "Fixed values must be provided with the user option."
|
|
224
|
+
assert len(values) == Nk, "values must have %d items." % Nk
|
|
225
225
|
self.means = np.array(values, dtype=float)
|
|
226
226
|
# Convert percent to decimal for storing.
|
|
227
227
|
self.means /= 100.0
|
|
228
|
-
self.mylog.vprint(
|
|
228
|
+
self.mylog.vprint("Setting rates using fixed user values:", *[u.pc(k) for k in self.means])
|
|
229
229
|
self._setFixedRates(self.means)
|
|
230
|
-
elif method ==
|
|
231
|
-
assert values is not None,
|
|
232
|
-
assert stdev is not None,
|
|
233
|
-
assert len(values) == Nk,
|
|
234
|
-
assert len(stdev) == Nk,
|
|
230
|
+
elif method == "stochastic":
|
|
231
|
+
assert values is not None, "Mean values must be provided with the stochastic option."
|
|
232
|
+
assert stdev is not None, "Standard deviations must be provided with the stochastic option."
|
|
233
|
+
assert len(values) == Nk, "values must have %d items." % Nk
|
|
234
|
+
assert len(stdev) == Nk, "stdev must have %d items." % Nk
|
|
235
235
|
self.means = np.array(values, dtype=float)
|
|
236
236
|
self.stdev = np.array(stdev, dtype=float)
|
|
237
237
|
# Convert percent to decimal for storing.
|
|
@@ -256,40 +256,40 @@ class Rates(object):
|
|
|
256
256
|
x += 1
|
|
257
257
|
corrarr = newcorr
|
|
258
258
|
else:
|
|
259
|
-
raise RuntimeError(
|
|
259
|
+
raise RuntimeError("Unable to process correlation shape of %s." % corrarr.shape)
|
|
260
260
|
|
|
261
261
|
self.corr = corrarr
|
|
262
|
-
assert np.array_equal(self.corr, self.corr.T),
|
|
262
|
+
assert np.array_equal(self.corr, self.corr.T), "Correlation matrix must be symmetric."
|
|
263
263
|
# Now build covariance matrix from stdev and correlation matrix.
|
|
264
264
|
# Multiply each row by a vector element-wise. Then columns.
|
|
265
265
|
covar = self.corr * self.stdev
|
|
266
266
|
self.covar = covar.T * self.stdev
|
|
267
267
|
self._rateMethod = self._stochRates
|
|
268
|
-
self.mylog.vprint(
|
|
269
|
-
self.mylog.vprint(
|
|
270
|
-
self.mylog.vprint(
|
|
268
|
+
self.mylog.vprint("Setting rates using stochastic method with means:", *[u.pc(k) for k in self.means])
|
|
269
|
+
self.mylog.vprint("\t standard deviations:", *[u.pc(k) for k in self.stdev])
|
|
270
|
+
self.mylog.vprint("\t and correlation matrix:\n\t\t", str(self.corr).replace("\n", "\n\t\t"))
|
|
271
271
|
else:
|
|
272
272
|
# Then methods relying on historical data range.
|
|
273
|
-
assert frm is not None,
|
|
273
|
+
assert frm is not None, "From year must be provided with this option."
|
|
274
274
|
assert FROM <= frm and frm <= TO, 'Lower range "frm=%d" out of bounds.' % frm
|
|
275
275
|
assert FROM <= to and to <= TO, 'Upper range "to=%d" out of bounds.' % to
|
|
276
|
-
assert frm < to,
|
|
276
|
+
assert frm < to, "Unacceptable range."
|
|
277
277
|
self.frm = frm
|
|
278
278
|
self.to = to
|
|
279
279
|
|
|
280
|
-
if method ==
|
|
281
|
-
self.mylog.vprint(
|
|
280
|
+
if method == "historical":
|
|
281
|
+
self.mylog.vprint("Using historical rates representing data from %d to %d." % (frm, to))
|
|
282
282
|
self._rateMethod = self._histRates
|
|
283
|
-
elif method ==
|
|
284
|
-
self.mylog.vprint(
|
|
283
|
+
elif method == "historical average" or method == "means":
|
|
284
|
+
self.mylog.vprint("Using average of rates from %d to %d." % (frm, to))
|
|
285
285
|
self.means, self.stdev, self.corr, self.covar = getRatesDistributions(frm, to, self.mylog)
|
|
286
286
|
self._setFixedRates(self.means)
|
|
287
|
-
elif method ==
|
|
288
|
-
self.mylog.vprint(
|
|
287
|
+
elif method == "histochastic":
|
|
288
|
+
self.mylog.vprint("Using histochastic rates derived from years %d to %d." % (frm, to))
|
|
289
289
|
self._rateMethod = self._stochRates
|
|
290
290
|
self.means, self.stdev, self.corr, self.covar = getRatesDistributions(frm, to, self.mylog)
|
|
291
291
|
else:
|
|
292
|
-
raise ValueError(
|
|
292
|
+
raise ValueError("Method $s not supported." % method)
|
|
293
293
|
|
|
294
294
|
self.method = method
|
|
295
295
|
|
|
@@ -297,7 +297,7 @@ class Rates(object):
|
|
|
297
297
|
|
|
298
298
|
def _setFixedRates(self, rates):
|
|
299
299
|
Nk = len(self._defRates)
|
|
300
|
-
assert len(rates) == Nk,
|
|
300
|
+
assert len(rates) == Nk, "Rate list provided must have %d entries." % Nk
|
|
301
301
|
self._myRates = np.array(rates)
|
|
302
302
|
self._rateMethod = self._fixedRates
|
|
303
303
|
|
|
@@ -369,7 +369,7 @@ def showRatesDistributions(frm=FROM, to=TO):
|
|
|
369
369
|
"""
|
|
370
370
|
import matplotlib.pyplot as plt
|
|
371
371
|
|
|
372
|
-
title =
|
|
372
|
+
title = "Rates from " + str(frm) + " to " + str(to)
|
|
373
373
|
# Bring year values to indices.
|
|
374
374
|
frm -= FROM
|
|
375
375
|
to -= FROM
|
|
@@ -383,25 +383,25 @@ def showRatesDistributions(frm=FROM, to=TO):
|
|
|
383
383
|
dat3 = np.array(Inflation[frm:to])
|
|
384
384
|
|
|
385
385
|
fig.suptitle(title)
|
|
386
|
-
ax[0].set_title(
|
|
387
|
-
label =
|
|
386
|
+
ax[0].set_title("S&P500")
|
|
387
|
+
label = "<>: " + u.pc(np.mean(dat0), 2, 1)
|
|
388
388
|
ax[0].hist(dat0, bins=nbins, label=label)
|
|
389
|
-
ax[0].legend(loc=
|
|
389
|
+
ax[0].legend(loc="upper left", fontsize=8, framealpha=0.7)
|
|
390
390
|
|
|
391
|
-
ax[1].set_title(
|
|
392
|
-
label =
|
|
391
|
+
ax[1].set_title("BondsBaa")
|
|
392
|
+
label = "<>: " + u.pc(np.mean(dat1), 2, 1)
|
|
393
393
|
ax[1].hist(dat1, bins=nbins, label=label)
|
|
394
|
-
ax[1].legend(loc=
|
|
394
|
+
ax[1].legend(loc="upper left", fontsize=8, framealpha=0.7)
|
|
395
395
|
|
|
396
|
-
ax[2].set_title(
|
|
397
|
-
label =
|
|
396
|
+
ax[2].set_title("TNotes")
|
|
397
|
+
label = "<>: " + u.pc(np.mean(dat2), 2, 1)
|
|
398
398
|
ax[2].hist(dat1, bins=nbins, label=label)
|
|
399
|
-
ax[2].legend(loc=
|
|
399
|
+
ax[2].legend(loc="upper left", fontsize=8, framealpha=0.7)
|
|
400
400
|
|
|
401
|
-
ax[3].set_title(
|
|
402
|
-
label =
|
|
401
|
+
ax[3].set_title("Inflation")
|
|
402
|
+
label = "<>: " + u.pc(np.mean(dat3), 2, 1)
|
|
403
403
|
ax[3].hist(dat3, bins=nbins, label=label)
|
|
404
|
-
ax[3].legend(loc=
|
|
404
|
+
ax[3].legend(loc="upper left", fontsize=8, framealpha=0.7)
|
|
405
405
|
|
|
406
406
|
plt.show()
|
|
407
407
|
|
owlplanner/tax2025.py
CHANGED
|
@@ -22,7 +22,7 @@ from datetime import date
|
|
|
22
22
|
##############################################################################
|
|
23
23
|
# Prepare the data.
|
|
24
24
|
|
|
25
|
-
taxBracketNames = [
|
|
25
|
+
taxBracketNames = ["10%", "12/15%", "22/25%", "24/28%", "32/33%", "35%", "37/40%"]
|
|
26
26
|
|
|
27
27
|
rates_2025 = np.array([0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.370])
|
|
28
28
|
rates_2026 = np.array([0.10, 0.15, 0.25, 0.28, 0.33, 0.35, 0.396])
|
|
@@ -154,7 +154,7 @@ def taxBrackets(N_i, n_d, N_n):
|
|
|
154
154
|
Return dictionary containing future tax brackets
|
|
155
155
|
unadjusted for inflation for plotting.
|
|
156
156
|
"""
|
|
157
|
-
assert 0 < N_i and N_i <= 2,
|
|
157
|
+
assert 0 < N_i and N_i <= 2, "Cannot process %d individuals." % N_i
|
|
158
158
|
# This 1 is the number of years left in TCJA from 2025.
|
|
159
159
|
ytc = 1
|
|
160
160
|
status = N_i - 1
|
|
@@ -218,7 +218,7 @@ def rho_in(yobs, N_n):
|
|
|
218
218
|
|
|
219
219
|
N_i = len(yobs)
|
|
220
220
|
if N_i == 2 and abs(yobs[0] - yobs[1]) > 10:
|
|
221
|
-
raise RuntimeError(
|
|
221
|
+
raise RuntimeError("RMD: Unsupported age difference of more than 10 years.")
|
|
222
222
|
|
|
223
223
|
rho = np.zeros((N_i, N_n))
|
|
224
224
|
thisyear = date.today().year
|
owlplanner/timelists.py
CHANGED
|
@@ -21,15 +21,15 @@ import pandas as pd
|
|
|
21
21
|
|
|
22
22
|
# Expected headers in each excel sheet, one per individual.
|
|
23
23
|
timeHorizonItems = [
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
"year",
|
|
25
|
+
"anticipated wages",
|
|
26
|
+
"taxable ctrb",
|
|
27
|
+
"401k ctrb",
|
|
28
|
+
"Roth 401k ctrb",
|
|
29
|
+
"IRA ctrb",
|
|
30
|
+
"Roth IRA ctrb",
|
|
31
|
+
"Roth conv",
|
|
32
|
+
"big-ticket items",
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
|
|
@@ -44,18 +44,18 @@ def read(finput, inames, horizons, mylog):
|
|
|
44
44
|
Returs a dictionary of dataframes by individual's names.
|
|
45
45
|
"""
|
|
46
46
|
|
|
47
|
-
mylog.vprint(
|
|
47
|
+
mylog.vprint("Reading wages, contributions, conversions, and big-ticket items over time...")
|
|
48
48
|
|
|
49
49
|
if isinstance(finput, dict):
|
|
50
50
|
timeLists = finput
|
|
51
|
-
finput =
|
|
52
|
-
streamName =
|
|
51
|
+
finput = "dictionary of DataFrames"
|
|
52
|
+
streamName = "dictionary of DataFrames"
|
|
53
53
|
else:
|
|
54
54
|
# Read all worksheets in memory but only process those with proper names.
|
|
55
55
|
try:
|
|
56
56
|
dfDict = pd.read_excel(finput, sheet_name=None)
|
|
57
57
|
except Exception as e:
|
|
58
|
-
raise Exception(
|
|
58
|
+
raise Exception("Could not read file %r: %s." % (finput, e))
|
|
59
59
|
streamName = "file '%s'" % finput
|
|
60
60
|
|
|
61
61
|
timeLists = condition(dfDict, inames, horizons, mylog)
|
|
@@ -80,9 +80,9 @@ def condition(dfDict, inames, horizons, mylog):
|
|
|
80
80
|
|
|
81
81
|
df = dfDict[iname]
|
|
82
82
|
|
|
83
|
-
df = df.loc[:, ~df.columns.str.contains(
|
|
83
|
+
df = df.loc[:, ~df.columns.str.contains("^Unnamed")]
|
|
84
84
|
for col in df.columns:
|
|
85
|
-
if col ==
|
|
85
|
+
if col == "" or col not in timeHorizonItems:
|
|
86
86
|
df.drop(col, axis=1)
|
|
87
87
|
|
|
88
88
|
for item in timeHorizonItems:
|
|
@@ -90,34 +90,36 @@ def condition(dfDict, inames, horizons, mylog):
|
|
|
90
90
|
raise ValueError(f"Item {item} not found for {iname}.")
|
|
91
91
|
|
|
92
92
|
# Only consider lines in proper year range.
|
|
93
|
-
df = df[df[
|
|
94
|
-
df = df[df[
|
|
93
|
+
df = df[df["year"] >= thisyear]
|
|
94
|
+
df = df[df["year"] < endyear]
|
|
95
95
|
missing = []
|
|
96
96
|
for n in range(horizons[i]):
|
|
97
97
|
year = thisyear + n
|
|
98
|
-
if not (df[df[
|
|
98
|
+
if not (df[df["year"] == year]).any(axis=None):
|
|
99
99
|
df.loc[len(df)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
|
|
100
100
|
missing.append(year)
|
|
101
101
|
else:
|
|
102
102
|
for item in timeHorizonItems:
|
|
103
|
-
if item !=
|
|
104
|
-
raise ValueError(
|
|
105
|
-
% (item, iname, df['year'].iloc[n])
|
|
106
|
-
)
|
|
103
|
+
if item != "big-ticket items" and df[item].iloc[n] < 0:
|
|
104
|
+
raise ValueError("Item %s for %s in year %d is < 0." % (item, iname, df["year"].iloc[n]))
|
|
107
105
|
|
|
108
106
|
if len(missing) > 0:
|
|
109
|
-
mylog.vprint(
|
|
107
|
+
mylog.vprint("Adding %d missing years for %s: %r." % (len(missing), iname, missing))
|
|
110
108
|
|
|
111
|
-
df.sort_values(
|
|
109
|
+
df.sort_values("year", inplace=True)
|
|
112
110
|
# Replace empty (NaN) cells with 0 value.
|
|
113
111
|
df.fillna(0, inplace=True)
|
|
114
112
|
|
|
115
113
|
timeLists[iname] = df
|
|
116
114
|
|
|
117
|
-
if df[
|
|
118
|
-
raise ValueError(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
if df["year"].iloc[-1] != endyear - 1:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
"Time horizon for",
|
|
118
|
+
iname,
|
|
119
|
+
"is too short.\n\tIt should end in",
|
|
120
|
+
endyear,
|
|
121
|
+
"but ends in",
|
|
122
|
+
df["year"].iloc[-1],
|
|
123
|
+
)
|
|
122
124
|
|
|
123
125
|
return timeLists
|
owlplanner/utils.py
CHANGED
|
@@ -20,12 +20,12 @@ def d(value, f=0, latex=False) -> str:
|
|
|
20
20
|
Number of decimals controlled by `f` which defaults to 0.
|
|
21
21
|
"""
|
|
22
22
|
if np.isnan(value):
|
|
23
|
-
return
|
|
23
|
+
return "NaN"
|
|
24
24
|
|
|
25
25
|
if latex:
|
|
26
|
-
mystr =
|
|
26
|
+
mystr = "\\${:,." + str(f) + "f}"
|
|
27
27
|
else:
|
|
28
|
-
mystr =
|
|
28
|
+
mystr = "${:,." + str(f) + "f}"
|
|
29
29
|
|
|
30
30
|
return mystr.format(value)
|
|
31
31
|
|
|
@@ -35,7 +35,7 @@ def pc(value, f=1, mul=100) -> str:
|
|
|
35
35
|
Return a string formatting decimal value in percent.
|
|
36
36
|
Number of decimals of percent controlled by `f` which defaults to 1.
|
|
37
37
|
"""
|
|
38
|
-
mystr =
|
|
38
|
+
mystr = "{:." + str(f) + "f}%"
|
|
39
39
|
|
|
40
40
|
return mystr.format(mul * value)
|
|
41
41
|
|
|
@@ -58,14 +58,14 @@ def getUnits(units) -> int:
|
|
|
58
58
|
Translate multiplication factor for units as expressed by an abbreviation
|
|
59
59
|
expressed in a string. Returns an integer.
|
|
60
60
|
"""
|
|
61
|
-
if units is None or units == 1 or units ==
|
|
61
|
+
if units is None or units == 1 or units == "1" or units == "one":
|
|
62
62
|
fac = 1
|
|
63
|
-
elif units in {
|
|
63
|
+
elif units in {"k", "K"}:
|
|
64
64
|
fac = 1000
|
|
65
|
-
elif units in {
|
|
65
|
+
elif units in {"m", "M"}:
|
|
66
66
|
fac = 1000000
|
|
67
67
|
else:
|
|
68
|
-
raise ValueError(
|
|
68
|
+
raise ValueError("Unknown units %r." % units)
|
|
69
69
|
|
|
70
70
|
return fac
|
|
71
71
|
|
|
@@ -87,7 +87,7 @@ def roundCents(values, decimals=2):
|
|
|
87
87
|
"""
|
|
88
88
|
multiplier = 10**decimals
|
|
89
89
|
|
|
90
|
-
newvalues = values * multiplier + 0.5*np.sign(values)
|
|
90
|
+
newvalues = values * multiplier + 0.5 * np.sign(values)
|
|
91
91
|
|
|
92
92
|
arr = np.fix(newvalues) / multiplier
|
|
93
93
|
# Remove negative zero-like values.
|
owlplanner/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2025.02.
|
|
1
|
+
__version__ = "2025.02.09"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
owlplanner/__init__.py,sha256=QqrdT0Qks20osBTg7h0vJHAxpP9lL7DA99xb0nYbtw4,254
|
|
2
|
+
owlplanner/abcapi.py,sha256=J2xlj6LXwIblot0g4UeO5BC-vIYXsrI72duH82H6Di8,6658
|
|
3
|
+
owlplanner/config.py,sha256=tKqqDxzygC283UPyZwa47oq0PBH0uEv_1bwiwAdRm4Q,11731
|
|
4
|
+
owlplanner/logging.py,sha256=0y6Mhj56vmilgnDvw0pA8ur15oz5O3IN7qNCaf8yUIk,2532
|
|
5
|
+
owlplanner/plan.py,sha256=5biGxkIfilBHOfoROpDs7m_BmzywJ6qldm_8aGAXbXU,114092
|
|
6
|
+
owlplanner/progress.py,sha256=uyCUmuvZzvg2UPonjmXmUsWnnP55Mm6n19Xg3WcjwHU,386
|
|
7
|
+
owlplanner/rates.py,sha256=CRQBaAXIB7hkxkLyDxxptBGuwnXwoIvK9_j4qTAc23Y,15627
|
|
8
|
+
owlplanner/tax2025.py,sha256=VNRo1CiskHLEY_m5-wPVBVK-eFoFIaDF2lFRW-bHdc4,7016
|
|
9
|
+
owlplanner/timelists.py,sha256=NgyPifalMBLfKi-2mtkAems4DnOJSkPtPPXR7LmbGTU,4052
|
|
10
|
+
owlplanner/utils.py,sha256=Wy3ngYuVmWB2CeJx-v6vdhlSeSszQTjtuPtbc7hTb_A,2353
|
|
11
|
+
owlplanner/version.py,sha256=WQPMjK8ZbcCw8owMuvICW0qcZSL2-1yIsSwBODMumNs,28
|
|
12
|
+
owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
|
|
14
|
+
owlplanner-2025.2.9.dist-info/METADATA,sha256=cxhHp7ve0Rpmdr2pChd-y21DmNOejUweN_x8tR7N9K8,63946
|
|
15
|
+
owlplanner-2025.2.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
16
|
+
owlplanner-2025.2.9.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
17
|
+
owlplanner-2025.2.9.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
owlplanner/__init__.py,sha256=QqrdT0Qks20osBTg7h0vJHAxpP9lL7DA99xb0nYbtw4,254
|
|
2
|
-
owlplanner/abcapi.py,sha256=eemIsdbtzdWCIj5VuuswgphxXMcxJ_GZfUlDi6lttFM,6658
|
|
3
|
-
owlplanner/config.py,sha256=1gofMnUUyOy4KXSWSPEij5yUKjvPu6kiPXcS_Nii4Rg,12220
|
|
4
|
-
owlplanner/logging.py,sha256=pXg_mMgBll-kklqaDRLDNVUFo-5DAa-yqTKtiVrhNWw,2530
|
|
5
|
-
owlplanner/plan.py,sha256=UBmOPVfpHPUZUo8Th6sROwTsCUi6PBuv11TD2x3XK70,114255
|
|
6
|
-
owlplanner/progress.py,sha256=YZjL5_m4MMgKPlWlhhKacPLt54tVhVGF1eXxxZapMYs,386
|
|
7
|
-
owlplanner/rates.py,sha256=aKOmau8i3uqxZGi7HQJpzooT3X-yAZhga5MZJ56pBzk,15627
|
|
8
|
-
owlplanner/tax2025.py,sha256=b2RgM6TBQa8ggo6ODyh0p_J7j79UUm8z5NiENqa1l_k,7016
|
|
9
|
-
owlplanner/timelists.py,sha256=WwymsYAGWcrEzMtc-wrLbn1EVA2fhqXGN4NHLJsH3Fs,4110
|
|
10
|
-
owlplanner/utils.py,sha256=adIwqGVQFfvekke0JCxYJD3PKHbptVCj3NrQT2TQIB4,2351
|
|
11
|
-
owlplanner/version.py,sha256=Voi7cBnkUuxoqJIUjmHB47rz-PDQTzK8NO_H0JraUMA,28
|
|
12
|
-
owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
|
|
14
|
-
owlplanner-2025.2.8.dist-info/METADATA,sha256=c8WdLd8PAtMZtmRULpn-Q3BrV7edwrnpXCsRrbLbiz8,63946
|
|
15
|
-
owlplanner-2025.2.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
16
|
-
owlplanner-2025.2.8.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
17
|
-
owlplanner-2025.2.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|