In my last post http://gekkoquant.com/2012/12/17/statistical-arbitrage-testing-for-cointegration-augmented-dicky-fuller/ I demonstrated cointegration, a mathematical test to identify stationary pairs where the spread by definition must be mean reverting.
In this post I intend to show how to trade a cointegrated pair and will continue analysing Royal Dutch Shell A vs B shares (we know they’re cointegrated from my last post). Trading a cointegrated pair is straight forward, we know the mean and variance of the spread, we know that those values are constant. The entry point for a stat arb is to simply look for a large deviation away from the mean.
A basic strategy is:
- If spread(t) >= Mean Spread + 2*Standard Deviation then go Short
- If spread(t) <= Mean Spread – 2*Standard Deviation then go Long
- If spread(t) >= nDay Moving Average + 2*nDay Rolling Standard deviation then go Short
- If spread(t) <= nDay Moving Average – 2*nDay Rolling Standard deviation then go long
- If spread(t) <= Mean Spread + 2*Std AND spread(t-1)> Mean Spread + 2*Std
- If spread(t) >= Mean Spread – 2*Std AND spread(t-1)< Mean Spread – 2*Std
- Advantage is that we only trade when we see the mean reversion, where as the other models are hoping for mean reversion on a large deviation from the mean (is the spread blowing up?)
This post will look at the moving average and rolling standard deviation model for Royal Dutch Shell A vs B shares, it will use the hedge ratio found in the last post.
Sharpe Ratio Shell A & B Stat Arb Shell A
Annualized Sharpe Ratio (Rf=0%):
Shell A&B Stat Arb 0.8224211
Shell A 0.166307
The stat arb has a Superior Sharpe ratio over simply investing in Shell A. At a first glance the sharpe ratio of 0.8 looks disappointing, however since the strategy spends most of it’s time out of the market it will have a low annualized sharpe ratio. To increase the sharpe ratio one can look at trading higher frequencies or have a portfolio pairs so that more time is spent in the market.
Onto the code:
library("quantmod") library("PerformanceAnalytics") backtestStartDate = as.Date("2010-01-02") #Starting date for the backtest symbolLst<-c("RDS-A","RDS-B") title<-c("Royal Dutch Shell A vs B Shares") ### SECTION 1 - Download Data & Calculate Returns ### #Download the data symbolData <- new.env() #Make a new environment for quantmod to store data in getSymbols(symbolLst, env = symbolData, src = "yahoo", from = backtestStartDate) #We know this pair is cointegrated from the tutorial #http://gekkoquant.com/2012/12/17/statistical-arbitrage-testing-for-cointegration-augmented-dicky-fuller/ #The tutorial found the hedge ratio to be 0.9653 stockPair <- list( a = coredata(Cl(eval(parse(text=paste("symbolData$\"",symbolLst[1],"\"",sep=""))))) #Stock A ,b = coredata(Cl(eval(parse(text=paste("symbolData$\"",symbolLst[2],"\"",sep=""))))) #Stock B ,hedgeRatio = 0.9653 ,name=title) simulateTrading <- function(stockPair){ #Generate the spread spread <- stockPair$a - stockPair$hedgeRatio*stockPair$b #Strategy is if the spread is greater than +/- nStd standard deviations of it's rolling 'lookback' day standard deviation #Then go long or short accordingly lookback <- 90 #look back 90 days nStd <- 1.5 #Number of standard deviations from the mean to trigger a trade movingAvg = rollmean(spread,lookback, na.pad=TRUE) #Moving average movingStd = rollapply(spread,lookback,sd,align="right", na.pad=TRUE) #Moving standard deviation / bollinger bands upperThreshold = movingAvg + nStd*movingStd lowerThreshold = movingAvg - nStd*movingStd aboveUpperBand <- spread>upperThreshold belowLowerBand <- spread<lowerThreshold aboveMAvg <- spread>movingAvg belowMAvg <- spread<movingAvg aboveUpperBand[is.na(aboveUpperBand)]<-0 belowLowerBand[is.na(belowLowerBand)]<-0 aboveMAvg[is.na(aboveMAvg)]<-0 belowMAvg[is.na(belowMAvg)]<-0 #The cappedCumSum function is where the magic happens #Its a nice trick to avoid writing a while loop #Hence since using vectorisation is faster than the while loop #The function basically does a cumulative sum, but caps the sum to a min and max value #It's used so that if we get many 'short sell triggers' it will only execute a maximum of 1 position #Short position - Go short if spread is above upper threshold and go long if below the moving avg #Note: shortPositionFunc only lets us GO short or close the position cappedCumSum <- function(x, y,max_value,min_value) max(min(x + y, max_value), min_value) shortPositionFunc <- function(x,y) { cappedCumSum(x,y,0,-1) } longPositionFunc <- function(x,y) { cappedCumSum(x,y,1,0) } shortPositions <- Reduce(shortPositionFunc,-1*aboveUpperBand+belowMAvg,accumulate=TRUE) longPositions <- Reduce(longPositionFunc,-1*aboveMAvg+belowLowerBand,accumulate=TRUE) positions = longPositions + shortPositions dev.new() par(mfrow=c(2,1)) plot(movingAvg,col="red",ylab="Spread",type='l',lty=2) title("Shell A vs B spread with bollinger bands") lines(upperThreshold, col="red") lines(lowerThreshold, col="red") lines(spread, col="blue") legend("topright", legend=c("Spread","Moving Average","Upper Band","Lower Band"), inset = .02, lty=c(1,2,1,1),col=c("blue","red","red","red")) # gives the legend lines the correct color and width plot((positions),type='l') #Calculate spread daily ret stockPair$a - stockPair$hedgeRatio*stockPair$b aRet <- Delt(stockPair$a,k=1,type="arithmetic") bRet <- Delt(stockPair$b,k=1,type="arithmetic") dailyRet <- aRet - stockPair$hedgeRatio*bRet dailyRet[is.na(dailyRet)] <- 0 tradingRet <- dailyRet * positions simulateTrading <- tradingRet } tradingRet <- simulateTrading(stockPair) #### Performance Analysis ### #Calculate returns for the index indexRet <- Delt(Cl(eval(parse(text=paste("symbolData$\"",symbolLst[1],"\"",sep="")))),k=1,type="arithmetic") #Daily returns indexRet <- as.zoo(indexRet) tradingRetZoo <- indexRet tradingRetZoo[,1] <- tradingRet zooTradeVec <- as.zoo(cbind(tradingRetZoo,indexRet)) #Convert to zoo object colnames(zooTradeVec) <- c("Shell A & B Stat Arb","Shell A") zooTradeVec <- na.omit(zooTradeVec) #Lets see how all the strategies faired against the index dev.new() charts.PerformanceSummary(zooTradeVec,main="Performance of Shell Statarb Strategy",geometric=FALSE) cat("Sharpe Ratio") print(SharpeRatio.annualized(zooTradeVec)) |