# Statistical Arbitrage – Trading a cointegrated pair

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
There are many variations of this strategy
Moving average / moving standard deviation (this will be explored later):
• 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
Wait for mean reversion:
• 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?)
All the above strategies look to exit their position when the spread has reverted to the mean. Personally I wouldn’t trade any of the above as they don’t specify an exit strategy for adverse trades. Ie if there is a 6 standard deviation move in the spread is this an amazing trade opportunity? OR more likely did the spread just blow 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:

?View Code RSPLUS

# Parameter Optimisation & Backtesting – Part 1

When developing a strategy that uses a technical indicator, such as a moving average, it is often difficult to decide on what period to use with the indicator. Should it look at the last 10, 20, or n days? The simple solution is just to iterate over many different parameters and look for the parameter combination that optimises a performance metric perhaps the Sharpe Ratio or minimises the Max Drawdown or some combination of both of them.

This method must be approached in a sensible manner, essentially we are fitting the strategy to the data and must be careful not to over fit and capture spurious profits. It is likely that any parameter that is trained will vary over time, the strategy must be able to deal with changes to this parameter.

For example say we are tuning the variable A, and we find that the optimum solution is A=n in the training data (it is very profitable with high sharpe ratio). How well does the strategy perform when we set A=n+1, or A=n-1 do we still get good results? If you don’t it’s an indicator that maybe you have over fitted.

For more discussion on this see http://epchan.blogspot.co.uk/2010/01/method-for-optimizing-parameters.html

Part two will contain a script to perform an optimisation on a simple moving average strategy.

# Trading Strategy – VWAP Mean Reversion

This strategy is going to use the volume weighted average price (VWAP) as an indicator to trade mean version back to VWAP. Annualized Sharpe Ratio (Rf=0%) is 0.9016936.

This post is a response to http://gekkoquant.com/2012/07/29/trading-strategy-sp-vwap-trend-follow/ where there was a bug in the code indicating that VWAP wasn’t reverting (this didn’t sit well with me, or some of the people who commented). As always don’t take my word for anything, backtest the strategy yourself. One of the dangers of using R or Matlab is that it’s easy for forward bias to slip into your code. There are libraries such as Quantstrat for R which protect against this, but I’ve found them terribly slow to run.

• All conditions are checked at the close, and the trade held for one day from the close
• If price/vwap > uLim go short
• If price/vwap < lLim go long

Onto the code:

