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.
Pingback: Quantocracy's Daily Wrap for 04/20/2015 | Quantocracy
I enjoyed the article and was wondering if you’ve seen newfound research’s sector fund work that adjusts the momentum lookback based on volatility.
No. Care to provide a link?
here is f squared investments offering based on newfound research’s model-
http://f-squaredinvestments.com/alphasector/alphasector-premium/
That link provides no way of replicating/assessing the strategy. I suppose it’s fair that they hold their cards close to their vest, but I can’t say one way or another what their strategy does.
Ilya, here’s Newfound’s presentation
Click to access Newfound-RMUS-Presentation.pdf
Reading the fine print of the presentation:
Newfound began to actively calculate the performance of the Newfound Risk Managed U.S. Sectors index on February 26, 2015.Performance results prior to February 26, 2015 are all backtested and hypothetical. Newfound began to manage and trade a brokerage account on February 26, 2015…
Since there is no such thing as a bad backtest (at least not one you will publicly show), it is critical to know exactly what they are doing. It is possible to produce hundreds of good looking back tests that will never perform in the real world. You just keep tweaking the parameters until you get something that worked in the past. For Newfound, there is essentially zero real world history. It’s all just hypothetical.
Ilya, really enjoy your work. Could you add the annual avg. turnover to your stats?
Would help me a lot to have a better grip on test to real performance.
Best Regards
Carsten
Carsten,
When calling Return.portfolio, set verbose = TRUE, and then what you’d do is subtract the monthly endpoint beginning of period weights from that date lag 1 of end of period weights.
Great post – how can you print the weights held each month?
In the final line, instead of return(stratRets), change that to
return(list(weights, stratRets))
And then when you call the function, the first element will be the monthly weights.
Hi Iliya – when you make this amendment to print the weights you get the following error:
Error in error(x, …) :
improper length of one or more arguments to merge.xts
Any idea?
Ilya, really appreciate you sharing your works. CASH column at weights table is always “0” from the beginning to end. Even through two big market clashes in 2002/3 and 2008/9, the cash allocation column is zero. So the thinking is what the purpose for the CASH allocation if it is always Zero. Thank.
It is ok for CASH at weights column, i debug in the middle of sctoStrat function. So i don’t see the whole picture before i make comments.