# 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: 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.  I will update this post at a later point in time when my optimisation has finished running for the other strategies.

?View Code RSPLUS
 ```library("quantmod") library("PerformanceAnalytics") library("zoo") library("gplots")   #INPUTS 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){ return(sub("^","",name,fixed=TRUE)) }   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(last(fit\$fitted.values)) } 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 #00001111111100000011   #trades[is.na(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 next } if((currentPos == 1) & (trades[i,1]==-1)){ currentPos <- 0 out[i,1] <- currentPos next } out[i,1] <- currentPos }   return(out) }   reduceShortTradeEntriesToTradOpenOrClosedSignal <- function(trades){ return(-1*reduceLongTradeEntriesToTradOpenOrClosedSignal(-1*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 <- pricequasiMean belowMAvg <- price 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) print(optimisationMatrix) } print(optimisationMatrix)       dev.new() 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))   }   if(FALSE){ dev.new() plot(Cl(mktData),type="l",main=paste(marketSymbol, "close prices")) lines(SMA(Cl(mktData),n=50),col="red",type="l") lines(linearRegressionCurve(Cl(mktData),n=50),col="blue",type="l") 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")```

## 6 thoughts on “Linear Regression Curves vs Bollinger Bands”

1. steve on said:

Great analysis. Any idea why using the ATR produces such good results? I’ll use your code and play around with some of the parameters. Thanks for posting it.

2. Gian Marco Vianello on said:

I’m trying to create a strategy based on the bollinger bands and your scripts is pretty great.
I need to use my data to check how my strategy will work. Can you help me to change the sript for my data?

3. Ping on said:

I’m new to programming. Your code is very compact; I’m learning from it.

The ATR function expects High, Low and Close. Since the mktData columns are Open, High, Low, Close, Volume and Adjusted, I think the ATR argument should be: ATR(x[,2:4], n). See below:

strategyLRCandATR <- function(mktData,nLookback,nDeviation){
generateTradingReturns(mktData,nLookback,nDeviation,function(x,n) { linearRegressionCurve(Cl(x),n) },function(x,n) { atr <- ATR(x[,2:4],n); return(atr\$atr) },"Linear Regression Curve – Average True Range",FALSE)

Thank you for sharing your codes!

4. GQ,

Is there any reason you wrote all of the trade generation code yourself, rather than used quantstrat?

-Ilya

• GekkoQuant on said:

Hey,

I’ve found quantstrat to be incredibly slow at certain tasks, so I lost interest in it quickly. However that was over 2 years ago things may have improved now.

5. Jack on said:

Thank you Gekko for sharing your codes and learned a lot from you.
one question: the codes below are copied from your “optimisationMatrix” function.
——
runReturns <- strategy(mktData,optimisationMatrix[i,"Lookback"],optimisationMatrix[i,"Deviation"])
——
however, I could not find where/how the function strategy above is defined, it seems coming from nowhere but the overall codes work fine.
would really appreciate if you could shed some light on this.