Intro to CriticalLineAlgo

Alejandro Sotolongo

2020-01-22

#devtools::install_github('alejandro-sotolongo/CriticalLineAlgo')
library(CriticalLineAlgo)

The critical line algorithm (CLA) was developed by Harry Markowitz to solve the corner portfolios of the efficiency frontier in his famous 1952 Portfolio Selection research. An open source version for Python exists and is outlined in the 2013 paper below. CriticalLineAlgo is a lightweight R implementation with minimal imports. The only outside package is MASS for general (Moore-Penrose) matrix inversion.

This package mostly follows the notation of Bailey and Lopez de Prado 2013 and Niedermayer and Niedermayer 2006.

There are two main functions to interface with: run_cla and calc_target_vol. The run_cla function solves for the corner portfolios and output a list with the stored output. The calc_target_vol function takes the list output from run_cla and finds the weights of an efficient portfolio for a specific level of volatility.

In order to run the CLA a mu_vec of estimated returns and cov_mat of estimated covariance are needed. This introduction reads the mu_vec and cov_mat from csv files stored in Google Drive. The estimates are historical mean and cov estimates from 16 ETF time-series to represent different asset classes. The mu_vec and cov_mat are not predictions of the future. Instead they are used here to demonstrate how this package works.

mu_data <- read.csv(sprintf('https://docs.google.com/uc?id=%s&export=download', 
                           '1vUPvKqJSR-3ZlWTWwf1r2kjguEDqmDM6'), 
                   row.names = NULL)
cov_data <- read.csv(sprintf('https://docs.google.com/uc?id=%s&export=download', 
                           '1g53v1D9mXvfcYKMsquHIj2CF7Lrn6-GX'),
                    row.names = NULL)
asset_class <- mu_data$ETF
mu_vec <- mu_data$mu
cov_mat <- as.matrix(cov_data[, 2:ncol(cov_data)])
mu_data
#>    ETF         mu
#> 1  IVV 0.14335496
#> 2  IWM 0.12231009
#> 3  VTV 0.13113963
#> 4  IWF 0.16094526
#> 5  EFA 0.05844601
#> 6  VWO 0.02959126
#> 7  TLT 0.05983401
#> 8  IEF 0.03455211
#> 9  EMB 0.05410923
#> 10 LQD 0.05320704
#> 11 HYG 0.05960452
#> 12 VNQ 0.10600503
#> 13 AMJ 0.01488107
#> 14 IGF 0.07625922
#> 15 GNR 0.01742278
#> 16 GLD 0.01517350

run_cla is the interface that runs the algorithm and returns the results. The mu_vec and cov_mat are passed in as the first two arguments.

store <- run_cla(mu_vec, cov_mat)

The $wgt_list in the store output contains the weights of the corner portfolios. These weights can easily be combined and used to calculate the corner portfolios’ expected return and volatility.

corner_wgt <- do.call(cbind, store$wgt_list)
exp_ret <- apply(corner_wgt, 2, function(m, w) {t(w) %*% m}, m = mu_vec)
exp_vol <- apply(corner_wgt, 2, function(covar, w) {sqrt(t(w) %*% covar %*% w)},
                 covar = cov_mat)
plot(sqrt(diag(cov_mat)), mu_vec, col = 'darkgrey', 
     ylab = 'Estimated Return', xlab = 'Estimated Volatility')
points(exp_vol, exp_ret, col = 'darkgreen', lwd = 2, type = 'b')
title('Efficiency Frontier Example')
mtext('Corner Portfolios in Green, Individual Assets in Grey')

