Multiple Factor Model – Building 130/30 Index
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
Nico brought to my attention the 130/30: The New Long-Only (2008) by A. Lo, P. Patel paper in his comment to the Multiple Factor Model – Building CSFB Factors post. This paper presents a very detailed step by step guide to building 130/30 Index using average CSFB Factors as the alpha model and MSCI Barra Multi-Factor Risk model. Today, I want to adapt this methodology and to show how to build 130/30 Index based on the CSFB Factors we created in the Multiple Factor Model – Building CSFB Factors post and the Risk Model we created in the Multiple Factor Model – Building Risk Model post.
Let’s start by loading the CSFB factors that we saved at the end of the Multiple Factor Model – Building CSFB Factors post. [If you are missing data.factors.Rdata file, please execute fm.all.factor.test() function first to create and save CSFB factors.] Next, let’s load the multiple factor risk model we saved at the end of the Multiple Factor Model – Building Risk Model post. [If you are missing risk.model.Rdata file, please execute fm.risk.model.test() function first to create and save multiple factor risk model.] Next, I will compute betas over a two-year rolling window.
############################################################################### # Load Systematic Investor Toolbox (SIT) # http://systematicinvestor.wordpress.com/systematic-investor-toolbox/ ############################################################################### con = gzcon(url('http://www.systematicportfolio.com/sit.gz', 'rb')) source(con) close(con) #***************************************************************** # Load data #****************************************************************** load.packages('quantmod') # Load CSFB factor data that we saved at the end of the fm.all.factor.test function load(file='data.factors.Rdata') nperiods = nrow(next.month.ret) tickers = colnames(next.month.ret) n = len(tickers) # Load multiple factor risk model data that we saved at the end of the fm.risk.model.test function load(file='risk.model.Rdata') factor.exposures = all.data[,,-1] factor.names = dimnames(factor.exposures)[[3]] nfactors = len(factor.names) #***************************************************************** # Compute Betas: b = cov(r,m) / var(m) # The betas are measured on a two-year rolling window # http://en.wikipedia.org/wiki/Beta_(finance) #****************************************************************** ret = mlag(next.month.ret) beta = ret * NA # 1/n benchmark portfolio benchmark = ntop(ret, n) benchmark.ret = rowSums(benchmark * ret, na.rm=T) # estimate betas for(t in 24:nperiods) { t.index = (t-23):t benchmark.var = var( benchmark.ret[t.index], na.rm=T ) t.count = count(ret[t.index, ]) t.cov = cov( ifna(ret[t.index,], 0), benchmark.ret[t.index], use='complete.obs' ) # require at least 20 months of history beta[t,] = iif(t.count > 20, t.cov/benchmark.var, NA) }
To construct efficient portfolio each month, I will solve mean-variance portfolio optimization problem of the following form:
Max weight * return - risk.aversion * weight * Covariance * weight Sum weight = 1 Sum weight * beta = 1 0 <= weight <= 0.1
Please read The Effects of Risk Aversion on Optimization (2010) by S. Liu, R. Xu paper for the detailed discussion of this optimization problem.
Please note that I will use risk.aversion = 0.0075 and portfolio beta = 1 as discussed on the page 22 of the 130/30: The New Long-Only (2008) by A. Lo, P. Patel paper. To model portfolio beta constraint, I will use the fact that portfolio beta is equal to the weighted average of the individual asset betas.
#***************************************************************** # Construct LONG ONLY portfolio using the multiple factor risk model #****************************************************************** load.packages('quadprog,corpcor,kernlab') weight = NA * next.month.ret weights = list() weights$benchmark = ntop(beta, n) weights$long.alpha = weight for(t in 36:nperiods) { #-------------------------------------------------------------------------- # Create constraints #-------------------------------------------------------------------------- # set min/max wgts for individual stocks: 0 =< x <= 10/100 constraints = new.constraints(n, lb = 0, ub = 10/100) # wgts must sum to 1 (fully invested) constraints = add.constraints(rep(1,n), type = '=', b = 1, constraints) #-------------------------------------------------------------------------- # beta of portfolio is the weighted average of the individual asset betas # http://www.duke.edu/~charvey/Classes/ba350/riskman/riskman.htm #-------------------------------------------------------------------------- constraints = add.constraints(ifna(as.vector(beta[t,]),0), type = '=', b = 1, constraints) #-------------------------------------------------------------------------- # Create factor exposures constraints #-------------------------------------------------------------------------- # adjust prior constraints, add factor exposures constraints = add.variables(nfactors, constraints) # BX - X1 = 0 constraints = add.constraints(rbind(ifna(factor.exposures[t,,], 0), -diag(nfactors)), rep(0, nfactors), type = '=', constraints) #-------------------------------------------------------------------------- # Create Covariance matrix # [Qu 0] # [ 0 Qf] #-------------------------------------------------------------------------- temp = diag(n) diag(temp) = ifna(specific.variance[t,], mean(coredata(specific.variance[t,]), na.rm=T))^2 cov.temp = diag(n + nfactors) cov.temp[1:n,1:n] = temp cov.temp[(n+1):(n+nfactors),(n+1):(n+nfactors)] = factor.covariance[t,,] #-------------------------------------------------------------------------- # create input assumptions #-------------------------------------------------------------------------- ia = list() ia$n = nrow(cov.temp) ia$annual.factor = 12 ia$symbols = c(tickers, factor.names) ia$cov = cov.temp #-------------------------------------------------------------------------- # page 9, Risk: We use the Barra default setting, risk aversion value of 0.0075, and # AS-CF risk aversion ratio of 1. # # The Effects of Risk Aversion on Optimization (2010) by S. Liu, R. Xu # page 4/5 #-------------------------------------------------------------------------- risk.aversion = 0.0075 ia$cov.temp = ia$cov # set expected return alpha = factors.avg$AVG[t,] / 5 ia$expected.return = c(ifna(coredata(alpha),0), rep(0, nfactors)) # remove companies that have no beta from optimization index = which(is.na(beta[t,])) if( len(index) > 0) { constraints$ub[index] = 0 constraints$lb[index] = 0 } # find solution sol = solve.QP.bounds(Dmat = 2* risk.aversion * ia$cov.temp, dvec = ia$expected.return, Amat = constraints$A, bvec = constraints$b, meq = constraints$meq, lb = constraints$lb, ub = constraints$ub) weights$long.alpha[t,] = sol$solution[1:n] }
Next, let’s construct 130/30 portfolio. I will restrict portfolio weights to be +/- 10% and will use portfolio construction technique that I documented in the 130/30 Portfolio Construction post.
#***************************************************************** # Construct Long/Short 130:30 portfolio using the multiple factor risk model # based on the examples in the aa.long.short.test functions #****************************************************************** weights$long.short.alpha = weight for(t in 36:nperiods) { #-------------------------------------------------------------------------- # Create constraints #-------------------------------------------------------------------------- # set min/max wgts for individual stocks: -10/100 =< x <= 10/100 constraints = new.constraints(n, lb = -10/100, ub = 10/100) # wgts must sum to 1 (fully invested) constraints = add.constraints(rep(1,n), type = '=', b = 1, constraints) #-------------------------------------------------------------------------- # beta of portfolio is the weighted average of the individual asset betas # http://www.duke.edu/~charvey/Classes/ba350/riskman/riskman.htm #-------------------------------------------------------------------------- constraints = add.constraints(ifna(as.vector(beta[t,]),0), type = '=', b = 1, constraints) #-------------------------------------------------------------------------- # Create factor exposures constraints #-------------------------------------------------------------------------- # adjust prior constraints, add factor exposures constraints = add.variables(nfactors, constraints) # BX - X1 = 0 constraints = add.constraints(rbind(ifna(factor.exposures[t,,], 0), -diag(nfactors)), rep(0, nfactors), type = '=', constraints) #-------------------------------------------------------------------------- # Create 130:30 # -v.i <= x.i <= v.i, v.i>0, SUM(v.i) = 1.6 #-------------------------------------------------------------------------- # adjust prior constraints, add v.i constraints = add.variables(n, constraints) # -v.i <= x.i <= v.i # x.i + v.i >= 0 constraints = add.constraints(rbind(diag(n), matrix(0,nfactors,n) ,diag(n)), rep(0, n), type = '>=', constraints) # x.i - v.i <= 0 constraints = add.constraints(rbind(diag(n), matrix(0,nfactors,n), -diag(n)), rep(0, n), type = '<=', constraints) # SUM(v.i) = 1.6 constraints = add.constraints(c(rep(0, n), rep(0, nfactors), rep(1, n)), 1.6, type = '=', constraints) #-------------------------------------------------------------------------- # Create Covariance matrix # [Qu 0] # [ 0 Qf] #-------------------------------------------------------------------------- temp = diag(n) diag(temp) = ifna(specific.variance[t,], mean(coredata(specific.variance[t,]), na.rm=T))^2 cov.temp = 0*diag(n + nfactors + n) cov.temp[1:n,1:n] = temp cov.temp[(n+1):(n+nfactors),(n+1):(n+nfactors)] = factor.covariance[t,,] #-------------------------------------------------------------------------- # create input assumptions #-------------------------------------------------------------------------- ia = list() ia$n = nrow(cov.temp) ia$annual.factor = 12 ia$symbols = c(tickers, factor.names, tickers) ia$cov = cov.temp #-------------------------------------------------------------------------- # page 9, Risk: We use the Barra default setting, risk aversion value of 0.0075, and # AS-CF risk aversion ratio of 1. # # The Effects of Risk Aversion on Optimization (2010) by S. Liu, R. Xu # page 4/5 #-------------------------------------------------------------------------- risk.aversion = 0.0075 ia$cov.temp = ia$cov # set expected return alpha = factors.avg$AVG[t,] / 5 ia$expected.return = c(ifna(coredata(alpha),0), rep(0, nfactors), rep(0, n)) # remove companies that have no beta from optimization index = which(is.na(beta[t,])) if( len(index) > 0) { constraints$ub[index] = 0 constraints$lb[index] = 0 } # find solution sol = solve.QP.bounds(Dmat = 2* risk.aversion * ia$cov.temp, dvec = ia$expected.return, Amat = constraints$A, bvec = constraints$b, meq = constraints$meq, lb = constraints$lb, ub = constraints$ub) weights$long.short.alpha[t,] = sol$solution[1:n] }
At this point, we created monthly long-only and 130:30 portfolios. Let’s examine their transition maps.
#***************************************************************** # Plot Transition Maps #****************************************************************** layout(1:3) for(i in names(weights)) plotbt.transition.map(weights[[i]], i)
The portfolio weights for the long-only portfolio (long.alpha) sum up to 100% and for the 130:30 portfolio (long.short.alpha) is 130% long and 30% short as expected.
Next let’s create trading strategies based on these portfolios and check their performance.
#***************************************************************** # Create strategies #****************************************************************** prices = data$prices prices = bt.apply.matrix(prices, function(x) ifna.prev(x)) # find month ends month.ends = endpoints(prices, 'months') # create strategies that invest in each qutile models = list() for(i in names(weights)) { data$weight[] = NA data$weight[month.ends,] = weights[[i]] capital = 100000 data$weight[] = (capital / prices) * (data$weight) models[[i]] = bt.run(data, type='share', capital=capital) } #***************************************************************** # Create Report #****************************************************************** models = rev(models) plotbt.custom.report.part1(models, dates='1998::') plotbt.custom.report.part2(models)
The 130:30 portfolio works best, producing high returns with smaller drawdowns than long-only and benchmark (1/N) portfolios.
The note of caution: the above results are based on $0 commission rate (i.e. trading is free). Also, I’m using the current Dow Jones index components through out the whole history; hence, introducing survivorship bias (i.e. Dow Jones index changed its composition a few times in the last 20 years).
To see how much of the problem is my assumption of $0 commission rate, let’s have a look at the annual portfolio turnovers.
# Plot Portfolio Turnover for each strategy layout(1) barplot.with.labels(sapply(models, compute.turnover, data), 'Average Annual Portfolio Turnover')
The 130:30 portfolio has a pretty high portfolio turnover; therefore, it will not perform as well in the real life as in our backtest.
To view the complete source code for this example, please have a look at the fm.long.short.test() function in factor.model.test.r at github.
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.