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