rownames(corner_wgt) <- asset_class
colnames(corner_wgt) <- paste('Vol ', round(exp_vol * 100, 1))
round(corner_wgt * 100, 1)
#>     Vol  15 Vol  15 Vol  8.3 Vol  6.8 Vol  6.1 Vol  5 Vol  4.8 Vol  4.8
#> IVV       0       0      0.0      0.0      0.0    0.0      0.0      0.0
#> IWM       0       0      0.0      0.0      0.0    0.0      0.0      0.0
#> VTV       0       0      0.0      0.0      0.0    2.0      4.4      5.0
#> IWF     100     100     59.3     44.3     39.0   28.6     25.2     24.2
#> EFA       0       0      0.0      0.0      0.0    0.0      0.0      0.0
#> VWO       0       0      0.0      0.0      0.0    0.0      0.0      0.0
#> TLT       0       0     40.7     23.9     14.8    0.0      0.0      0.0
#> IEF       0       0      0.0      0.0     14.8   39.5     42.6     43.3
#> EMB       0       0      0.0      0.0      0.0    0.0      0.0      0.6
#> LQD       0       0      0.0     31.8     31.4   29.9     27.9     26.9
#> HYG       0       0      0.0      0.0      0.0    0.0      0.0      0.0
#> VNQ       0       0      0.0      0.0      0.0    0.0      0.0      0.0
#> AMJ       0       0      0.0      0.0      0.0    0.0      0.0      0.0
#> IGF       0       0      0.0      0.0      0.0    0.0      0.0      0.0
#> GNR       0       0      0.0      0.0      0.0    0.0      0.0      0.0
#> GLD       0       0      0.0      0.0      0.0    0.0      0.0      0.0
#>     Vol  4.3 Vol  3.9 Vol  3.9 Vol  3.8
#> IVV      0.0      5.5      5.7      0.0
#> IWM      0.0      0.0      0.0      0.0
#> VTV      7.2      7.0      7.0      9.3
#> IWF     14.1      0.9      0.0      0.0
#> EFA      0.0      0.0      0.0      0.0
#> VWO      0.0      0.0      0.0      0.0
#> TLT      0.0      0.0      0.0      0.0
#> IEF     51.1     59.6     59.5     58.7
#> EMB      2.4      4.1      4.1      4.3
#> LQD     13.9      0.0      0.0      0.0
#> HYG     11.3     22.9     23.6     27.6
#> VNQ      0.0      0.0      0.0      0.0
#> AMJ      0.0      0.0      0.0      0.0
#> IGF      0.0      0.0      0.0      0.0
#> GNR      0.0      0.0      0.0      0.0
#> GLD      0.0      0.0      0.0      0.0

run_cla supports adding lower and upper bound weight limits. This version of the CLA has a long-only constraint (i.e., the sum of the portfolio weights = 1). There are two ways of entering asset constraints. A single numeric value will be applied to each asset, e.g., entering 0.5 as the upper bound constraint will limit every asset to a maximum weight of 50%. Alternatively, a vector of weights that correspond to each asset in the mu_vec and cov_mat can be entered as input to allow for different constraints for each asset. The code below is an example of the latter: a constraint of 50% will be applied to all assets except HYG, AMJ, and GLD will be constrained at 10%.

up_bound <- rep(0.5, length(mu_vec))
up_bound[asset_class %in% c('HYG', 'AMJ', 'GLD')] <- 0.1
store_constr <- run_cla(mu_vec, cov_mat, up_bound = up_bound)
corner_wgt <- do.call(cbind, store_constr$wgt_list)
exp_ret <- apply(corner_wgt, 2, function(m, w) {t(w) %*% m}, m = mu_vec)
exp_vol <- apply(corner_wgt, 2, function(covar, w) {sqrt(t(w) %*% covar %*% w)},
                 covar = cov_mat)
plot(sqrt(diag(cov_mat)), mu_vec, col = 'darkgrey', 
     ylab = 'Estimated Return', xlab = 'Estimated Volatility')
points(exp_vol, exp_ret, col = 'darkgreen', lwd = 2, type = 'b')
title('Efficiency Frontier Example with Constraints')
mtext('Corner Portfolios in Green, Individual Assets in Grey')

