Skip to content

Commit fa1a073

Browse files
committed
Progress. The TODOs still remain
1 parent 50d3898 commit fa1a073

File tree

3 files changed

+97
-119
lines changed

3 files changed

+97
-119
lines changed

example_us.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,28 @@
1616
selected_maturities=[6, 12, 24, 36, 48, 60, 72, 84, 96, 108, 120]
1717
)
1818

19-
acm.tp[119].plot()
19+
# CHART
20+
size = 7
21+
fig = plt.figure(figsize=(size * (16 / 7.3), size))
22+
23+
ax = plt.subplot2grid((1, 2), (0, 0))
24+
ax.plot(ylds_d[120], label="Actual Yield", lw=1)
25+
ax.plot(acm.miy[120], label="Fitted Yield", lw=1, ls='--')
26+
ax.set_title("10-Year Model Fit")
27+
ax.xaxis.grid(color="grey", linestyle="-", linewidth=0.5, alpha=0.5)
28+
ax.yaxis.grid(color="grey", linestyle="-", linewidth=0.5, alpha=0.5)
29+
ax.tick_params(rotation=90, axis="x")
30+
ax.legend(loc="upper right")
31+
32+
ax = plt.subplot2grid((1, 2), (0, 1))
33+
ax.plot(ylds_d[120], label="Yield", lw=1)
34+
ax.plot(acm.rny[120], label="Risk Neutral Yield", lw=1)
35+
ax.plot(acm.tp[120], label="Term Premium", lw=1)
36+
ax.set_title("10-Year Yield Decomposition")
37+
ax.xaxis.grid(color="grey", linestyle="-", linewidth=0.5, alpha=0.5)
38+
ax.yaxis.grid(color="grey", linestyle="-", linewidth=0.5, alpha=0.5)
39+
ax.tick_params(rotation=90, axis="x")
40+
ax.legend(loc="upper right")
41+
42+
plt.tight_layout()
2043
plt.show()

pyacm/acm.py

Lines changed: 72 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33

44
from numpy.linalg import inv
55
from sklearn.decomposition import PCA
6+
from statsmodels.tools.tools import add_constant
67

78
from pyacm.utils import vec, vec_quad_form, commutation_matrix
89

910
# TODO Curve in daily frequency could be None
1011
# TODO Make sure it works for DI Futures
12+
# TODO make sure data is accepted as decimals
1113
class NominalACM:
1214
"""
1315
This class implements the model from the article:
@@ -137,6 +139,7 @@ def __init__(
137139

138140
# TODO assert columns of daily and monthly are the same
139141
# TODO assert monthly index frequency
142+
# TODO assert columns are consecutive integers
140143

141144
self.n_factors = n_factors
142145
self.curve = curve
@@ -163,21 +166,36 @@ def __init__(
163166

164167
# 3rd Step - Convexity-adjusted price of risk
165168
self.lambda0, self.lambda1, self.mu_star, self.phi_star = self._retrieve_lambda()
166-
# TODO EVERYTHING RIGHT UP TO HERE
167169

168-
# TODO PAREI AQUI - Olhar em que situações pode-se usar os yield mensais.
169-
if self.curve.index.freqstr == 'M':
170-
X = self.pc_factors_m
171-
r1 = self.curve_monthly.iloc[:, 0]
172-
else:
173-
X = self.pc_factors_d
174-
r1 = self.curve.iloc[:, 0]
170+
# Short Rate Equation
171+
self.delta0, self.delta1 = self._short_rate_equation(
172+
r1=self.curve_monthly.iloc[:, 0],
173+
X=self.pc_factors_m,
174+
)
175+
176+
# Affine Yield Coefficients
177+
self.A, self.B = self._affine_coefficients(
178+
lambda0=self.lambda0,
179+
lambda1=self.lambda1,
180+
)
181+
182+
# Risk-Neutral Coefficients
183+
self.Arn, self.Brn = self._affine_coefficients(
184+
lambda0=np.zeros(self.lambda0.shape),
185+
lambda1=np.zeros(self.lambda1.shape),
186+
)
175187

176-
self.miy = self._affine_recursions(self.lambda0, self.lambda1, X, r1)
177-
self.rny = self._affine_recursions(0, 0, X, r1)
188+
# Model Implied Yield
189+
self.miy = self._compute_yields(self.A, self.B)
190+
191+
# Risk Neutral Yield
192+
self.rny = self._compute_yields(self.Arn, self.Brn)
193+
194+
# Term Premium
178195
self.tp = self.miy - self.rny
179-
self.er_loadings, self.er_hist_m, self.er_hist_d = self._expected_return()
180-
self.z_lambda, self.z_beta = self._inference()
196+
197+
# Expected Return
198+
self.er_loadings, self.er_hist = self._expected_return()
181199

182200
def fwd_curve(self, date=None):
183201
"""
@@ -340,34 +358,43 @@ def _retrieve_lambda(self):
340358

