<- c("SP500", "DTWEXAFEGS") # "SP500" does not contain dividends; note: "DTWEXM" discontinued as of Jan 2020
factors_r <- c("DGS10", "BAMLH0A0HYM2") factors_d
Decomposition
Underlying returns are structural bets that can be analyzed through dimension reduction techniques such as principal components analysis (PCA). Most empirical studies apply PCA to a covariance matrix (note: for multi-asset portfolios, use the correlation matrix because asset-class variances are on different scales) of equity returns (yield changes) and find that movements in the equity markets (yield curve) can be explained by a subset of principal components. For example, the yield curve can be decomposed in terms of shift, twist, and butterfly, respectively.
\[ \begin{aligned} \boldsymbol{\Sigma}&=\lambda_{1}\mathbf{v}_{1}\mathbf{v}_{1}^\mathrm{T}+\lambda_{2}\mathbf{v}_{2}\mathbf{v}_{2}^\mathrm{T}+\cdots+\lambda_{k}\mathbf{v}_{k}\mathbf{v}_{k}^\mathrm{T}\\ &=V\Lambda V^{\mathrm{T}} \end{aligned} \]
<- function(x, comps) {
eigen_decomp
<- eigen(cov(x))
LV <- LV[["values"]][1:comps]
L <- LV[["vectors"]][ , 1:comps]
V
<- V %*% sweep(t(V), 1, L, "*")
result
return(result)
}
<- 1 comps
eigen_decomp(overlap_xts, comps) * scale[["periods"]] * scale[["overlap"]]
[,1] [,2] [,3] [,4]
[1,] 0.0299142972 -3.640577e-03 -1.434018e-04 2.561930e-03
[2,] -0.0036405768 4.430590e-04 1.745203e-05 -3.117874e-04
[3,] -0.0001434018 1.745203e-05 6.874328e-07 -1.228126e-05
[4,] 0.0025619298 -3.117874e-04 -1.228126e-05 2.194096e-04
# cov(overlap_xts) * scale[["periods"]] * scale[["overlap"]]
Variance
We often look at the proportion of variance explained by the first \(i\) principal components as an indication of how many components are needed.
\[ \begin{aligned} \frac{\sum_{j=1}^{i}{\lambda_{j}}}{\sum_{j=1}^{k}{\lambda_{j}}} \end{aligned} \]
<- function(x) {
variance_explained
<- eigen(cov(x))
LV <- LV[["values"]]
L
<- cumsum(L) / sum(L)
result
return(result)
}
variance_explained(overlap_xts)
[1] 0.8737215 0.9921592 0.9982626 1.0000000
Similarity
Also, a challenge of rolling PCA is to try to match the eigenvectors: may need to change the sign and order.
\[ \begin{aligned} \text{similarity}=\frac{\mathbf{v}_{t}\cdot\mathbf{v}_{t-1}}{\|\mathbf{v}_{t}\|\|\mathbf{v}_{t-1}\|} \end{aligned} \]
<- function(V, V0) {
similarity
<- ncol(V)
n_cols_v <- ncol(V0)
n_cols_v0 <- matrix(0, nrow = n_cols_v, ncol = n_cols_v0)
result
for (i in 1:n_cols_v) {
for (j in 1:n_cols_v0) {
<- crossprod(V[ , i], V0[ , j]) /
result[i, j] sqrt(crossprod(V[ , i]) * crossprod(V0[ , j]))
}
}
return(result)
}
<- function(x, width, comp) {
roll_eigen1
<- nrow(x)
n_rows <- list()
result_ls
for (i in width:n_rows) {
<- max(i - width + 1, 1):i
idx
<- eigen(cov(x[idx, ]))
LV <- LV[["vectors"]]
V
<- append(result_ls, list(V[ , comp]))
result_ls
}
<- do.call(rbind, result_ls)
result <- xts(result, index(x)[width:n_rows])
result colnames(result) <- colnames(x)
return(result)
}
<- 1 comp
<- roll_eigen1(overlap_xts, width, comp) raw_df
# # install.packages("devtools")
# devtools::install_github("jasonjfoster/rolleigen") # roll (>= 1.1.7)
# library(rolleigen)
# raw_df <- roll_eigen(overlap_xts, width, order = TRUE)[["vectors"]][ , comp, ]
# raw_df <- xts(t(raw_df), index(overlap_xts))
# colnames(raw_df) <- colnames(overlap_xts)
<- function(x, width, comp) {
roll_eigen2
<- nrow(x)
n_rows <- list()
V_ls <- list()
result_ls
for (i in width:n_rows) {
<- max(i - width + 1, 1):i
idx
<- eigen(cov(x[idx, ]))
LV <- LV[["vectors"]]
V
if (i > width) {
# cosine <- crossprod(V, V_ls[[length(V_ls)]])
<- similarity(V, V_ls[[length(V_ls)]])
cosine <- apply(abs(cosine), 1, which.max)
order <- t(sign(diag(cosine[ , order])) * t(V[ , order]))
V
}
<- append(V_ls, list(V))
V_ls <- append(result_ls, list(V[ , comp]))
result_ls
}
<- do.call(rbind, result_ls)
result <- xts(result, index(x)[width:n_rows])
result colnames(result) <- colnames(x)
return(result)
}
<- roll_eigen2(overlap_xts, width, comp) clean_df
Implied shocks
Product of the \(n\)th eigenvector and square root of the \(n\)th eigenvalue:
<- function(x, width, comp) {
roll_shocks
<- nrow(x)
n_rows <- list()
V_ls <- list()
result_ls
for (i in width:n_rows) {
<- max(i - width + 1, 1):i
idx
<- eigen(cov(x[idx, ]))
LV <- LV[["values"]]
L <- LV[["vectors"]]
V
if (length(V_ls) > 1) {
# cosine <- crossprod(V, V_ls[[length(V_ls)]])
<- similarity(V, V_ls[[length(V_ls)]])
cosine <- apply(abs(cosine), 1, which.max)
order <- L[order]
L <- t(sign(diag(cosine[ , order])) * t(V[ , order]))
V
}
<- sqrt(L[comp]) * V[ , comp]
shocks <- append(V_ls, list(V))
V_ls <- append(result_ls, list(shocks))
result_ls
}
<- do.call(rbind, result_ls)
result <- xts(result, index(x)[width:n_rows])
result colnames(result) <- colnames(x)
return(result)
}
<- roll_shocks(overlap_xts, width, comp) * sqrt(scale[["periods"]] * scale[["overlap"]]) shocks_xts