rownames(corner_wgt) <- asset_class
colnames(corner_wgt) <- paste('Vol ', round(exp_vol * 100, 1))
round(corner_wgt * 100, 1)
#>     Vol  14.5 Vol  14.5 Vol  8.1 Vol  7.4 Vol  7.4 Vol  7.4 Vol  7.4
#> IVV        50        50      9.2      0.7      0.0      0.0      0.0
#> IWM         0         0      0.0      0.0      0.0      0.0      0.0
#> VTV         0         0      0.0      0.0      0.3      0.0      0.0
#> IWF        50        50     50.0     50.0     50.0     50.0     50.0
#> EFA         0         0      0.0      0.0      0.0      0.0      0.0
#> VWO         0         0      0.0      0.0      0.0      0.0      0.0
#> TLT         0         0     40.8     31.1     30.7     30.4     30.3
#> IEF         0         0      0.0      0.0      0.0      0.0      0.0
#> EMB         0         0      0.0      0.0      0.0      0.0      0.0
#> LQD         0         0      0.0     18.2     19.0     19.6     19.7
#> HYG         0         0      0.0      0.0      0.0      0.0      0.0
#> VNQ         0         0      0.0      0.0      0.0      0.0      0.0
#> AMJ         0         0      0.0      0.0      0.0      0.0      0.0
#> IGF         0         0      0.0      0.0      0.0      0.0      0.0
#> GNR         0         0      0.0      0.0      0.0      0.0      0.0
#> GLD         0         0      0.0      0.0      0.0      0.0      0.0
#>     Vol  6.8 Vol  6.1 Vol  5 Vol  5 Vol  5 Vol  4.8 Vol  4.8 Vol  4.3
#> IVV      0.0      0.0    0.0    0.0    0.0      0.0      0.0      0.0
#> IWM      0.0      0.0    0.0    0.0    0.0      0.0      0.0      0.0
#> VTV      0.0      0.0    2.0    2.0    2.0      4.4      5.0      6.8
#> IWF     44.3     39.0   28.6   28.6   28.6     25.2     24.2     15.6
#> EFA      0.0      0.0    0.0    0.0    0.0      0.0      0.0      0.0
#> VWO      0.0      0.0    0.0    0.0    0.0      0.0      0.0      0.0
#> TLT     23.9     14.8    0.0    0.0    0.0      0.0      0.0      0.0
#> IEF      0.0     14.8   39.5   39.5   39.5     42.6     43.3     50.0
#> EMB      0.0      0.0    0.0    0.0    0.0      0.0      0.6      2.1
#> LQD     31.8     31.4   29.9   29.9   29.9     27.9     26.9     15.8
#> HYG      0.0      0.0    0.0    0.0    0.0      0.0      0.0      9.7
#> VNQ      0.0      0.0    0.0    0.0    0.0      0.0      0.0      0.0
#> AMJ      0.0      0.0    0.0    0.0    0.0      0.0      0.0      0.0
#> IGF      0.0      0.0    0.0    0.0    0.0      0.0      0.0      0.0
#> GNR      0.0      0.0    0.0    0.0    0.0      0.0      0.0      0.0
#> GLD      0.0      0.0    0.0    0.0    0.0      0.0      0.0      0.0
#>     Vol  4.3 Vol  4.3 Vol  4.3 Vol  4.3 Vol  4 Vol  4
#> IVV      0.0      0.0      0.0      0.0    6.7    0.0
#> IWM      0.0      0.0      0.0      0.0    0.0    0.0
#> VTV      6.8      6.8      6.9      7.4    9.6   13.7
#> IWF     15.6     15.6     15.3     14.3    0.0    0.0
#> EFA      0.0      0.0      0.0      0.0    0.0    0.0
#> VWO      0.0      0.0      0.0      0.0    0.0    0.0
#> TLT      0.0      0.0      0.0      0.0    0.0    0.0
#> IEF     50.0     50.0     50.0     50.0   50.0   50.0
#> EMB      2.1      2.1      2.2      2.8    9.3   12.5
#> LQD     15.8     15.8     15.6     15.5   14.4   13.9
#> HYG      9.7      9.7     10.0     10.0   10.0   10.0
#> VNQ      0.0      0.0      0.0      0.0    0.0    0.0
#> AMJ      0.0      0.0      0.0      0.0    0.0    0.0
#> IGF      0.0      0.0      0.0      0.0    0.0    0.0
#> GNR      0.0      0.0      0.0      0.0    0.0    0.0
#> GLD      0.0      0.0      0.0      0.0    0.0    0.0

calc_target_vol solves for the weights for an efficient portfolio with a specific volatility target. If the volatility target is not on the frontier a warning message will print and a value of NA will be returned.

vol_10_wgt <- calc_target_vol(store_constr, cov_mat, 0.1)
rownames(vol_10_wgt) <- asset_class
round(vol_10_wgt * 100, 1)
#>     [,1]
#> IVV 24.9
#> IWM  0.0
#> VTV  0.0
#> IWF 50.0
#> EFA  0.0
#> VWO  0.0
#> TLT 25.1
#> IEF  0.0
#> EMB  0.0
#> LQD  0.0
#> HYG  0.0
#> VNQ  0.0
#> AMJ  0.0
#> IGF  0.0
#> GNR  0.0
#> GLD  0.0
sqrt(t(vol_10_wgt) %*% cov_mat %*% vol_10_wgt)
#>            [,1]
#> [1,] 0.09999382