33
44from numpy .linalg import inv
55from sklearn .decomposition import PCA
6+ from statsmodels .tools .tools import add_constant
67
78from 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
1113class 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
0 commit comments