owlplanner 2026.1.26__py3-none-any.whl → 2026.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- owlplanner/config.py +17 -3
- owlplanner/plan.py +188 -84
- owlplanner/rates.py +377 -366
- owlplanner/tax2026.py +11 -9
- owlplanner/version.py +1 -1
- {owlplanner-2026.1.26.dist-info → owlplanner-2026.2.2.dist-info}/METADATA +2 -2
- {owlplanner-2026.1.26.dist-info → owlplanner-2026.2.2.dist-info}/RECORD +11 -12
- owlplanner/In Discussion #58, the case of Kim and Sam.md +0 -307
- {owlplanner-2026.1.26.dist-info → owlplanner-2026.2.2.dist-info}/WHEEL +0 -0
- {owlplanner-2026.1.26.dist-info → owlplanner-2026.2.2.dist-info}/entry_points.txt +0 -0
- {owlplanner-2026.1.26.dist-info → owlplanner-2026.2.2.dist-info}/licenses/AUTHORS +0 -0
- {owlplanner-2026.1.26.dist-info → owlplanner-2026.2.2.dist-info}/licenses/LICENSE +0 -0
owlplanner/rates.py
CHANGED
|
@@ -1,366 +1,377 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Historical and statistical rate of return data for asset classes.
|
|
3
|
-
|
|
4
|
-
This module provides historical annual rates of return for different asset
|
|
5
|
-
classes: S&P500, Baa corporate bonds, real estate, 3-mo T-Bills, 10-year Treasury
|
|
6
|
-
notes, and inflation as measured by CPI from 1928 to present. Values were
|
|
7
|
-
extracted from NYU's Stern School of business historical returns data.
|
|
8
|
-
|
|
9
|
-
Copyright (C) 2025-2026 The Owlplanner Authors
|
|
10
|
-
|
|
11
|
-
This program is free software: you can redistribute it and/or modify
|
|
12
|
-
it under the terms of the GNU General Public License as published by
|
|
13
|
-
the Free Software Foundation, either version 3 of the License, or
|
|
14
|
-
(at your option) any later version.
|
|
15
|
-
|
|
16
|
-
This program is distributed in the hope that it will be useful,
|
|
17
|
-
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
18
|
-
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
19
|
-
GNU General Public License for more details.
|
|
20
|
-
|
|
21
|
-
You should have received a copy of the GNU General Public License
|
|
22
|
-
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
###################################################################
|
|
26
|
-
import numpy as np
|
|
27
|
-
import pandas as pd
|
|
28
|
-
import os
|
|
29
|
-
import sys
|
|
30
|
-
|
|
31
|
-
from owlplanner import mylogging as log
|
|
32
|
-
from owlplanner import utils as u
|
|
33
|
-
|
|
34
|
-
# All data goes from 1928 to 2025. Update the TO value when data
|
|
35
|
-
# becomes available for subsequent years.
|
|
36
|
-
FROM = 1928
|
|
37
|
-
TO = 2025
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
#
|
|
156
|
-
self.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
self.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
self.
|
|
165
|
-
|
|
166
|
-
#
|
|
167
|
-
self.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
self.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
self.
|
|
222
|
-
self.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
self.
|
|
233
|
-
self.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
self.
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
#
|
|
350
|
-
return
|
|
351
|
-
|
|
352
|
-
def
|
|
353
|
-
"""
|
|
354
|
-
Return an array of 4 values representing the historical rates
|
|
355
|
-
of stock, Corporate Baa bonds, Treasury notes, and inflation,
|
|
356
|
-
respectively.
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
1
|
+
"""
|
|
2
|
+
Historical and statistical rate of return data for asset classes.
|
|
3
|
+
|
|
4
|
+
This module provides historical annual rates of return for different asset
|
|
5
|
+
classes: S&P500, Baa corporate bonds, real estate, 3-mo T-Bills, 10-year Treasury
|
|
6
|
+
notes, and inflation as measured by CPI from 1928 to present. Values were
|
|
7
|
+
extracted from NYU's Stern School of business historical returns data.
|
|
8
|
+
|
|
9
|
+
Copyright (C) 2025-2026 The Owlplanner Authors
|
|
10
|
+
|
|
11
|
+
This program is free software: you can redistribute it and/or modify
|
|
12
|
+
it under the terms of the GNU General Public License as published by
|
|
13
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
14
|
+
(at your option) any later version.
|
|
15
|
+
|
|
16
|
+
This program is distributed in the hope that it will be useful,
|
|
17
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
18
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
19
|
+
GNU General Public License for more details.
|
|
20
|
+
|
|
21
|
+
You should have received a copy of the GNU General Public License
|
|
22
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
###################################################################
|
|
26
|
+
import numpy as np
|
|
27
|
+
import pandas as pd
|
|
28
|
+
import os
|
|
29
|
+
import sys
|
|
30
|
+
|
|
31
|
+
from owlplanner import mylogging as log
|
|
32
|
+
from owlplanner import utils as u
|
|
33
|
+
|
|
34
|
+
# All data goes from 1928 to 2025. Update the TO value when data
|
|
35
|
+
# becomes available for subsequent years.
|
|
36
|
+
FROM = 1928
|
|
37
|
+
TO = 2025
|
|
38
|
+
|
|
39
|
+
# Rate methods that use the same rate every year (reverse/roll are no-ops).
|
|
40
|
+
CONSTANT_RATE_METHODS = (
|
|
41
|
+
"default", "optimistic", "conservative", "user",
|
|
42
|
+
"historical average", "mean",
|
|
43
|
+
)
|
|
44
|
+
# Rate methods that produce deterministic series (no regeneration needed).
|
|
45
|
+
RATE_METHODS_NO_REGEN = (
|
|
46
|
+
"default", "optimistic", "conservative", "user",
|
|
47
|
+
"historical average", "historical",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
where = os.path.dirname(sys.modules["owlplanner"].__file__)
|
|
51
|
+
file = os.path.join(where, "data/rates.csv")
|
|
52
|
+
try:
|
|
53
|
+
df = pd.read_csv(file)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
raise RuntimeError(f"Could not find rates data file: {e}") from e
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Annual rate of return (%) of S&P 500 since 1928, including dividends.
|
|
59
|
+
SP500 = df["S&P 500"]
|
|
60
|
+
|
|
61
|
+
# Annual rate of return (%) of Baa Corporate Bonds since 1928.
|
|
62
|
+
BondsBaa = df["Bonds Baa"]
|
|
63
|
+
|
|
64
|
+
# Annual rate of return (%) of Real Estate since 1928.
|
|
65
|
+
RealEstate = df["real estate"]
|
|
66
|
+
|
|
67
|
+
# Annual rate of return (%) for 10-y Treasury notes since 1928.
|
|
68
|
+
TNotes = df["TNotes"]
|
|
69
|
+
|
|
70
|
+
# Annual rates of return for 3-month Treasury bills since 1928.
|
|
71
|
+
TBills = df["TBills"]
|
|
72
|
+
|
|
73
|
+
# Inflation rate as U.S. CPI index (%) since 1928.
|
|
74
|
+
Inflation = df["Inflation"]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def getRatesDistributions(frm, to, mylog=None):
|
|
78
|
+
"""
|
|
79
|
+
Pre-compute normal distribution parameters for the series above.
|
|
80
|
+
This calculation takes into account the correlations between
|
|
81
|
+
the different rates. Function returns means and covariance matrix.
|
|
82
|
+
"""
|
|
83
|
+
if mylog is None:
|
|
84
|
+
mylog = log.Logger()
|
|
85
|
+
|
|
86
|
+
# Convert years to index and check range.
|
|
87
|
+
frm -= FROM
|
|
88
|
+
to -= FROM
|
|
89
|
+
if not (0 <= frm and frm <= len(SP500)):
|
|
90
|
+
raise ValueError(f"Range 'from' {frm} out of bounds.")
|
|
91
|
+
if not (0 <= to and to <= len(SP500)):
|
|
92
|
+
raise ValueError(f"Range 'to' {to} out of bounds.")
|
|
93
|
+
if frm >= to:
|
|
94
|
+
raise ValueError(f'"from" {frm} must be smaller than "to" {to}.')
|
|
95
|
+
|
|
96
|
+
series = {
|
|
97
|
+
"SP500": SP500,
|
|
98
|
+
"BondsBaa": BondsBaa,
|
|
99
|
+
"T. Notes": TNotes,
|
|
100
|
+
"Inflation": Inflation,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
df = pd.DataFrame(series)
|
|
104
|
+
df = df.truncate(before=frm, after=to)
|
|
105
|
+
|
|
106
|
+
means = df.mean()
|
|
107
|
+
stdev = df.std()
|
|
108
|
+
covar = df.cov()
|
|
109
|
+
|
|
110
|
+
mylog.vprint("means: (%)\n", means)
|
|
111
|
+
mylog.vprint("standard deviation: (%)\n", stdev)
|
|
112
|
+
|
|
113
|
+
# Convert to NumPy array and from percent to decimal.
|
|
114
|
+
means = np.array(means) / 100.0
|
|
115
|
+
stdev = np.array(stdev) / 100.0
|
|
116
|
+
covar = np.array(covar) / 10000.0
|
|
117
|
+
# Build correlation matrix by dividing by the stdev for each column and row.
|
|
118
|
+
corr = covar / stdev[:, None]
|
|
119
|
+
corr = corr.T / stdev[:, None]
|
|
120
|
+
# Fold round-off errors in proper bounds.
|
|
121
|
+
corr[corr > 1] = 1
|
|
122
|
+
corr[corr < -1] = -1
|
|
123
|
+
mylog.vprint("correlation matrix: \n\t\t%s" % str(corr).replace("\n", "\n\t\t"))
|
|
124
|
+
|
|
125
|
+
return means, stdev, corr, covar
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class Rates(object):
|
|
129
|
+
"""
|
|
130
|
+
Rates are stored in a 4-array in the following order:
|
|
131
|
+
Stocks, Bonds, Fixed assets, and Inflation.
|
|
132
|
+
Rate are stored in decimal, but the API is in percent.
|
|
133
|
+
|
|
134
|
+
To use this class first build an object:
|
|
135
|
+
``r = Rates()``
|
|
136
|
+
then ``r.setMethod(...)``
|
|
137
|
+
then ``mySeries = r.genSeries()``
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(self, mylog=None, seed=None):
|
|
141
|
+
"""
|
|
142
|
+
Default constructor.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
mylog: Logger instance (optional)
|
|
146
|
+
seed: Random seed for reproducible stochastic rates (optional)
|
|
147
|
+
"""
|
|
148
|
+
if mylog is None:
|
|
149
|
+
self.mylog = log.Logger()
|
|
150
|
+
else:
|
|
151
|
+
self.mylog = mylog
|
|
152
|
+
|
|
153
|
+
# Store seed for stochastic rate generation
|
|
154
|
+
# Always use a Generator instance for thread safety and modern API
|
|
155
|
+
# If seed is None, default_rng() will use entropy/current time
|
|
156
|
+
self._seed = seed
|
|
157
|
+
self._rng = np.random.default_rng(seed)
|
|
158
|
+
|
|
159
|
+
# Default rates are average over last 30 years.
|
|
160
|
+
self._defRates = np.array([0.1101, 0.0736, 0.0503, 0.0251])
|
|
161
|
+
|
|
162
|
+
# Realistic rates are average predictions of major firms
|
|
163
|
+
# as reported by MorningStar in 2023.
|
|
164
|
+
self._optimisticRates = np.array([0.086, 0.049, 0.033, 0.025])
|
|
165
|
+
|
|
166
|
+
# Conservative rates.
|
|
167
|
+
self._conservRates = np.array([0.06, 0.04, 0.033, 0.028])
|
|
168
|
+
|
|
169
|
+
self.means = np.zeros((4))
|
|
170
|
+
self.stdev = np.zeros((4))
|
|
171
|
+
self.corr = np.zeros((4, 4))
|
|
172
|
+
self.covar = np.zeros((4, 4))
|
|
173
|
+
|
|
174
|
+
self.frm = FROM
|
|
175
|
+
self.to = TO
|
|
176
|
+
|
|
177
|
+
# Default values for rates.
|
|
178
|
+
self.setMethod("default")
|
|
179
|
+
|
|
180
|
+
def setMethod(self, method, frm=None, to=TO, values=None, stdev=None, corr=None):
|
|
181
|
+
"""
|
|
182
|
+
Select the method to generate the annual rates of return
|
|
183
|
+
for the different classes of assets. Different methods include:
|
|
184
|
+
- default: average over last 30 years.
|
|
185
|
+
- optimistic: predictions from various firms reported by MorningStar.
|
|
186
|
+
- conservative: conservative values.
|
|
187
|
+
- user: user-selected fixed rates.
|
|
188
|
+
- historical: historical rates from 1928 to last year.
|
|
189
|
+
- historical average or means: average over historical data.
|
|
190
|
+
- histochastic: randomly generated from the statistical properties of a historical range.
|
|
191
|
+
- stochastic: randomly generated from means, standard deviation and optionally a correlation matrix.
|
|
192
|
+
The correlation matrix can be provided as a full matrix or
|
|
193
|
+
by only specifying the off-diagonal elements as a simple list
|
|
194
|
+
of (Nk*Nk - Nk)/2 values for Nk assets.
|
|
195
|
+
For 4 assets, this represents a list of 6 off-diagonal values.
|
|
196
|
+
"""
|
|
197
|
+
if method not in [
|
|
198
|
+
"default",
|
|
199
|
+
"optimistic",
|
|
200
|
+
"conservative",
|
|
201
|
+
"user",
|
|
202
|
+
"historical",
|
|
203
|
+
"historical average",
|
|
204
|
+
"mean",
|
|
205
|
+
"stochastic",
|
|
206
|
+
"histochastic",
|
|
207
|
+
]:
|
|
208
|
+
raise ValueError(f"Unknown rate selection method {method}.")
|
|
209
|
+
|
|
210
|
+
Nk = len(self._defRates)
|
|
211
|
+
# First process fixed methods relying on values.
|
|
212
|
+
if method == "default":
|
|
213
|
+
self.means = self._defRates
|
|
214
|
+
# self.mylog.vprint('Using default fixed rates values:', *[u.pc(k) for k in values])
|
|
215
|
+
self._setFixedRates(self._defRates)
|
|
216
|
+
elif method == "optimistic":
|
|
217
|
+
self.means = self._defRates
|
|
218
|
+
self.mylog.vprint("Using optimistic fixed rates values:", *[u.pc(k) for k in self.means])
|
|
219
|
+
self._setFixedRates(self._optimisticRates)
|
|
220
|
+
elif method == "conservative":
|
|
221
|
+
self.means = self._conservRates
|
|
222
|
+
self.mylog.vprint("Using conservative fixed rates values:", *[u.pc(k) for k in self.means])
|
|
223
|
+
self._setFixedRates(self._conservRates)
|
|
224
|
+
elif method == "user":
|
|
225
|
+
if values is None:
|
|
226
|
+
raise ValueError("Fixed values must be provided with the user option.")
|
|
227
|
+
if len(values) != Nk:
|
|
228
|
+
raise ValueError(f"Values must have {Nk} items.")
|
|
229
|
+
self.means = np.array(values, dtype=float)
|
|
230
|
+
# Convert percent to decimal for storing.
|
|
231
|
+
self.means /= 100.0
|
|
232
|
+
self.mylog.vprint("Setting rates using fixed user values:", *[u.pc(k) for k in self.means])
|
|
233
|
+
self._setFixedRates(self.means)
|
|
234
|
+
elif method == "stochastic":
|
|
235
|
+
if values is None:
|
|
236
|
+
raise ValueError("Mean values must be provided with the stochastic option.")
|
|
237
|
+
if stdev is None:
|
|
238
|
+
raise ValueError("Standard deviations must be provided with the stochastic option.")
|
|
239
|
+
if len(values) != Nk:
|
|
240
|
+
raise ValueError(f"Values must have {Nk} items.")
|
|
241
|
+
if len(stdev) != Nk:
|
|
242
|
+
raise ValueError(f"stdev must have {Nk} items.")
|
|
243
|
+
self.means = np.array(values, dtype=float)
|
|
244
|
+
self.stdev = np.array(stdev, dtype=float)
|
|
245
|
+
# Convert percent to decimal for storing.
|
|
246
|
+
self.means /= 100.0
|
|
247
|
+
self.stdev /= 100.0
|
|
248
|
+
# Build covariance matrix from standard deviation and correlation matrix.
|
|
249
|
+
if corr is None:
|
|
250
|
+
corrarr = np.identity(Nk)
|
|
251
|
+
else:
|
|
252
|
+
corrarr = np.array(corr)
|
|
253
|
+
# Full correlation matrix was provided.
|
|
254
|
+
if corrarr.shape == (Nk, Nk):
|
|
255
|
+
pass
|
|
256
|
+
# Only off-diagonal elements were provided: build full matrix.
|
|
257
|
+
elif corrarr.shape == ((Nk * (Nk - 1)) // 2,):
|
|
258
|
+
newcorr = np.identity(Nk)
|
|
259
|
+
x = 0
|
|
260
|
+
for i in range(Nk):
|
|
261
|
+
for j in range(i + 1, Nk):
|
|
262
|
+
newcorr[i, j] = corrarr[x]
|
|
263
|
+
newcorr[j, i] = corrarr[x]
|
|
264
|
+
x += 1
|
|
265
|
+
corrarr = newcorr
|
|
266
|
+
else:
|
|
267
|
+
raise RuntimeError(f"Unable to process correlation shape of {corrarr.shape}.")
|
|
268
|
+
|
|
269
|
+
self.corr = corrarr
|
|
270
|
+
if not np.array_equal(self.corr, self.corr.T):
|
|
271
|
+
raise ValueError("Correlation matrix must be symmetric.")
|
|
272
|
+
# Now build covariance matrix from stdev and correlation matrix.
|
|
273
|
+
# Multiply each row by a vector element-wise. Then columns.
|
|
274
|
+
covar = self.corr * self.stdev
|
|
275
|
+
self.covar = covar.T * self.stdev
|
|
276
|
+
self._rateMethod = self._stochRates
|
|
277
|
+
self.mylog.vprint("Setting rates using stochastic method with means:", *[u.pc(k) for k in self.means])
|
|
278
|
+
self.mylog.vprint("\t standard deviations:", *[u.pc(k) for k in self.stdev])
|
|
279
|
+
self.mylog.vprint("\t and correlation matrix:\n\t\t", str(self.corr).replace("\n", "\n\t\t"))
|
|
280
|
+
else:
|
|
281
|
+
# Then methods relying on historical data range.
|
|
282
|
+
if frm is None:
|
|
283
|
+
raise ValueError("From year must be provided with this option.")
|
|
284
|
+
if not (FROM <= frm <= TO):
|
|
285
|
+
raise ValueError(f"Lower range 'frm={frm}' out of bounds.")
|
|
286
|
+
if not (FROM <= to <= TO):
|
|
287
|
+
raise ValueError(f"Upper range 'to={to}' out of bounds.")
|
|
288
|
+
if not (frm < to):
|
|
289
|
+
raise ValueError("Unacceptable range.")
|
|
290
|
+
self.frm = frm
|
|
291
|
+
self.to = to
|
|
292
|
+
|
|
293
|
+
if method == "historical":
|
|
294
|
+
self.mylog.vprint(f"Using historical rates representing data from {frm} to {to}.")
|
|
295
|
+
self._rateMethod = self._histRates
|
|
296
|
+
elif method == "historical average" or method == "means":
|
|
297
|
+
self.mylog.vprint(f"Using average of rates from {frm} to {to}.")
|
|
298
|
+
self.means, self.stdev, self.corr, self.covar = getRatesDistributions(frm, to, self.mylog)
|
|
299
|
+
self._setFixedRates(self.means)
|
|
300
|
+
elif method == "histochastic":
|
|
301
|
+
self.mylog.vprint(f"Using histochastic rates derived from years {frm} to {to}.")
|
|
302
|
+
self._rateMethod = self._stochRates
|
|
303
|
+
self.means, self.stdev, self.corr, self.covar = getRatesDistributions(frm, to, self.mylog)
|
|
304
|
+
else:
|
|
305
|
+
raise ValueError(f"Method {method} not supported.")
|
|
306
|
+
|
|
307
|
+
self.method = method
|
|
308
|
+
|
|
309
|
+
return self.means, self.stdev, self.corr
|
|
310
|
+
|
|
311
|
+
def _setFixedRates(self, rates):
|
|
312
|
+
Nk = len(self._defRates)
|
|
313
|
+
if len(rates) != Nk:
|
|
314
|
+
raise ValueError(f"Rate list provided must have {Nk} entries.")
|
|
315
|
+
self._myRates = np.array(rates)
|
|
316
|
+
self._rateMethod = self._fixedRates
|
|
317
|
+
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
def genSeries(self, N):
|
|
321
|
+
"""
|
|
322
|
+
Generate a series of Nx4 entries of rates representing S&P500,
|
|
323
|
+
corporate Baa bonds, 10-y treasury notes, and inflation,
|
|
324
|
+
respectively. If there are less than 'N' entries
|
|
325
|
+
in sub-series selected by 'setMethod()', values will be repeated
|
|
326
|
+
modulo the length of the sub-series.
|
|
327
|
+
"""
|
|
328
|
+
rateSeries = np.zeros((N, 4))
|
|
329
|
+
|
|
330
|
+
# Convert years to indices.
|
|
331
|
+
ifrm = self.frm - FROM
|
|
332
|
+
ito = self.to - FROM
|
|
333
|
+
|
|
334
|
+
# Add one since bounds are inclusive.
|
|
335
|
+
span = ito - ifrm + 1
|
|
336
|
+
|
|
337
|
+
# Assign 4 values at the time.
|
|
338
|
+
for n in range(N):
|
|
339
|
+
rateSeries[n][:] = self._rateMethod((ifrm + (n % span)))[:]
|
|
340
|
+
|
|
341
|
+
return rateSeries
|
|
342
|
+
|
|
343
|
+
def _fixedRates(self, n):
|
|
344
|
+
"""
|
|
345
|
+
Return rates provided.
|
|
346
|
+
For fixed rates, values are time-independent, and therefore
|
|
347
|
+
the 'n' argument is ignored.
|
|
348
|
+
"""
|
|
349
|
+
# Fixed rates are stored in decimal.
|
|
350
|
+
return self._myRates
|
|
351
|
+
|
|
352
|
+
def _histRates(self, n):
|
|
353
|
+
"""
|
|
354
|
+
Return an array of 4 values representing the historical rates
|
|
355
|
+
of stock, Corporate Baa bonds, Treasury notes, and inflation,
|
|
356
|
+
respectively.
|
|
357
|
+
"""
|
|
358
|
+
hrates = np.array([SP500[n], BondsBaa[n], TNotes[n], Inflation[n]])
|
|
359
|
+
|
|
360
|
+
# Historical rates are stored in percent. Convert from percent to decimal.
|
|
361
|
+
return hrates / 100
|
|
362
|
+
|
|
363
|
+
def _stochRates(self, n):
|
|
364
|
+
"""
|
|
365
|
+
Return an array of 4 values representing the historical rates
|
|
366
|
+
of stock, Corporate Baa bonds, Treasury notes, and inflation,
|
|
367
|
+
respectively. Values are pulled from normal distributions
|
|
368
|
+
having the same characteristics as the historical data for
|
|
369
|
+
the range of years selected. Argument 'n' is ignored.
|
|
370
|
+
|
|
371
|
+
But these variables need to be looked at together
|
|
372
|
+
through multivariate analysis. Code below accounts for
|
|
373
|
+
covariance between stocks, corp bonds, t-notes, and inflation.
|
|
374
|
+
"""
|
|
375
|
+
srates = self._rng.multivariate_normal(self.means, self.covar)
|
|
376
|
+
|
|
377
|
+
return srates
|