The JP Morgan SCTO strategy
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
This strategy goes over JP Morgan’s SCTO strategy, a basic XL-sector/RWR rotation strategy with the typical associated risks and returns with a momentum equity strategy. It’s nothing spectacular, but if a large bank markets it, it’s worth looking at.
Recently, one of my readers, a managing director at a quantitative investment firm, sent me a request to write a rotation strategy based around the 9 sector spiders and RWR. The way it works (or at least, the way I interpreted it) is this:
Every month, compute the return (not sure how “the return” is defined) and rank. Take the top 5 ranks, and weight them in a normalized fashion to the inverse of their 22-day volatility. Zero out any that have negative returns. Lastly, check the predicted annualized vol of the portfolio, and if it’s greater than 20%, bring it back down to 20%. The cash asset–SHY–receives any remaining allocation due to setting securities to zero.
For the reference I used, here’s the investment case document from JP Morgan itself.
Here’s my implementation:
Step 1) get the data, compute returns.
require(quantmod) require(PerformanceAnalytics) symbols <- c("XLB", "XLE", "XLF", "XLI", "XLK", "XLP", "XLU", "XLV", "XLY", "RWR", "SHY") getSymbols(symbols, from="1990-01-01") prices <- list() for(i in 1:length(symbols)) { prices[[i]] <- Ad(get(symbols[i])) } prices <- do.call(cbind, prices) colnames(prices) <- gsub("\.[A-z]*", "", colnames(prices)) returns <- na.omit(Return.calculate(prices))
Step 2) The function itself.
sctoStrat <- function(returns, cashAsset = "SHY", lookback = 4, annVolLimit = .2, topN = 5, scale = 252) { ep <- endpoints(returns, on = "months") weights <- list() cashCol <- grep(cashAsset, colnames(returns)) #remove cash from asset returns cashRets <- returns[, cashCol] assetRets <- returns[, -cashCol] for(i in 2:(length(ep) - lookback)) { retSubset <- assetRets[ep[i]:ep[i+lookback]] #forecast is the cumulative return of the lookback period forecast <- Return.cumulative(retSubset) #annualized (realized) volatility uses a 22-day lookback period annVol <- StdDev.annualized(tail(retSubset, 22)) #rank the forecasts (the cumulative returns of the lookback) rankForecast <- rank(forecast) - ncol(assetRets) + topN #weight is inversely proportional to annualized vol weight <- 1/annVol #zero out anything not in the top N assets weight[rankForecast <= 0] <- 0 #normalize and zero out anything with a negative return weight <- weight/sum(weight) weight[forecast < 0] <- 0 #compute forecasted vol of portfolio forecastVol <- sqrt(as.numeric(t(weight)) %*% cov(retSubset) %*% as.numeric(weight)) * sqrt(scale) #if forecasted vol greater than vol limit, cut it down if(as.numeric(forecastVol) > annVolLimit) { weight <- weight * annVolLimit/as.numeric(forecastVol) } weights[[i]] <- xts(weight, order.by=index(tail(retSubset, 1))) } #replace cash back into returns returns <- cbind(assetRets, cashRets) weights <- do.call(rbind, weights) #cash weights are anything not in securities weights$CASH <- 1-rowSums(weights) #compute and return strategy returns stratRets <- Return.portfolio(R = returns, weights = weights) return(stratRets) }
In this case, I took a little bit of liberty with some specifics that the reference was short on. I used the full covariance matrix for forecasting the portfolio variance (not sure if JPM would ignore the covariances and do a weighted sum of individual volatilities instead), and for returns, I used the four-month cumulative. I’ve seen all sorts of permutations on how to compute returns, ranging from some average of 1, 3, 6, and 12 month cumulative returns to some lookback period to some two period average, so I’m all ears if others have differing ideas, which is why I left it as a lookback parameter.
Step 3) Running the strategy.
scto4_20 <- sctoStrat(returns) getSymbols("SPY", from = "1990-01-01") spyRets <- Return.calculate(Ad(SPY)) comparison <- na.omit(cbind(scto4_20, spyRets)) colnames(comparison) <- c("strategy", "SPY") charts.PerformanceSummary(comparison) apply.yearly(comparison, Return.cumulative) stats <- rbind(table.AnnualizedReturns(comparison), maxDrawdown(comparison), CalmarRatio(comparison), SortinoRatio(comparison)*sqrt(252)) round(stats, 3)
Here are the statistics:
strategy SPY Annualized Return 0.118 0.089 Annualized Std Dev 0.125 0.193 Annualized Sharpe (Rf=0%) 0.942 0.460 Worst Drawdown 0.165 0.552 Calmar Ratio 0.714 0.161 Sortino Ratio (MAR = 0%) 1.347 0.763 strategy SPY 2002-12-31 -0.035499564 -0.05656974 2003-12-31 0.253224759 0.28181559 2004-12-31 0.129739794 0.10697941 2005-12-30 0.066215224 0.04828267 2006-12-29 0.167686936 0.15845242 2007-12-31 0.153890329 0.05146218 2008-12-31 -0.096736711 -0.36794994 2009-12-31 0.181759432 0.26351755 2010-12-31 0.099187188 0.15056146 2011-12-30 0.073734427 0.01894986 2012-12-31 0.067679129 0.15990336 2013-12-31 0.321039353 0.32307769 2014-12-31 0.126633020 0.13463790 2015-04-16 0.004972434 0.02806776
And the equity curve:
To me, it looks like a standard rotation strategy. Aims for the highest momentum securities, diversifies to try and control risk, hits a drawdown in the crisis, recovers, and slightly lags the bull run on SPY. Nothing out of the ordinary.
So, for those interested, here you go. I’m surprised that JP Morgan itself markets this sort of thing, considering that they probably employ top-notch quants that can easily come up with products and/or strategies that are far better.
Thanks for reading.
NOTE: I am a freelance consultant in quantitative analysis on topics related to this blog. If you have contract or full time roles available for proprietary research that could benefit from my skills, please contact me through my LinkedIn here.
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.