341359
return lambda0, lambda1, muStar, phiStar
342360

343-
def _affine_recursions(self, lambda0, lambda1, X_in, r1):
344-
X = X_in.T.values[:, 1:]
345-
r1 = vec(r1.values)[-X.shape[1]:, :]
346-
347-
A = np.zeros((1, self.n))
348-
B = np.zeros((self.n_factors, self.n))
349-
350-
delta = r1.T @ np.linalg.pinv(np.vstack((np.ones((1, X.shape[1])), X)))
351-
delta0 = delta[[0], [0]]
352-
delta1 = delta[[0], 1:]
353-
354-
A[0, 0] = - delta0
355-
B[:, 0] = - delta1
356-
357-
for i in range(self.n - 1):
358-
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
359-
B[:, i + 1] = B[:, i] @ (self.phi - lambda1) - delta1
360-
361-
# Construct fitted yields
362-
ttm = np.arange(1, self.n + 1) / 12
363-
fitted_log_prices = (A.T + B.T @ X).T
364-
fitted_yields = - fitted_log_prices / ttm
365-
fitted_yields = pd.DataFrame(
366-
data=fitted_yields,
367-
index=self.curve.index[1:],
361+
@staticmethod
362+
def _short_rate_equation(r1, X):
363+
r1 = r1 / 1200 # TODO remove the 100
364+
X = add_constant(X)
365+
Delta = inv(X.T @ X) @ X.T @ r1
366+
delta0 = Delta.iloc[0]
367+
delta1 = Delta.iloc[1:].values
368+
return delta0, delta1
369+
370+
def _affine_coefficients(self, lambda0, lambda1):
371+
lambda0 = lambda0.reshape(-1, 1)
372+
373+
A = np.zeros(self.n)
374+
B = np.zeros((self.n, self.n_factors))
375+
376+
A[0] = - self.delta0
377+
B[0, :] = - self.delta1
378+
379+
for n in range(1, self.n):
380+
Bpb = np.kron(B[n - 1, :], B[n - 1, :])
381+
s0term = 0.5 * (Bpb @ self.s0 + self.omega[0, 0])
382+
383+
A[n] = A[n - 1] + B[n - 1, :] @ (self.mu - lambda0) + s0term + A[0]
384+
B[n, :] = B[n - 1, :] @ (self.phi - lambda1) + B[0, :]
385+
386+
return A, B
387+
388+
def _compute_yields(self, A, B):
389+
A = A.reshape(-1, 1)
390+
multiplier = np.tile(self.curve.columns / 12, (self.t_d, 1)).T / 100 # TODO remove the 100
391+
yields = (- ((np.tile(A, (1, self.t_d)) + B @ self.pc_factors_d.T) / multiplier).T).values
392+
yields = pd.DataFrame(
393+
data=yields,
394+
index=self.curve.index,
368395
columns=self.curve.columns,
369396
)
370-
return fitted_yields
397+
return yields
371398

372399
def _expected_return(self):
373400
"""
@@ -378,93 +405,20 @@ def _expected_return(self):
378405
expected returns
379406
"""
380407
stds = self.pc_factors_m.std().values[:, None].T
381-
er_loadings = (self.beta.T @ self.lambda1) * stds
408+
er_loadings = (self.B @ self.lambda1) * stds
382409
er_loadings = pd.DataFrame(
383410
data=er_loadings,
384411
columns=self.pc_factors_m.columns,
385-
index=self.selected_maturities,
412+
index=range(1, self.n + 1),
386413
)
387414

388-
# Monthly
389-
exp_ret = (self.beta.T @ (self.lambda1 @ self.pc_factors_m.T + self.lambda0)).values
390-
conv_adj = np.diag(self.beta.T @ self.Sigma @ self.beta) + self.sigma2
391-
er_hist = (exp_ret + conv_adj[:, None]).T
392-
er_hist_m = pd.DataFrame(
393-
data=er_hist,
394-
index=self.pc_factors_m.index,
395-
columns=self.curve.columns[:er_hist.shape[1]]
396-
)
397-
398-
# Higher frequency
399-
exp_ret = (self.beta.T @ (self.lambda1 @ self.pc_factors_d.T + self.lambda0)).values
400-
conv_adj = np.diag(self.beta.T @ self.Sigma @ self.beta) + self.sigma2
415+
# Historical estimate
416+
exp_ret = (self.B @ (self.lambda1 @ self.pc_factors_d.T + self.lambda0.reshape(-1, 1))).values
417+
conv_adj = np.diag(self.B @ self.Sigma @ self.B.T) + self.omega[0, 0]
401418
er_hist = (exp_ret + conv_adj[:, None]).T
402419
er_hist_d = pd.DataFrame(
403420
data=er_hist,
404421
index=self.pc_factors_d.index,
405-
columns=self.curve.columns[:er_hist.shape[1]]
422+
columns=self.curve.columns,
406423
)
407-
408-
return er_loadings, er_hist_m, er_hist_d
409-
410-
def _inference(self):
411-
# TODO I AM NOT SURE THAT THIS SECTION IS CORRECT
412-
413-
# Auxiliary matrices
414-
Z = self.pc_factors_m.copy().T
415-
Z = Z.values[:, 1:]
416-
Z = np.vstack((np.ones((1, self.t)), Z))
417-
418-
Lamb = np.hstack((self.lambda0, self.lambda1))
419-
420-
rho1 = np.zeros((self.n_factors + 1, 1))
421-
rho1[0, 0] = 1
422-
423-
A_beta = np.zeros((self.n_factors * self.beta.shape[1], self.beta.shape[1]))
424-
425-
for ii in range(self.beta.shape[1]):
426-
A_beta[ii * self.beta.shape[0]:(ii + 1) * self.beta.shape[0], ii] = self.beta[:, ii]
427-
428-
BStar = np.squeeze(np.apply_along_axis(vec_quad_form, 1, self.beta.T))
429-
430-
comm_kk = commutation_matrix(shape=(self.n_factors, self.n_factors))
431-
comm_kn = commutation_matrix(shape=(self.n_factors, self.beta.shape[1]))
432-
433-
# Assymptotic variance of the betas
434-
v_beta = self.sigma2 * np.kron(np.eye(self.beta.shape[1]), inv(self.Sigma))
435-
436-
# Assymptotic variance of the lambdas
437-
upsilon_zz = (1 / self.t) * Z @ Z.T
438-
v1 = np.kron(inv(upsilon_zz), self.Sigma)
439-
v2 = self.sigma2 * np.kron(inv(upsilon_zz), inv(self.beta @ self.beta.T))
440-
v3 = self.sigma2 * np.kron(Lamb.T @ self.Sigma @ Lamb, inv(self.beta @ self.beta.T))
441-
442-
v4_sim = inv(self.beta @ self.beta.T) @ self.beta @ A_beta.T
443-
v4_mid = np.kron(np.eye(self.beta.shape[1]), self.Sigma)
444-
v4 = self.sigma2 * np.kron(rho1 @ rho1.T, v4_sim @ v4_mid @ v4_sim.T)
445-
446-
v5_sim = inv(self.beta @ self.beta.T) @ self.beta @ BStar
447-
v5_mid = (np.eye(self.n_factors ** 2) + comm_kk) @ np.kron(self.Sigma, self.Sigma)
448-
v5 = 0.25 * np.kron(rho1 @ rho1.T, v5_sim @ v5_mid @ v5_sim.T)
449-
450-
v6_sim = inv(self.beta @ self.beta.T) @ self.beta @ np.ones((self.beta.shape[1], 1))
451-
v6 = 0.5 * (self.sigma2 ** 2) * np.kron(rho1 @ rho1.T, v6_sim @ v6_sim.T)
452-
453-
v_lambda_tau = v1 + v2 + v3 + v4 + v5 + v6
454-
455-
c_lambda_tau_1 = np.kron(Lamb.T, inv(self.beta @ self.beta.T) @ self.beta)
456-
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))
457-
c_lambda_tau = - c_lambda_tau_1 @ comm_kn @ v_beta @ c_lambda_tau_2.T
458-
459-
v_lambda = v_lambda_tau + c_lambda_tau + c_lambda_tau.T
460-
461-
# extract the z-tests
462-
sd_lambda = np.sqrt(np.diag(v_lambda).reshape(Lamb.shape, order='F'))
463-
sd_beta = np.sqrt(np.diag(v_beta).reshape(self.beta.shape, order='F'))
464-
465-
z_beta = pd.DataFrame(self.beta / sd_beta, index=self.pc_factors_m.columns, columns=self.selected_maturities).T
466-
z_lambda = pd.DataFrame(Lamb / sd_lambda, index=self.pc_factors_m.columns, columns=[f"lambda {i}" for i in range(Lamb.shape[1])])
467-
468-
return z_lambda, z_beta
469-
470-
424+
return er_loadings, er_hist_d

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
'numpy',
2727
'pandas',
2828
'scikit-learn',
29+
'statsmodels',
2930
'tqdm',
3031
],
3132
keywords=[

0 commit comments

Comments
 (0)