library(learnr) library(PortfolioAnalytics) library(ROI) library(ROI.plugin.quadprog) library(tidyverse) library(tidyquant) library(ati) library(timetk) tutorial_options(exercise.eval = TRUE) knitr::opts_chunk$set(error = TRUE,warning=FALSE) library(foreach)
removals<-ls() rm(list=removals) # Always good practice to remove all object before beginning new work library(learnr) library(PortfolioAnalytics) library(ROI) library(ROI.plugin.quadprog) library(tidyverse) library(tidyquant) library(ati) library(timetk) tutorial_options(exercise.eval = TRUE) knitr::opts_chunk$set(error = TRUE, echo = TRUE,warning=FALSE, exercise=TRUE) library(foreach) registerDoSEQ()
Before you begin, run the following to update the packages. The install.packages command may not be necessary depending on your local
R libs
.
remove.packages('ati') remotes::install_github('barryquinn1/ati') remotes::install_github('rstudio/learnr') install.packages(c("foreach","iterators",'quadprog',"ROI","ROI.plugin.quadprog")) .rs.restartR()
In the previous exercises we considered a Marcenko-Pastur function which predefined the variance parameter. A more accuracy approach is to optimise the estimate of the Marcenko-Pastur distribution from the data underlying data then adjust the covariance matrix for the noise identified by this random matrix theory, the function estRMT()
does exactly this.
estRMT
algorithmThe main idea behind de-noising the covariance matrix is to eliminate the eigenvalues of the covariance matrix that are representing noise and not useful information.
This is done by determining the maximum theoretical value of the eigenvalue of such matrix as a threshold and then setting all the calculated eigenvalues above the threshold to the same value.
The function provided below for de-noising the covariance works as follows:
For example, we have a set of 5 sorted eigenvalues $(\lambda_1, \lambda_2, ....., \lambda_5)$ two of which are above the maximum theoretical value, then we set $$\lambda_4^{NEW}=\lambda_5^{NEW}=\frac{\lambda_4^{OLD}+\lambda_5^{OLD}}{2}$$ Eigenvalues above the maximum theoretical value are left intact. $$\lambda_1^{NEW}=\lambda_1^{OLD}\\lambda_2^{NEW}=\lambda_2^{OLD}\\lambda_3^{NEW}=\lambda_3^{OLD}$$
$$\tilde{C}=W \Lambda W$$ * To rescale $\tilde{C}$ so that the main diagonal consists of 1s the following transformation is made. This is how the final $C_{denoised}$ is obtained.
$$C_{denoised}=\tilde{C}[(diag[\tilde{C}])^{1/2}(diag[\tilde{C}])^{1/2'}]^{-1}$$ * The new correlation matrix is then transformed back to the new de-noised covariance matrix.
In this tutorial, you will learn some portfolio analytics
A first step would be to rearrange the data to that price and market value are variables
# Data and covariance objects ftse350<-ati::ftse350 ftse350 %>% select(-Name) %>% spread(variable,value) %>% group_by(ticker) %>% summarise(mean_mv=mean(`Market Value`)) %>% mutate(rank = min_rank(desc(mean_mv))) %>% filter(rank<=25) %>% select(ticker) %>% unlist(use.names = F) -> tickers
ftse350 %>% select(-Name) %>% spread(variable,value) %>% group_by(ticker) %>% summarise(mean_mv=mean(`Market Value`)) %>% mutate(rank = min_rank(desc(mean_mv))) %>% filter(rank<=25) %>% select(ticker) %>% unlist(use.names = F) -> tickers
ftse350 %>% select(-Name) %>% spread(variable,value) %>% filter(ticker %in% tickers) %>% group_by(ticker) %>% tq_transmute(select = Price, mutate_fun = monthlyReturn) %>% pivot_wider(names_from=ticker, values_from=monthly.returns)->ftse_r_m
Hint: Look at the help file for the estRMT()
function and estimate an denoised covariance matrix where the eigenvalues below the threshold are replaced with the average value.
ftse_r_m %>% tk_xts(silent = TRUE)->ftse_r_m_ts
model<-estRMT(ftse_r_m_ts)
heatmap()
ati::ftse25_rtns_mthly ->ftse_r_m
cov_denoise<-model$cov cov_raw<-cov(ftse_r_m[,-1]) heatmap(cov_raw); heatmap(cov_denoise)
As we can see, the main diagonal has not changed, but the other covariances are different. This means that the algorithm has affected those eigenvalues of the correlation matrix which have more noise associated with them.
De-noised correlation matrix from the previous methods can also be de-toned by excluding a number of first eigenvectors representing the market component. According to Marcos Lopez de Prado (2020)
Financial correlation matrices usually incorporate a market component. The market component is characterized by the first eigenvector, with loadings $W_{n,1}\approx N^{-\frac{1}{2}}, n = 1, ..., N.$ Accordingly, a market component affects every item of the covariance matrix. In the context of clustering applications, it is useful to remove the market component, if it exists (a hypothesis that can be tested statistically). By removing the market component, we allow a greater portion of the correlation to be explained by components that affect specific subsets of the securities. It is similar to removing a loud tone that prevents us from hearing other sounds. The detoned correlation matrix is singular, as a result of eliminating (at least) one eigenvector. This is not a problem for clustering applications, as most approaches do not require the invertibility of the correlation matrix. Still, a detoned correlation matrix $C_{detoned}$ cannot be used directly for mean-variance portfolio optimization”*
The de-toning function works as follows:
$$\hat{C}=C_{denoised} - W_m\Lambda_mW_m^{'}$$
$$C_{detoned}=\hat{C}[(diag[\hat{C}])^{1/2}(diag[\hat{C}])^{1/2'}]^{-1}$$
One can apply de-toning to the covariance matrix by setting the detone parameter to True in estRMT
function. Note that detoning will always be used with either of the denoising methods described before.
ati::ftse25_rtns_mthly->ftse_r_m ftse_r_m %>% tk_xts(silent = TRUE)->ftse_r_m_ts model<-estRMT(ftse_r_m_ts) cov_denoise<-model$cov
model1<-estRMT(ftse_r_m_ts,detone = TRUE) model1$cov -> cov_detone heatmap(cov_denoise,main ="Simple Covariance");heatmap(cov_detone,main = "Detoned Covariance")
The results of de-toning are significantly different from the de-noising results. Notice how the axis labels have shifted. This indicates that the deleted market component had an effect on the covariance between elements.
The major workhorse of this chapter is the portfolioAnalytics
package developed by Peterson and Carl (2018).
Assume no third/fourth order effects.
custom.portfolio.moments <- function(R, portfolio) { momentargs<-list() momentargs$mu<-matrix(as.vector(apply(R,2, "mean")), ncol = 1) momentargs$sigma<-estRMT(R, parallel=FALSE)$cov momentargs$m3 <- matrix(0, nrow=ncol(R), ncol=ncol(R)^2) momentargs$m4 <- matrix(0, nrow=ncol(R), ncol=ncol(R)^3) return(momentargs) }
Use the package PortfolioAnalytics
we will construct a portfolio with the following specification.
1. No short sales are allowed.
2. All cash needs to be invested at all times.
3. Set the objective to maximize the quadratic utility which maximizes returns while controlling for risk.
pspec.lo <- portfolio.spec(assets = colnames(ftse_r_m_ts)) # Specification 1 and 2 pspec.lo <- add.constraint(pspec.lo, type="full_investment") pspec.lo <- add.constraint(pspec.lo, type="long_only") # Specification 3 pspec.lo <- add.objective(portfolio=pspec.lo, type="return", name="mean") pspec.lo <- add.objective(portfolio=pspec.lo, type="risk", name="var")
library(ROI) library(ROI.plugin.quadprog) library(foreach) foreach::registerDoSEQ()
library(timetk) ati::ftse25_rtns_mthly->ftse_r_m ftse_r_m %>% tk_xts(silent = TRUE)->ftse_r_m_ts custom.portfolio.moments <- function(R, portfolio) { momentargs<-list() momentargs$mu<-matrix(as.vector(apply(R,2, "mean")), ncol = 1) momentargs$sigma<-estRMT(R, parallel=FALSE)$cov momentargs$m3 <- matrix(0, nrow=ncol(R), ncol=ncol(R)^2) momentargs$m4 <- matrix(0, nrow=ncol(R), ncol=ncol(R)^3) return(momentargs) } pspec.lo <- portfolio.spec(assets = colnames(ftse_r_m_ts)) # Specification 1 and 2 pspec.lo <- add.constraint(pspec.lo, type="full_investment") pspec.lo <- add.constraint(pspec.lo, type="long_only") # Specification 3 pspec.lo <- add.objective(portfolio=pspec.lo, type="return", name="mean") pspec.lo <- add.objective(portfolio=pspec.lo, type="risk", name="var")
registerDoSEQ() opt.ordinary <- optimize.portfolio.rebalancing( ftse_r_m_ts,pspec.lo, optimize_method="ROI", rebalance_on='months', training_period=30, trailing_periods=30) system.time( opt.rmt <-optimize.portfolio.rebalancing( ftse_r_m_ts, pspec.lo, optimize_method="ROI", momentFUN = "custom.portfolio.moments", rebalance_on="months", training_period=30, trailing_periods=30) )
ati::ftse25_rtns_mthly->ftse_r_m ftse_r_m %>% tk_xts(silent = TRUE)->ftse_r_m_ts
ordinary.wts <- na.omit(extractWeights(opt.ordinary)) rmt.wts <- na.omit(extractWeights(opt.rmt)) ordinary <- Return.rebalancing(R=ftse_r_m_ts,weights=ordinary.wts) rmt <- Return.rebalancing(R=ftse_r_m_ts,weights=rmt.wts) rmt.strat.rets <- merge.xts(ordinary,rmt) colnames(rmt.strat.rets) <- c("ordinary", "rmt")
Chart the results
charts.PerformanceSummary(rmt.strat.rets, wealth.index = T, colorset = c("red","darkgrey"), main="Comparison of Portfolio Performance",cex.legend = 1.1,cex.axis = 1.1,legend.loc = "topleft")
This is clear evidence that a denoised portfolio which is rebalanced monthly performs better than a portfolio which is optimised on the ordinary noisy returns. While both cumulative returns follow a similar time series path, the denoised portfolio returns experience much less drawdown than the ordinary returns portfolio.
Peterson, Brian G., and Peter Carl. 2018. PortfolioAnalytics: Portfolio Analysis, Including Numerical Methods for Optimization of Portfolios. https://github.com/braverock/PortfolioAnalytics.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.