Linear Regression Curves vs Bollinger Bands

In my last post I showed what a linear regression curve was, this post will use it as part of a mean reverting trading strategy.

The strategy is simple:

  • Calculate a rolling ‘average’ and a rolling ‘deviation’
  • If the Close price is greater than the average+n*deviation go short (and close when you cross the mean)
  • If the Close price is less than the average-n*deviation go long (and close when you cross the mean)

Two cases will be analysed, one strategy will use a simple moving average(SMA), the other will use the linear regression curve(LRC) for the average. The deviation function will be Standard Devation, Average True Range, and LRCDeviation (same as standard deviation but replace the mean with the LRC).

Results (Lookback = 20 and Deviation Multiplier = 2:

mean reversion linear regression curves

Annualized Sharpe Ratio (Rf=0%)

  • GSPC = 0.05257118
  • Simple Moving Avg – Standard Deviation = 0.2535342
  • Simple Moving Avg – Average True Range = 0.1165512
  • Simple Moving Avg – LRC Deviation 0.296234
  • Linear Regression Curve – Standard Deviation = 0.2818447
  • Linear Regression Curve – Average True Range = 0.5824727
  • Linear Regression Curve – LRC Deviation = 0.04672071

Optimisation analysis:

Annoyingly the colour scale is different between the two charts, however the sharpe ratio is written in each cell. Lighter colours indicate better performance.

Over a 13year period and trading the GSPC the LRC achieved a sharpe of ~0.6 where as the SMA achieved a sharpe of ~0.3. The LRC appears superior to the SMA.

Mean Reversion LRC STDEV Mean Reversion SMA STDEVI will update this post at a later point in time when my optimisation has finished running for the other strategies.

?View Code RSPLUS
marketSymbol <- "^GSPC"
nLookback <- 20 #The lookback to calcute the moving average / linear regression curve / average true range / standard deviation
nDeviation <- 2
#Specify dates for downloading data, training models and running simulation
startDate = as.Date("2000-01-01") #Specify what date to get the prices from
symbolData <- new.env() #Make a new environment for quantmod to store data in
stockCleanNameFunc <- function(name){
getSymbols(marketSymbol, env = symbolData, src = "yahoo", from = startDate)
cleanName <- stockCleanNameFunc(marketSymbol)
mktData <- get(cleanName,symbolData)
linearRegressionCurve <- function(data,n){
    regression <- function(dataBlock){
           fit <-lm(dataBlock~seq(1,length(dataBlock),1))
    return (rollapply(data,width=n,regression,align="right",by.column=FALSE,na.pad=TRUE))
linearRegressionCurveStandardDeviation <- function(data,n){
    deviation <- function(dataBlock){
        fit <-lm(dataBlock~seq(1,length(dataBlock),1))
        quasiMean <- (last(fit$fitted.values))
        quasiMean <- rep(quasiMean,length(dataBlock))
        stDev <- sqrt((1/length(dataBlock))* sum((dataBlock - quasiMean)^2))
        return (stDev)
    return (rollapply(data,width=n,deviation,align="right",by.column=FALSE,na.pad=TRUE))
reduceLongTradeEntriesToTradOpenOrClosedSignal <- function(trades){
    #Takes something like
    #000011110000-1-1000011 (1 = go long, -1 = go short)
    #and turns it into
    #trades[] <- 0
    out <- trades #copy the datastructure over
    currentPos <-0
    for(i in 1:length(out[,1])){
      if((currentPos == 0) & (trades[i,1]==1)){
        currentPos <- 1
        out[i,1] <- currentPos
      if((currentPos == 1) & (trades[i,1]==-1)){
        currentPos <- 0
        out[i,1] <- currentPos
      out[i,1] <- currentPos
reduceShortTradeEntriesToTradOpenOrClosedSignal <- function(trades){
generateTradingReturns <- function(mktPrices, nLookback, nDeviation, avgFunction, deviationFunction,title,showGraph=TRUE){
    quasiMean <- avgFunction(mktPrices,n=nLookback)
    quasiDeviation <- deviationFunction(mktPrices,n=nLookback)
    colnames(quasiMean) <- "QuasiMean"
    colnames(quasiDeviation) <- "QuasiDeviation"
    price <- Cl(mktPrices)
    upperThreshold = quasiMean + nDeviation*quasiDeviation
    lowerThreshold = quasiMean - nDeviation*quasiDeviation
    aboveUpperBand <- price>upperThreshold
    belowLowerBand <- price<lowerThreshold
    aboveMAvg <- price>quasiMean
    belowMAvg <- price<quasiMean
    rawShort <- (-1)*aboveUpperBand+belowMAvg
    shortPositions <- reduceShortTradeEntriesToTradOpenOrClosedSignal(rawShort)
    rawLong <- (-1)*aboveMAvg+belowLowerBand
    longPositions <- reduceLongTradeEntriesToTradOpenOrClosedSignal(rawLong)
    positions = longPositions + shortPositions
    signal <- positions
      plot(Cl(mktPrices),type="l",main=paste(marketSymbol, "close prices"))
      legend('bottomright',c("Close",paste("Band - ",title),paste("Average - ",title)),lty=1, col=c('black', 'red', 'blue'), bty='n', cex=.75)
    mktReturns <- Cl(mktPrices)/Lag(Cl(mktPrices)) - 1
    tradingReturns <- Lag(signal)*mktReturns
    tradingReturns[] <- 0
    colnames(tradingReturns) <- title
    return (tradingReturns)
strategySMAandSTDEV <- function(mktData,nLookback,nDeviation){
       generateTradingReturns(mktData,nLookback,nDeviation,function(x,n) { SMA(Cl(x),n) },function(x,n) { rollapply(Cl(x),width=n, align="right",sd) },"Simple Moving Avg - Standard Deviation",FALSE)
strategySMAandATR <- function(mktData,nLookback,nDeviation){
       generateTradingReturns(mktData,nLookback,nDeviation,function(x,n) { SMA(Cl(x),n) },function(x,n) { atr <- ATR(x,n); return(atr$atr) },"Simple Moving Avg - Average True Range",FALSE)
strategySMAandLRCDev <- function(mktData,nLookback,nDeviation){
        generateTradingReturns(mktData,nLookback,nDeviation,function(x,n) { SMA(Cl(x),n) },function(x,n) { linearRegressionCurveStandardDeviation(Cl(x),n) },"Simple Moving Avg - LRC Deviation",FALSE)
strategyLRCandSTDEV <- function(mktData,nLookback,nDeviation){
       generateTradingReturns(mktData,nLookback,nDeviation,function(x,n) { linearRegressionCurve(Cl(x),n) },function(x,n) { rollapply(Cl(x),width=n, align="right",sd) },"Linear Regression Curve - Standard Deviation",FALSE)
strategyLRCandATR <- function(mktData,nLookback,nDeviation){
       generateTradingReturns(mktData,nLookback,nDeviation,function(x,n) { linearRegressionCurve(Cl(x),n) },function(x,n) { atr <- ATR(x,n); return(atr$atr) },"Linear Regression Curve - Average True Range",FALSE)
strategyLRCandLRCDev <- function(mktData,nLookback,nDeviation){
       generateTradingReturns(mktData,nLookback,nDeviation,function(x,n) { linearRegressionCurve(Cl(x),n) },function(x,n) { linearRegressionCurveStandardDeviation(Cl(x),n) },"Linear Regression Curve - LRC Deviation",FALSE)
bollingerBandsSMAandSTDEVTradingReturns <- strategySMAandSTDEV(mktData,nLookback,nDeviation)
bollingerBandsSMAandATRTradingReturns <- strategySMAandATR(mktData,nLookback,nDeviation)
bollingerBandsSMAandLRCDevTradingReturns <- strategySMAandLRCDev(mktData,nLookback,nDeviation)
bollingerBandsLRCandSTDEVTradingReturns <- strategyLRCandSTDEV(mktData,nLookback,nDeviation)
bollingerBandsLRCandATRTradingReturns <- strategyLRCandATR(mktData,nLookback,nDeviation)
bollingerBandsLRCandLRCDevTradingReturns <- strategyLRCandLRCDev(mktData,nLookback,nDeviation)
mktClClRet <- Cl(mktData)/Lag(Cl(mktData))-1
tradingReturns <- merge(as.zoo(mktClClRet),
charts.PerformanceSummary(tradingReturns,main=paste("Mean Reversion using nLookback",nLookback,"and nDeviation",nDeviation,"bands"),geometric=FALSE)
cat("Sharpe Ratio")
colorFunc <- function(x){
  x <- max(-4,min(4,x))
  if(x > 0){
  colorFunc <- rgb(0,(255*x/4)/255 , 0/255, 1)
  } else {
  colorFunc <- rgb((255*(-1*x)/4)/255,0 , 0/255, 1)
optimiseTradingStrat <- function(mktData,lookbackStart,lookbackEnd,lookbackStep,deviationStart,deviationEnd,deviationStep,strategy,title){
      lookbackRange <- seq(lookbackStart,lookbackEnd,lookbackStep)
      deviationRange <- seq(deviationStart,deviationEnd,deviationStep)
      combinations <- length(lookbackRange)*length(deviationRange)
      combLookback <- rep(lookbackRange,each=combinations/length(lookbackRange))
      combDeviation <- rep(deviationRange,combinations/length(deviationRange))
      optimisationMatrix <- t(rbind(t(combLookback),t(combDeviation),rep(NA,combinations),rep(NA,combinations),rep(NA,combinations)))
      colnames(optimisationMatrix) <- c("Lookback","Deviation","SharpeRatio","CumulativeReturns","MaxDrawDown")
        for(i in 1:length(optimisationMatrix[,1])){
            print(paste("On run",i,"out of",length(optimisationMatrix[,1]),"nLookback=",optimisationMatrix[i,"Lookback"],"nDeviation=",optimisationMatrix[i,"Deviation"]))
            runReturns <- strategy(mktData,optimisationMatrix[i,"Lookback"],optimisationMatrix[i,"Deviation"])
            optimisationMatrix[i,"SharpeRatio"] <- SharpeRatio.annualized(runReturns)
            optimisationMatrix[i,"CumulativeReturns"] <- sum(runReturns)
            optimisationMatrix[i,"MaxDrawDown"] <-  maxDrawdown(runReturns,geometric=FALSE)

          z <- matrix(optimisationMatrix[,"SharpeRatio"],nrow=length(lookbackRange),ncol=length(deviationRange),byrow=TRUE)
          colors <- colorFunc(optimisationMatrix[,"SharpeRatio"])
          rownames(z) <- lookbackRange
          colnames(z) <-deviationRange
          heatmap.2(z, key=TRUE,trace="none",cellnote=round(z,digits=2),Rowv=NA, Colv=NA, scale="column", margins=c(5,10),xlab="Deviation",ylab="Lookback",main=paste("Sharpe Ratio for Strategy",title))
  plot(Cl(mktData),type="l",main=paste(marketSymbol, "close prices"))
  legend('bottomright',c("Close",paste("Simple Moving Average Lookback=50"),paste("Linear Regression Curve Lookback=50")),lty=1, col=c('black', 'red', 'blue'), bty='n', cex=.75)
nLookbackStart <- 20
nLookbackEnd <- 200
nLookbackStep <- 20
nDeviationStart <- 1
nDeviationEnd <- 2.5
nDeviationStep <- 0.1
#optimiseTradingStrat(mktData,nLookbackStart,nLookbackEnd,nLookbackStep,nDeviationStart,nDeviationEnd,nDeviationStep,strategySMAandSTDEV,"AvgFunc=SMA and DeviationFunc=STDEV")
#optimiseTradingStrat(mktData,nLookbackStart,nLookbackEnd,nLookbackStep,nDeviationStart,nDeviationEnd,nDeviationStep,strategySMAandATR,"AvgFunc=SMA and DeviationFunc=ATR")
#optimiseTradingStrat(mktData,nLookbackStart,nLookbackEnd,nLookbackStep,nDeviationStart,nDeviationEnd,nDeviationStep,strategySMAandLRCDev,"AvgFunc=SMA and DeviationFunc=LRCDev")
#optimiseTradingStrat(mktData,nLookbackStart,nLookbackEnd,nLookbackStep,nDeviationStart,nDeviationEnd,nDeviationStep,strategyLRCandSTDEV,"AvgFunc=LRC and DeviationFunc=STDEV")
#optimiseTradingStrat(mktData,nLookbackStart,nLookbackEnd,nLookbackStep,nDeviationStart,nDeviationEnd,nDeviationStep,strategyLRCandATR,"AvgFunc=LRC and DeviationFunc=ATR")
#doptimiseTradingStrat(mktData,nLookbackStart,nLookbackEnd,nLookbackStep,nDeviationStart,nDeviationEnd,nDeviationStep,strategyLRCandLRCDev,"AvgFunc=LRC and DeviationFunc=LRCDev")

High Probability Credit Spreads – Using Linear Regression Curves

I came across this video series over the weekend, an option trader discusses how he trades credit spreads (mainly looks for mean reversion). Most of you will be familiar with bollinger bands as a common mean reversion strategy, essentially you take the moving average and moving standard deviation of the stock. You then plot on to your chart the moving average and an upper and lower band(moving average +/- n*standard deviations).

It is assumed that the price will revert to the moving average hence any price move to the bands is a good entry point. A common problem with this strategy is that the moving average is a LAGGING indicator and is often very slow to track the price moves if a long lookback period is used.

Video 1 presents a technique called “linear regression curves” about 10mins in. Linear regression curves aim to solve the problem of the moving average being slow to track the price.

Linear Regression Curve vs Simple Moving Average

demo of linear regression curve good tracking


See how tightly the blue linear regression curve follows the close price, it’s significantly quicker to identify turns in the market where as the simple moving average has considerable tracking error. The MSE could be taken to quantify the tightness.

How to calculate the linear regression curve:

linear regression diagram

In this example you have 100 closing prices for your given stock. Bar 1 is the oldest price, bar 100 is the most recent price. We will use a 20day regression.

1. Take prices 1-20 and draw the line of best fit through them
2. At the end of your best fit line (so bar 20), draw a little circle
3. Take prices 2-21 and draw the line of best fit through them
4. At the end of your best fit line (so bar 21) draw a little circle
5. Repeat upto bar 100
6. Join all of your little circles, this is your ‘linear regression curve’
So in a nutshell you just join the ends of a rolling linear regression.