pyacm 0.4__py3-none-any.whl → 1.1__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.
- pyacm/acm.py +264 -193
- {pyacm-0.4.dist-info → pyacm-1.1.dist-info}/METADATA +25 -22
- pyacm-1.1.dist-info/RECORD +7 -0
- {pyacm-0.4.dist-info → pyacm-1.1.dist-info}/WHEEL +1 -1
- pyacm/utils.py +0 -43
- pyacm-0.4.dist-info/RECORD +0 -8
- {pyacm-0.4.dist-info → pyacm-1.1.dist-info/licenses}/LICENSE +0 -0
- {pyacm-0.4.dist-info → pyacm-1.1.dist-info}/top_level.txt +0 -0
pyacm/acm.py
CHANGED
|
@@ -3,8 +3,7 @@ import pandas as pd
|
|
|
3
3
|
|
|
4
4
|
from numpy.linalg import inv
|
|
5
5
|
from sklearn.decomposition import PCA
|
|
6
|
-
|
|
7
|
-
from pyacm.utils import vec, vec_quad_form, commutation_matrix
|
|
6
|
+
from statsmodels.tools.tools import add_constant
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
class NominalACM:
|
|
@@ -39,22 +38,19 @@ class NominalACM:
|
|
|
39
38
|
Yield curve data resampled to a monthly frequency by averageing
|
|
40
39
|
the observations
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
Number of observations in the timeseries dimension
|
|
41
|
+
t_m: int
|
|
42
|
+
Number of observations in the monthly timeseries dimension
|
|
43
|
+
|
|
44
|
+
t_d: int
|
|
45
|
+
Number of observations in the daily timeseries dimension
|
|
44
46
|
|
|
45
47
|
n: int
|
|
46
|
-
Number of observations in the cross-sectional dimension
|
|
47
|
-
|
|
48
|
+
Number of observations in the cross-sectional dimension, the number of
|
|
49
|
+
maturities available
|
|
48
50
|
|
|
49
51
|
rx_m: pd.DataFrame
|
|
50
52
|
Excess returns in monthly frquency
|
|
51
53
|
|
|
52
|
-
rf_m: pandas.Series
|
|
53
|
-
Risk-free rate in monthly frequency
|
|
54
|
-
|
|
55
|
-
rf_d: pandas.Series
|
|
56
|
-
Risk-free rate in daily frequency
|
|
57
|
-
|
|
58
54
|
pc_factors_m: pandas.DataFrame
|
|
59
55
|
Principal components in monthly frequency
|
|
60
56
|
|
|
@@ -67,23 +63,26 @@ class NominalACM:
|
|
|
67
63
|
pc_factors_d: pandas.DataFrame
|
|
68
64
|
Principal components in daily frequency
|
|
69
65
|
|
|
70
|
-
pc_loadings_d: pandas.DataFrame
|
|
71
|
-
Factor loadings of the daily PCs
|
|
72
|
-
|
|
73
|
-
pc_explained_d: pandas.Series
|
|
74
|
-
Percent of total variance explained by each monthly principal component
|
|
75
|
-
|
|
76
66
|
mu, phi, Sigma, v: numpy.array
|
|
77
67
|
Estimates of the VAR(1) parameters, the first stage of estimation.
|
|
78
68
|
The names are the same as the original paper
|
|
79
69
|
|
|
80
|
-
|
|
70
|
+
beta: numpy.array
|
|
81
71
|
Estimates of the risk premium equation, the second stage of estimation.
|
|
82
|
-
The
|
|
72
|
+
The name is the same as the original paper
|
|
83
73
|
|
|
84
74
|
lambda0, lambda1: numpy.array
|
|
85
|
-
Estimates of the price of risk parameters, the third stage of
|
|
86
|
-
|
|
75
|
+
Estimates of the price of risk parameters, the third stage of
|
|
76
|
+
estimation.
|
|
77
|
+
|
|
78
|
+
delta0, delta1: numpy.array
|
|
79
|
+
Estimates of the short rate equation coefficients.
|
|
80
|
+
|
|
81
|
+
A, B: numpy.array
|
|
82
|
+
Affine coefficients for the fitted yields of different maturities
|
|
83
|
+
|
|
84
|
+
Arn, Brn: numpy.array
|
|
85
|
+
Affine coefficients for the risk neutral yields of different maturities
|
|
87
86
|
|
|
88
87
|
miy: pandas.DataFrame
|
|
89
88
|
Model implied / fitted yields
|
|
@@ -97,20 +96,17 @@ class NominalACM:
|
|
|
97
96
|
er_loadings: pandas.DataFrame
|
|
98
97
|
Loadings of the expected reutrns on the principal components
|
|
99
98
|
|
|
100
|
-
|
|
101
|
-
Historical estimates of expected returns, computed in-sample
|
|
102
|
-
|
|
103
|
-
er_hist_d: pandas.DataFrame
|
|
104
|
-
Historical estimates of expected returns, computed in-sample, in daily frequency
|
|
105
|
-
|
|
106
|
-
z_lambda: pandas.DataFrame
|
|
107
|
-
Z-stat for inference on the price of risk parameters
|
|
108
|
-
|
|
109
|
-
z_beta: pandas.DataFrame
|
|
110
|
-
Z-stat for inference on the loadings of expected returns
|
|
99
|
+
er_hist: pandas.DataFrame
|
|
100
|
+
Historical estimates of expected returns, computed in-sample.
|
|
111
101
|
"""
|
|
112
102
|
|
|
113
|
-
def __init__(
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
curve,
|
|
106
|
+
curve_m=None,
|
|
107
|
+
n_factors=5,
|
|
108
|
+
selected_maturities=None,
|
|
109
|
+
):
|
|
114
110
|
"""
|
|
115
111
|
Runs the baseline varsion of the ACM term premium model. Works for data
|
|
116
112
|
with monthly frequency or higher.
|
|
@@ -119,40 +115,93 @@ class NominalACM:
|
|
|
119
115
|
----------
|
|
120
116
|
curve : pandas.DataFrame
|
|
121
117
|
Annualized log-yields. Maturities (columns) must start at month 1
|
|
122
|
-
and be equally spaced in monthly frequency.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
118
|
+
and be equally spaced in monthly frequency. Column labels must be
|
|
119
|
+
integers from 1 to n. Observations (index) must be a pandas
|
|
120
|
+
DatetimeIndex with daily frequency.
|
|
121
|
+
|
|
122
|
+
curve_m: pandas.DataFrame
|
|
123
|
+
Annualized log-yields in monthly frequency to be used for the
|
|
124
|
+
parameters estimates. This is here in case the user wants to use a
|
|
125
|
+
different curve for the parameter estimation. If None is passed,
|
|
126
|
+
the input `curve` is resampled to monthly frequency. If something
|
|
127
|
+
is passed, maturities (columns) must start at month 1 and be
|
|
128
|
+
equally spaced in monthly frequency. Column labels must be
|
|
129
|
+
integers from 1 to n. Observations (index) must be a pandas
|
|
130
|
+
DatetimeIndex with monthly frequency.
|
|
126
131
|
|
|
127
132
|
n_factors : int
|
|
128
133
|
number of principal components to used as state variables.
|
|
134
|
+
|
|
135
|
+
selected_maturities: list of int
|
|
136
|
+
the maturities to be considered in the parameter estimation steps.
|
|
137
|
+
If None is passed, all the maturities are considered. The user may
|
|
138
|
+
choose smaller set of yields to consider due to, for example,
|
|
139
|
+
liquidity and representativeness of certain maturities.
|
|
129
140
|
"""
|
|
130
141
|
|
|
142
|
+
self._assertions(curve, curve_m, selected_maturities)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
131
146
|
self.n_factors = n_factors
|
|
132
147
|
self.curve = curve
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
self.a, self.beta, self.c, self.sigma2 = self._excess_return_regression()
|
|
142
|
-
self.lambda0, self.lambda1 = self._retrieve_lambda()
|
|
143
|
-
|
|
144
|
-
if self.curve.index.freqstr == 'M':
|
|
145
|
-
X = self.pc_factors_m
|
|
146
|
-
r1 = self.rf_m
|
|
148
|
+
|
|
149
|
+
if selected_maturities is None:
|
|
150
|
+
self.selected_maturities = curve.columns
|
|
151
|
+
else:
|
|
152
|
+
self.selected_maturities = selected_maturities
|
|
153
|
+
|
|
154
|
+
if curve_m is None:
|
|
155
|
+
self.curve_monthly = curve.resample('M').mean()
|
|
147
156
|
else:
|
|
148
|
-
|
|
149
|
-
|
|
157
|
+
self.curve_monthly = curve_m
|
|
158
|
+
|
|
159
|
+
self.t_d = self.curve.shape[0]
|
|
160
|
+
self.t_m = self.curve_monthly.shape[0] - 1
|
|
161
|
+
self.n = self.curve.shape[1]
|
|
162
|
+
self.pc_factors_m, self.pc_factors_d, self.pc_loadings_m, self.pc_explained_m = self._get_pcs(self.curve_monthly, self.curve)
|
|
163
|
+
|
|
164
|
+
self.rx_m = self._get_excess_returns()
|
|
165
|
+
|
|
166
|
+
# ===== ACM Three-Step Regression =====
|
|
167
|
+
# 1st Step - Factor VAR
|
|
168
|
+
self.mu, self.phi, self.Sigma, self.v, self.s0 = self._estimate_var()
|
|
169
|
+
|
|
170
|
+
# 2nd Step - Excess Returns
|
|
171
|
+
self.beta, self.omega, self.beta_star = self._excess_return_regression()
|
|
172
|
+
|
|
173
|
+
# 3rd Step - Convexity-adjusted price of risk
|
|
174
|
+
self.lambda0, self.lambda1, self.mu_star, self.phi_star = self._retrieve_lambda()
|
|
150
175
|
|
|
151
|
-
|
|
152
|
-
self.
|
|
176
|
+
# Short Rate Equation
|
|
177
|
+
self.delta0, self.delta1 = self._short_rate_equation(
|
|
178
|
+
r1=self.curve_monthly.iloc[:, 0],
|
|
179
|
+
X=self.pc_factors_m,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Affine Yield Coefficients
|
|
183
|
+
self.A, self.B = self._affine_coefficients(
|
|
184
|
+
lambda0=self.lambda0,
|
|
185
|
+
lambda1=self.lambda1,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Risk-Neutral Coefficients
|
|
189
|
+
self.Arn, self.Brn = self._affine_coefficients(
|
|
190
|
+
lambda0=np.zeros(self.lambda0.shape),
|
|
191
|
+
lambda1=np.zeros(self.lambda1.shape),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Model Implied Yield
|
|
195
|
+
self.miy = self._compute_yields(self.A, self.B)
|
|
196
|
+
|
|
197
|
+
# Risk Neutral Yield
|
|
198
|
+
self.rny = self._compute_yields(self.Arn, self.Brn)
|
|
199
|
+
|
|
200
|
+
# Term Premium
|
|
153
201
|
self.tp = self.miy - self.rny
|
|
154
|
-
|
|
155
|
-
|
|
202
|
+
|
|
203
|
+
# Expected Return
|
|
204
|
+
self.er_loadings, self.er_hist = self._expected_return()
|
|
156
205
|
|
|
157
206
|
def fwd_curve(self, date=None):
|
|
158
207
|
"""
|
|
@@ -174,14 +223,13 @@ class NominalACM:
|
|
|
174
223
|
df = pd.concat(
|
|
175
224
|
[
|
|
176
225
|
fwd_mkt.rename("Observed"),
|
|
177
|
-
fwd_miy.rename("
|
|
226
|
+
fwd_miy.rename("Fitted"),
|
|
178
227
|
fwd_rny.rename("Risk-Neutral"),
|
|
179
228
|
],
|
|
180
229
|
axis=1,
|
|
181
230
|
)
|
|
182
231
|
return df
|
|
183
232
|
|
|
184
|
-
|
|
185
233
|
@staticmethod
|
|
186
234
|
def _compute_fwd_curve(curve):
|
|
187
235
|
aux_curve = curve.reset_index(drop=True)
|
|
@@ -192,95 +240,191 @@ class NominalACM:
|
|
|
192
240
|
fwds = pd.Series(fwds.values, index=curve.index)
|
|
193
241
|
return fwds
|
|
194
242
|
|
|
243
|
+
@staticmethod
|
|
244
|
+
def _assertions(curve, curve_m, selected_maturities):
|
|
245
|
+
# Selected maturities are available
|
|
246
|
+
if selected_maturities is not None:
|
|
247
|
+
assert all([col in curve.columns for col in selected_maturities]), \
|
|
248
|
+
"not all `selected_columns` are available in `curve`"
|
|
249
|
+
|
|
250
|
+
# Consecutive monthly maturities
|
|
251
|
+
cond1 = curve.columns[0] != 1
|
|
252
|
+
cond2 = not all(np.diff(curve.columns.values) == 1)
|
|
253
|
+
if cond1 or cond2:
|
|
254
|
+
msg = "`curve` columns must be consecutive integers starting from 1"
|
|
255
|
+
raise AssertionError(msg)
|
|
256
|
+
|
|
257
|
+
# Only if `curve_m` is passed
|
|
258
|
+
if curve_m is not None:
|
|
259
|
+
|
|
260
|
+
# Same columns
|
|
261
|
+
assert curve_m.columns.equals(curve.columns), \
|
|
262
|
+
"columns of `curve` and `curve_m` must be the same"
|
|
263
|
+
|
|
264
|
+
# Monthly frequency
|
|
265
|
+
assert pd.infer_freq(curve_m.index) == 'M', \
|
|
266
|
+
"`curve_m` must have a DatetimeIndex with monthly frequency"
|
|
267
|
+
|
|
195
268
|
def _get_excess_returns(self):
|
|
196
269
|
ttm = np.arange(1, self.n + 1) / 12
|
|
197
270
|
log_prices = - self.curve_monthly * ttm
|
|
198
271
|
rf = - log_prices.iloc[:, 0].shift(1)
|
|
199
272
|
rx = (log_prices - log_prices.shift(1, axis=0).shift(-1, axis=1)).subtract(rf, axis=0)
|
|
200
|
-
rx = rx.
|
|
201
|
-
|
|
273
|
+
rx = rx.shift(1, axis=1)
|
|
274
|
+
|
|
275
|
+
rx = rx.dropna(how='all', axis=0)
|
|
276
|
+
rx[1] = 0
|
|
277
|
+
return rx
|
|
278
|
+
|
|
279
|
+
def _get_pcs(self, curve_m, curve_d):
|
|
280
|
+
|
|
281
|
+
# The authors' code shows that they ignore the first 2 maturities for
|
|
282
|
+
# the PC estimation.
|
|
283
|
+
curve_m_cut = curve_m.iloc[:, 2:]
|
|
284
|
+
curve_d_cut = curve_d.iloc[:, 2:]
|
|
285
|
+
|
|
286
|
+
mean_yields = curve_m_cut.mean()
|
|
287
|
+
curve_m_cut = curve_m_cut - mean_yields
|
|
288
|
+
curve_d_cut = curve_d_cut - mean_yields
|
|
202
289
|
|
|
203
|
-
def _get_pcs(self, curve):
|
|
204
290
|
pca = PCA(n_components=self.n_factors)
|
|
205
|
-
pca.fit(
|
|
291
|
+
pca.fit(curve_m_cut)
|
|
206
292
|
col_names = [f'PC {i + 1}' for i in range(self.n_factors)]
|
|
207
|
-
df_loadings = pd.DataFrame(
|
|
208
|
-
|
|
209
|
-
|
|
293
|
+
df_loadings = pd.DataFrame(
|
|
294
|
+
data=pca.components_.T,
|
|
295
|
+
columns=col_names,
|
|
296
|
+
index=curve_m_cut.columns,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
df_pc_m = curve_m_cut @ df_loadings
|
|
300
|
+
sigma_factor = df_pc_m.std()
|
|
301
|
+
df_pc_m = df_pc_m / df_pc_m.std()
|
|
302
|
+
df_loadings = df_loadings / sigma_factor
|
|
210
303
|
|
|
211
|
-
#
|
|
212
|
-
|
|
213
|
-
df_loadings =
|
|
214
|
-
|
|
304
|
+
# Enforce average positive loadings
|
|
305
|
+
sign_changes = np.sign(df_loadings.mean())
|
|
306
|
+
df_loadings = sign_changes * df_loadings
|
|
307
|
+
df_pc_m = sign_changes * df_pc_m
|
|
308
|
+
|
|
309
|
+
# Daily frequency
|
|
310
|
+
df_pc_d = curve_d_cut @ df_loadings
|
|
215
311
|
|
|
216
312
|
# Percent Explained
|
|
217
|
-
df_explained = pd.Series(
|
|
218
|
-
|
|
219
|
-
|
|
313
|
+
df_explained = pd.Series(
|
|
314
|
+
data=pca.explained_variance_ratio_,
|
|
315
|
+
name='Explained Variance',
|
|
316
|
+
index=col_names,
|
|
317
|
+
)
|
|
220
318
|
|
|
221
|
-
return
|
|
319
|
+
return df_pc_m, df_pc_d, df_loadings, df_explained
|
|
222
320
|
|
|
223
321
|
def _estimate_var(self):
|
|
224
322
|
X = self.pc_factors_m.copy().T
|
|
225
323
|
X_lhs = X.values[:, 1:] # X_t+1. Left hand side of VAR
|
|
226
|
-
X_rhs = np.vstack((np.ones((1, self.
|
|
324
|
+
X_rhs = np.vstack((np.ones((1, self.t_m)), X.values[:, 0:-1])) # X_t and a constant.
|
|
227
325
|
|
|
228
326
|
var_coeffs = (X_lhs @ np.linalg.pinv(X_rhs))
|
|
229
|
-
|
|
327
|
+
|
|
230
328
|
phi = var_coeffs[:, 1:]
|
|
231
329
|
|
|
330
|
+
# Leave the estimated constant
|
|
331
|
+
# mu = var_coeffs[:, [0]]
|
|
332
|
+
|
|
333
|
+
# Force constant to zero
|
|
334
|
+
mu = np.zeros((self.n_factors, 1))
|
|
335
|
+
var_coeffs[:, [0]] = 0
|
|
336
|
+
|
|
337
|
+
# Residuals
|
|
232
338
|
v = X_lhs - var_coeffs @ X_rhs
|
|
233
|
-
Sigma = v @ v.T / self.
|
|
339
|
+
Sigma = v @ v.T / (self.t_m - 1)
|
|
340
|
+
|
|
341
|
+
s0 = np.cov(v).reshape((-1, 1))
|
|
234
342
|
|
|
235
|
-
return mu, phi, Sigma, v
|
|
343
|
+
return mu, phi, Sigma, v, s0
|
|
236
344
|
|
|
237
345
|
def _excess_return_regression(self):
|
|
346
|
+
|
|
347
|
+
if self.selected_maturities is not None:
|
|
348
|
+
rx = self.rx_m[self.selected_maturities].values
|
|
349
|
+
else:
|
|
350
|
+
rx = self.rx_m.values
|
|
351
|
+
|
|
238
352
|
X = self.pc_factors_m.copy().T.values[:, :-1]
|
|
239
|
-
Z = np.vstack((np.ones((1, self.
|
|
240
|
-
abc =
|
|
241
|
-
E =
|
|
242
|
-
|
|
353
|
+
Z = np.vstack((np.ones((1, self.t_m)), X, self.v)).T # Lagged X and Innovations
|
|
354
|
+
abc = inv(Z.T @ Z) @ (Z.T @ rx)
|
|
355
|
+
E = rx - Z @ abc
|
|
356
|
+
omega = np.var(E.reshape(-1, 1)) * np.eye(len(self.selected_maturities))
|
|
357
|
+
|
|
358
|
+
abc = abc.T
|
|
359
|
+
beta = abc[:, -self.n_factors:]
|
|
360
|
+
|
|
361
|
+
beta_star = np.zeros((len(self.selected_maturities), self.n_factors**2))
|
|
243
362
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
c = abc[:, self.n_factors + 1:]
|
|
363
|
+
for i in range(len(self.selected_maturities)):
|
|
364
|
+
beta_star[i, :] = np.kron(beta[i, :], beta[i, :]).T
|
|
247
365
|
|
|
248
|
-
return
|
|
366
|
+
return beta, omega, beta_star
|
|
249
367
|
|
|
250
368
|
def _retrieve_lambda(self):
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
lambda0 = np.linalg.pinv(self.beta.T) @ (self.a + 0.5 * (BStar @ vec(self.Sigma) + self.sigma2))
|
|
254
|
-
return lambda0, lambda1
|
|
369
|
+
rx = self.rx_m[self.selected_maturities]
|
|
370
|
+
factors = np.hstack([np.ones((self.t_m, 1)), self.pc_factors_m.iloc[:-1].values])
|
|
255
371
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
372
|
+
# Orthogonalize factors with respect to v
|
|
373
|
+
v_proj = self.v.T @ np.linalg.pinv(self.v @ self.v.T) @ self.v
|
|
374
|
+
factors = factors - v_proj @ factors
|
|
259
375
|
|
|
260
|
-
|
|
261
|
-
|
|
376
|
+
adjustment = self.beta_star @ self.s0 + np.diag(self.omega).reshape(-1, 1)
|
|
377
|
+
rx_adjusted = rx.values + (1 / 2) * np.tile(adjustment, (1, self.t_m)).T
|
|
378
|
+
Y = (inv(factors.T @ factors) @ factors.T @ rx_adjusted).T
|
|
262
379
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
380
|
+
# Compute Lambda
|
|
381
|
+
X = self.beta
|
|
382
|
+
Lambda = inv(X.T @ X) @ X.T @ Y
|
|
383
|
+
lambda0 = Lambda[:, 0]
|
|
384
|
+
lambda1 = Lambda[:, 1:]
|
|
266
385
|
|
|
267
|
-
|
|
268
|
-
|
|
386
|
+
muStar = self.mu.reshape(-1) - lambda0
|
|
387
|
+
phiStar = self.phi - lambda1
|
|
269
388
|
|
|
270
|
-
|
|
271
|
-
A[0, i + 1] = A[0, i] + B[:, i].T @ (self.mu - lambda0) + 1 / 2 * (B[:, i].T @ self.Sigma @ B[:, i] + 0 * self.sigma2) - delta0
|
|
272
|
-
B[:, i + 1] = B[:, i] @ (self.phi - lambda1) - delta1
|
|
389
|
+
return lambda0, lambda1, muStar, phiStar
|
|
273
390
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
391
|
+
@staticmethod
|
|
392
|
+
def _short_rate_equation(r1, X):
|
|
393
|
+
r1 = r1 / 12
|
|
394
|
+
X = add_constant(X)
|
|
395
|
+
Delta = inv(X.T @ X) @ X.T @ r1
|
|
396
|
+
delta0 = Delta.iloc[0]
|
|
397
|
+
delta1 = Delta.iloc[1:].values
|
|
398
|
+
return delta0, delta1
|
|
399
|
+
|
|
400
|
+
def _affine_coefficients(self, lambda0, lambda1):
|
|
401
|
+
lambda0 = lambda0.reshape(-1, 1)
|
|
402
|
+
|
|
403
|
+
A = np.zeros(self.n)
|
|
404
|
+
B = np.zeros((self.n, self.n_factors))
|
|
405
|
+
|
|
406
|
+
A[0] = - self.delta0
|
|
407
|
+
B[0, :] = - self.delta1
|
|
408
|
+
|
|
409
|
+
for n in range(1, self.n):
|
|
410
|
+
Bpb = np.kron(B[n - 1, :], B[n - 1, :])
|
|
411
|
+
s0term = 0.5 * (Bpb @ self.s0 + self.omega[0, 0])
|
|
412
|
+
|
|
413
|
+
A[n] = A[n - 1] + B[n - 1, :] @ (self.mu - lambda0) + s0term + A[0]
|
|
414
|
+
B[n, :] = B[n - 1, :] @ (self.phi - lambda1) + B[0, :]
|
|
415
|
+
|
|
416
|
+
return A, B
|
|
417
|
+
|
|
418
|
+
def _compute_yields(self, A, B):
|
|
419
|
+
A = A.reshape(-1, 1)
|
|
420
|
+
multiplier = np.tile(self.curve.columns / 12, (self.t_d, 1)).T
|
|
421
|
+
yields = (- ((np.tile(A, (1, self.t_d)) + B @ self.pc_factors_d.T) / multiplier).T).values
|
|
422
|
+
yields = pd.DataFrame(
|
|
423
|
+
data=yields,
|
|
424
|
+
index=self.curve.index,
|
|
281
425
|
columns=self.curve.columns,
|
|
282
426
|
)
|
|
283
|
-
return
|
|
427
|
+
return yields
|
|
284
428
|
|
|
285
429
|
def _expected_return(self):
|
|
286
430
|
"""
|
|
@@ -291,93 +435,20 @@ class NominalACM:
|
|
|
291
435
|
expected returns
|
|
292
436
|
"""
|
|
293
437
|
stds = self.pc_factors_m.std().values[:, None].T
|
|
294
|
-
er_loadings = (self.
|
|
438
|
+
er_loadings = (self.B @ self.lambda1) * stds
|
|
295
439
|
er_loadings = pd.DataFrame(
|
|
296
440
|
data=er_loadings,
|
|
297
441
|
columns=self.pc_factors_m.columns,
|
|
298
|
-
index=self.
|
|
442
|
+
index=range(1, self.n + 1),
|
|
299
443
|
)
|
|
300
444
|
|
|
301
|
-
#
|
|
302
|
-
exp_ret = (self.
|
|
303
|
-
conv_adj = np.diag(self.
|
|
304
|
-
er_hist = (exp_ret
|
|
305
|
-
er_hist_m = pd.DataFrame(
|
|
306
|
-
data=er_hist,
|
|
307
|
-
index=self.pc_factors_m.index,
|
|
308
|
-
columns=self.curve.columns[:er_hist.shape[1]]
|
|
309
|
-
)
|
|
310
|
-
|
|
311
|
-
# Higher frequency
|
|
312
|
-
exp_ret = (self.beta.T @ (self.lambda1 @ self.pc_factors_d.T + self.lambda0)).values
|
|
313
|
-
conv_adj = np.diag(self.beta.T @ self.Sigma @ self.beta) + self.sigma2
|
|
314
|
-
er_hist = (exp_ret + conv_adj[:, None]).T
|
|
445
|
+
# Historical estimate
|
|
446
|
+
exp_ret = (self.B @ (self.lambda1 @ self.pc_factors_d.T + self.lambda0.reshape(-1, 1))).values
|
|
447
|
+
conv_adj = np.diag(self.B @ self.Sigma @ self.B.T) + self.omega[0, 0]
|
|
448
|
+
er_hist = (exp_ret - 0.5 * conv_adj[:, None]).T
|
|
315
449
|
er_hist_d = pd.DataFrame(
|
|
316
450
|
data=er_hist,
|
|
317
451
|
index=self.pc_factors_d.index,
|
|
318
|
-
columns=self.curve.columns
|
|
452
|
+
columns=self.curve.columns,
|
|
319
453
|
)
|
|
320
|
-
|
|
321
|
-
return er_loadings, er_hist_m, er_hist_d
|
|
322
|
-
|
|
323
|
-
def _inference(self):
|
|
324
|
-
# TODO I AM NOT SURE THAT THIS SECTION IS CORRECT
|
|
325
|
-
|
|
326
|
-
# Auxiliary matrices
|
|
327
|
-
Z = self.pc_factors_m.copy().T
|
|
328
|
-
Z = Z.values[:, 1:]
|
|
329
|
-
Z = np.vstack((np.ones((1, self.t)), Z))
|
|
330
|
-
|
|
331
|
-
Lamb = np.hstack((self.lambda0, self.lambda1))
|
|
332
|
-
|
|
333
|
-
rho1 = np.zeros((self.n_factors + 1, 1))
|
|
334
|
-
rho1[0, 0] = 1
|
|
335
|
-
|
|
336
|
-
A_beta = np.zeros((self.n_factors * self.beta.shape[1], self.beta.shape[1]))
|
|
337
|
-
|
|
338
|
-
for ii in range(self.beta.shape[1]):
|
|
339
|
-
A_beta[ii * self.beta.shape[0]:(ii + 1) * self.beta.shape[0], ii] = self.beta[:, ii]
|
|
340
|
-
|
|
341
|
-
BStar = np.squeeze(np.apply_along_axis(vec_quad_form, 1, self.beta.T))
|
|
342
|
-
|
|
343
|
-
comm_kk = commutation_matrix(shape=(self.n_factors, self.n_factors))
|
|
344
|
-
comm_kn = commutation_matrix(shape=(self.n_factors, self.beta.shape[1]))
|
|
345
|
-
|
|
346
|
-
# Assymptotic variance of the betas
|
|
347
|
-
v_beta = self.sigma2 * np.kron(np.eye(self.beta.shape[1]), inv(self.Sigma))
|
|
348
|
-
|
|
349
|
-
# Assymptotic variance of the lambdas
|
|
350
|
-
upsilon_zz = (1 / self.t) * Z @ Z.T
|
|
351
|
-
v1 = np.kron(inv(upsilon_zz), self.Sigma)
|
|
352
|
-
v2 = self.sigma2 * np.kron(inv(upsilon_zz), inv(self.beta @ self.beta.T))
|
|
353
|
-
v3 = self.sigma2 * np.kron(Lamb.T @ self.Sigma @ Lamb, inv(self.beta @ self.beta.T))
|
|
354
|
-
|
|
355
|
-
v4_sim = inv(self.beta @ self.beta.T) @ self.beta @ A_beta.T
|
|
356
|
-
v4_mid = np.kron(np.eye(self.beta.shape[1]), self.Sigma)
|
|
357
|
-
v4 = self.sigma2 * np.kron(rho1 @ rho1.T, v4_sim @ v4_mid @ v4_sim.T)
|
|
358
|
-
|
|
359
|
-
v5_sim = inv(self.beta @ self.beta.T) @ self.beta @ BStar
|
|
360
|
-
v5_mid = (np.eye(self.n_factors ** 2) + comm_kk) @ np.kron(self.Sigma, self.Sigma)
|
|
361
|
-
v5 = 0.25 * np.kron(rho1 @ rho1.T, v5_sim @ v5_mid @ v5_sim.T)
|
|
362
|
-
|
|
363
|
-
v6_sim = inv(self.beta @ self.beta.T) @ self.beta @ np.ones((self.beta.shape[1], 1))
|
|
364
|
-
v6 = 0.5 * (self.sigma2 ** 2) * np.kron(rho1 @ rho1.T, v6_sim @ v6_sim.T)
|
|
365
|
-
|
|
366
|
-
v_lambda_tau = v1 + v2 + v3 + v4 + v5 + v6
|
|
367
|
-
|
|
368
|
-
c_lambda_tau_1 = np.kron(Lamb.T, inv(self.beta @ self.beta.T) @ self.beta)
|
|
369
|
-
c_lambda_tau_2 = np.kron(rho1, inv(self.beta @ self.beta.T) @ self.beta @ A_beta.T @ np.kron(np.eye(self.beta.shape[1]), self.Sigma))
|
|
370
|
-
c_lambda_tau = - c_lambda_tau_1 @ comm_kn @ v_beta @ c_lambda_tau_2.T
|
|
371
|
-
|
|
372
|
-
v_lambda = v_lambda_tau + c_lambda_tau + c_lambda_tau.T
|
|
373
|
-
|
|
374
|
-
# extract the z-tests
|
|
375
|
-
sd_lambda = np.sqrt(np.diag(v_lambda).reshape(Lamb.shape, order='F'))
|
|
376
|
-
sd_beta = np.sqrt(np.diag(v_beta).reshape(self.beta.shape, order='F'))
|
|
377
|
-
|
|
378
|
-
z_beta = pd.DataFrame(self.beta / sd_beta, index=self.pc_factors_m.columns, columns=self.curve.columns[:-1]).T
|
|
379
|
-
z_lambda = pd.DataFrame(Lamb / sd_lambda, index=self.pc_factors_m.columns, columns=[f"lambda {i}" for i in range(Lamb.shape[1])])
|
|
380
|
-
|
|
381
|
-
return z_lambda, z_beta
|
|
382
|
-
|
|
383
|
-
|
|
454
|
+
return er_loadings, er_hist_d
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pyacm
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.1
|
|
4
4
|
Summary: ACM Term Premium
|
|
5
5
|
Author: Tobias Adrian, Richard K. Crump, Emanuel Moench
|
|
6
6
|
Maintainer: Gustavo Amarante
|
|
@@ -12,11 +12,20 @@ Requires-Dist: matplotlib
|
|
|
12
12
|
Requires-Dist: numpy
|
|
13
13
|
Requires-Dist: pandas
|
|
14
14
|
Requires-Dist: scikit-learn
|
|
15
|
-
Requires-Dist:
|
|
15
|
+
Requires-Dist: statsmodels
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: description
|
|
18
|
+
Dynamic: description-content-type
|
|
19
|
+
Dynamic: keywords
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
Dynamic: maintainer
|
|
22
|
+
Dynamic: maintainer-email
|
|
23
|
+
Dynamic: requires-dist
|
|
24
|
+
Dynamic: summary
|
|
16
25
|
|
|
17
26
|
|
|
18
27
|
[paper_website]: https://www.newyorkfed.org/medialibrary/media/research/staff_reports/sr340.pdf
|
|
19
|
-
|
|
28
|
+
|
|
20
29
|
|
|
21
30
|
# pyacm
|
|
22
31
|
Implementation of ["Pricing the Term Structure with Linear Regressions" from
|
|
@@ -35,7 +44,6 @@ carries all the relevant variables as atributes:
|
|
|
35
44
|
- Term premium
|
|
36
45
|
- Historical in-sample expected returns
|
|
37
46
|
- Expected return loadings
|
|
38
|
-
- Hypothesis testing (Not sure if correct, more info observations below)
|
|
39
47
|
|
|
40
48
|
|
|
41
49
|
# Instalation
|
|
@@ -43,6 +51,7 @@ carries all the relevant variables as atributes:
|
|
|
43
51
|
pip install pyacm
|
|
44
52
|
```
|
|
45
53
|
|
|
54
|
+
|
|
46
55
|
# Usage
|
|
47
56
|
```python
|
|
48
57
|
from pyacm import NominalACM
|
|
@@ -59,17 +68,16 @@ The tricky part of using this model is getting the correct data format. The
|
|
|
59
68
|
- Maturities (columns) must be equally spaced in **monthly** frequency and start
|
|
60
69
|
at month 1. This means that you need to construct a bootstraped curve for every
|
|
61
70
|
date and interpolate it at fixed monthly maturities
|
|
62
|
-
- Whichever maturity you want to be the longest, your input data should have one
|
|
63
|
-
column more. For example, if you want term premium estimate up to the 10-year
|
|
64
|
-
yield (120 months), your input data should include maturities up to 121 months.
|
|
65
|
-
This is needed to properly compute the returns.
|
|
66
71
|
|
|
67
|
-
# Examples
|
|
68
72
|
|
|
69
|
-
|
|
73
|
+
# Examples
|
|
74
|
+
Updated estimates for the US are available on the [NY FED website](https://www.newyorkfed.org/research/data_indicators/term-premia-tabs#/overview).
|
|
75
|
+
The file [`example_us`](https://github.com/gusamarante/pyacm/blob/main/example_us.py) reproduces the original outputs using the same
|
|
76
|
+
dataset as the authors.
|
|
70
77
|
|
|
71
78
|
The jupyter notebook [`example_br`](https://github.com/gusamarante/pyacm/blob/main/example_br.ipynb)
|
|
72
|
-
contains an example application to the Brazilian DI futures curve that
|
|
79
|
+
contains an example application to the Brazilian DI futures curve that
|
|
80
|
+
showcases all the available methods and attributes.
|
|
73
81
|
|
|
74
82
|
<p align="center">
|
|
75
83
|
<img src="https://raw.githubusercontent.com/gusamarante/pyacm/refs/heads/main/images/DI%20term%20premium.png" alt="DI Term Premium"/>
|
|
@@ -82,14 +90,9 @@ contains an example application to the Brazilian DI futures curve that showcases
|
|
|
82
90
|
> FRB of New York Staff Report No. 340,
|
|
83
91
|
> Available at SSRN: https://ssrn.com/abstract=1362586 or http://dx.doi.org/10.2139/ssrn.1362586
|
|
84
92
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
- Data for zero yields uses monthly maturities starting from month 1
|
|
88
|
-
- All principal components and model parameters are estiamted with data resampled to a monthly frequency, averaging observations in each month
|
|
89
|
-
- To get daily / real-time estimates, the factor loadings estimated from the monthly frquency are used to transform the daily data
|
|
90
|
-
|
|
93
|
+
I would like to thank Emanuel Moench for sharing his original MATLAB code in
|
|
94
|
+
order to perfectly replicate these results.
|
|
91
95
|
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
guidelines.
|
|
96
|
+
# Citation
|
|
97
|
+
> Gustavo Amarante (2025). pyacm: Python Implementation of the ACM Term Premium
|
|
98
|
+
> Model. Retrieved from https://github.com/gusamarante/pyacm
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pyacm/__init__.py,sha256=pRFuR3Au_ybQAmkJduGrLMKGJd1pxjhGfhsfsjlK-mU,66
|
|
2
|
+
pyacm/acm.py,sha256=mpcGjZR5reW25jkkyt_u2g2h_JOGmc1BD4FnBe1M5zQ,15528
|
|
3
|
+
pyacm-1.1.dist-info/licenses/LICENSE,sha256=YbUXx25Z6PzP4k4rsbs6tN58NiCwGIIrTMzql4iTeDs,1073
|
|
4
|
+
pyacm-1.1.dist-info/METADATA,sha256=kTyQaKG5q0VwGEoRK1Vj32BDm_tgfHY5XSds5SmHYck,3524
|
|
5
|
+
pyacm-1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
pyacm-1.1.dist-info/top_level.txt,sha256=xQy7q1eSKCnRtTnwb-Iz_spT0UDNdTyzKd43yz-ffrI,6
|
|
7
|
+
pyacm-1.1.dist-info/RECORD,,
|
pyacm/utils.py
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def vec(mat):
|
|
5
|
-
"""
|
|
6
|
-
Stack the columns of `mat` into a column vector. If mat is a M x N matrix,
|
|
7
|
-
then vec(mat) is an MN X 1 vector.
|
|
8
|
-
|
|
9
|
-
Parameters
|
|
10
|
-
----------
|
|
11
|
-
mat: numpy.array
|
|
12
|
-
"""
|
|
13
|
-
vec_mat = mat.reshape((-1, 1), order='F')
|
|
14
|
-
return vec_mat
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def vec_quad_form(mat):
|
|
18
|
-
"""
|
|
19
|
-
`vec` operation for quadratic forms
|
|
20
|
-
|
|
21
|
-
Parameters
|
|
22
|
-
----------
|
|
23
|
-
mat: numpy.array
|
|
24
|
-
"""
|
|
25
|
-
return vec(np.outer(mat, mat))
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def commutation_matrix(shape):
|
|
29
|
-
"""
|
|
30
|
-
Generates the commutation matrix for a matrix with shape equal to `shape`.
|
|
31
|
-
|
|
32
|
-
The definition of a commutation matrix `k` is:
|
|
33
|
-
k @ vec(mat) = vec(mat.T)
|
|
34
|
-
|
|
35
|
-
Parameters
|
|
36
|
-
----------
|
|
37
|
-
shape : tuple
|
|
38
|
-
2-d tuple (m, n) with the shape of `mat`
|
|
39
|
-
"""
|
|
40
|
-
m, n = shape
|
|
41
|
-
w = np.arange(m * n).reshape((m, n), order="F").T.ravel(order="F")
|
|
42
|
-
k = np.eye(m * n)[w, :]
|
|
43
|
-
return k
|
pyacm-0.4.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
pyacm/__init__.py,sha256=pRFuR3Au_ybQAmkJduGrLMKGJd1pxjhGfhsfsjlK-mU,66
|
|
2
|
-
pyacm/acm.py,sha256=5UxKhc5lptSE-K99Uo3b6hMkIfM-5y1P01qeb_K5xW0,14491
|
|
3
|
-
pyacm/utils.py,sha256=-PmH9L3LpzqUP-QU5BHisoLSBYrq-3PaPgR-W1sS1z8,904
|
|
4
|
-
pyacm-0.4.dist-info/LICENSE,sha256=YbUXx25Z6PzP4k4rsbs6tN58NiCwGIIrTMzql4iTeDs,1073
|
|
5
|
-
pyacm-0.4.dist-info/METADATA,sha256=eRotNIVK0skIEzKi4f0u2bnInhd_Ap2CuStomM-KliU,4164
|
|
6
|
-
pyacm-0.4.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
7
|
-
pyacm-0.4.dist-info/top_level.txt,sha256=xQy7q1eSKCnRtTnwb-Iz_spT0UDNdTyzKd43yz-ffrI,6
|
|
8
|
-
pyacm-0.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|