?View Code RSPLUS
 library("quantmod") library("PerformanceAnalytics")   #Trade logic - Look for mean reversion #If price/vwap > uLim go SHORT #If price/vwap < lLim go LONG   #Script parameters symbol <- "^GSPC" #Symbol nlookback <- 3 #Number of days to lookback and calculate vwap uLim <- 1.001 #If price/vwap > uLim enter a short trade lLim <- 0.999 #If price/vwap < lLim enter a long trade     #Specify dates for downloading data startDate = as.Date("2006-01-01") #Specify what date to get the prices from symbolData <- new.env() #Make a new environment for quantmod to store data in getSymbols(symbol, env = symbolData, src = "yahoo", from = startDate) mktdata <- eval(parse(text=paste("symbolData$",sub("^","",symbol,fixed=TRUE)))) mktdata <- head(mktdata,-1) #Hack to fix some stupid duplicate date problem with yahoo #Calculate volume weighted average price vwap <- VWAP(Cl(mktdata), Vo(mktdata), n=nlookback) #Can calculate vwap like this, but it is slower #vwap <- runSum(Cl(mktdata)*Vo(mktdata),nlookback)/runSum(Vo(mktdata),nlookback) #Calulate the daily returns dailyRet <- Delt(Cl(mktdata),k=1,type="arithmetic") #Daily Returns #signal = price/vwap signal <- Cl(mktdata) / vwap signal[is.na(signal)] <- 1 #Setting to one means that no trade will occur for NA's #Stripping NA's caused all manner of problems in a previous post trade <- apply(signal,1, function(x) {if(xuLim) { return(-1) } else { return (0) }}}) #Calculate the P&L #The daily ret is DailyRet(T)=(Close(T)-Close(T-1))/Close(T-1) #We enter the trade on day T so need the DailyRet(T+1) as our potential profit #Hence the lag in the line below strategyReturns <- trade * lag(dailyRet,-1) strategyReturns <- na.omit(strategyReturns) #### Performance Analysis ### #Calculate returns for the index indexRet <- dailyRet #Daily returns colnames(indexRet) <- "IndexRet" zooTradeVec <- cbind(as.zoo(strategyReturns),as.zoo(indexRet)) #Convert to zoo object colnames(zooTradeVec) <- c(paste(symbol," VWAP Trade"),symbol) zooTradeVec <- na.omit(zooTradeVec) #Lets see how all the strategies faired against the index dev.new() charts.PerformanceSummary(zooTradeVec,main=paste("Performance of ", symbol, " VWAP Strategy"),geometric=FALSE) #Lets calculate a table of montly returns by year and strategy cat("Calander Returns - Note 13.5 means a return of 13.5%\n") print(table.CalendarReturns(zooTradeVec)) #Calculate the sharpe ratio cat("Sharpe Ratio") print(SharpeRatio.annualized(zooTradeVec)) # Trading Strategy – S&P VWAP Trend Follow (BUGGY) UPDATE: The exceptional returns seen in this strategy were due to a 2 day look forward bias in the signal (and then subsequent trade direction), ie when returns were calculated for day T the trade signal used was actually from day T+2. This bias occurred in the lines: ?View Code RSCODE  signal <- na.omit(signal) Both the signal and trade dataframe had the correct dates for each signal/trades however when indexRet*trade happened then trade was treated as undated vectors (which is 2 elements shorter than index ret) hence the 2 day shift. The moral of this story is to merge dataframes before multiplying! Thank you for everyone that commented on this, a corrected post is to follow! Original Post This strategy is going to use the volume weighted average price (VWAP) as an indicator to determine the direction of the current trend and trade the same direction as the trend. Annualized Sharpe Ratio (Rf=0%) is 8.510472. Trade logic: • All conditions are checked at the close, and the trade held for one day from the close • If price/vwap > uLim go long • If price/vwap < lLim go short Initially I thought that the price would be mean reverting to VWAP (this can be see in high freq data) however this didn’t appear to be the case with EOD data. For such a simple strategy I’m amazed that the Sharpe ratio is so high (suspiciously high). The code has been double&tripple checked to see if any forward bias has slipped in, however I haven’t spotted anything. Onto the code: ?View Code RSPLUS  library("quantmod") library("PerformanceAnalytics") #Trade logic - Follow the trade demand, ie if price > vwap then go long #If price/vwap > uLim go LONG #If price/vwap < lLim go SHORT #Script parameters symbol <- "^GSPC" #Symbol nlookback <- 3 #Number of days to lookback and calculate vwap uLim <- 1.001 #If price/vwap > uLim enter a long trade lLim <- 0.999 #If price/vwap < lLim enter a short trade #Specify dates for downloading data startDate = as.Date("2006-01-01") #Specify what date to get the prices from symbolData <- new.env() #Make a new environment for quantmod to store data in getSymbols(symbol, env = symbolData, src = "yahoo", from = startDate) mktdata <- eval(parse(text=paste("symbolData$",sub("^","",symbol,fixed=TRUE)))) mktdata <- head(mktdata,-1) #Hack to fix some stupid duplicate date problem with yahoo   #Calculate volume weighted average price vwap <- VWAP(Cl(mktdata), Vo(mktdata), n=nlookback) #Can calculate vwap like this, but it is slower #vwap <- runSum(Cl(mktdata)*Vo(mktdata),nlookback)/runSum(Vo(mktdata),nlookback)   #Calulate the daily returns dailyRet <- Delt(Cl(mktdata),k=1,type="arithmetic") #Daily Returns   #signal = price/vwap signal <- Cl(mktdata) / vwap signal <- na.omit(signal) trade <- apply(signal,1, function(x) {if(xuLim) { return(1) } else { return (0) }}})   #Calculate the P&L #The daily ret is DailyRet(T)=(Close(T)-Close(T-1))/Close(T-1) #We enter the trade on day T so need the DailyRet(T+1) as our potential profit #Hence the lag in the line below strategyReturns <- trade * lag(dailyRet,-1) strategyReturns <- na.omit(strategyReturns)   #### Performance Analysis ### #Calculate returns for the index indexRet <- dailyRet #Daily returns colnames(indexRet) <- "IndexRet" zooTradeVec <- cbind(as.zoo(strategyReturns),as.zoo(indexRet)) #Convert to zoo object colnames(zooTradeVec) <- c(paste(symbol," VWAP Trade"),symbol) zooTradeVec <- na.omit(zooTradeVec)   #Lets see how all the strategies faired against the index dev.new() charts.PerformanceSummary(zooTradeVec,main=paste("Performance of ", symbol, " VWAP Strategy"),geometric=FALSE)     #Lets calculate a table of montly returns by year and strategy cat("Calander Returns - Note 13.5 means a return of 13.5%\n") print(table.CalendarReturns(zooTradeVec)) #Calculate the sharpe ratio cat("Sharpe Ratio") print(SharpeRatio.annualized(zooTradeVec))

This strategy is going to look at a vega neutral volatility carry trading strategy. Two different futures contract will be traded, the VXX and VXZ. These contracts are rolling futures on the S&P 500 Vix index, the VXX is a short term future and the VXZ is a medium term future. Annualized Sharpe Ratio (Rf=0%) is 1.759449.

The strategy is very simple, the rules are:

• If VXX / VXZ > 1 then in backwardation so do a reverse carry trade (buy VXX, sell VXZ)
• If VXX / VXZ < 1 then do a carry trade (sell VXX, buy VXZ)

If the volatility spot price doesn’t change, then we’re extracting the cost of carry. Due to buying and selling (or vice versa) the short to mid term futures the vega exposure is hedged.

In the script the above two rules have been slightly changed, a slight offset is added/subtracted from the ratio. Essentially we want to be deep into contango zone or deep into backwardation zone before we trade, if we’re close to the flip point then don’t trade.

Section 1: Downloaded the data, and calculate the Open to Close return. This strategy will look for entry at the open and exit at the close.

Section 2: Regress the daily returns of VXX with VXZ to calculate the hedge ratio

Section 3: Generate the backwardation / contango signal