# Where is the bias in this simple backtesting framework?

I usually write my backtests in Python or a dedicated backtesting environment, but I wanted to experiment with some of R's predictive analytics functionality. I wrote an extremely simple backtester for doing some quick and dirty research, but wind up with some obvious bias in my results. I assume the bias is related to future-peeking, but I am not sure.

The code uses a rolling window approach and creates a list of adjacent training/testing windows, as per a standard walk-forward approach. For each training window, it fits a simple linear regression model based on some lagged variables and the next-day's return. It then tests the model on the adjacent testing window. After looping through the list of windows, the results are aggregated and plotted.

Based on my code and output shown below, is it obvious where the bias is coming from? Looking at the results, I must have somehow mixed my training and testing windows, or possibly I am not understanding the correct way to lag the dependent variables. Either way, I am stuck and any advice would be greatly appreciated.

## Libraries
library(caret);library(quantmod);library(TTR);require(ggplot2);require(reshape)

## Data Acquisition and Wrangling

getSymbols("SPY", from="2010-01-01")

# create returns series and some indicators
# remember to lag indicators to prevent look-ahead bias!
ret <- Delt(Op(SPY), Cl(SPY)) #day session return
atr <- ATR(SPY[, c(2:4)], 14)$atr rsi <- RSI(Cl(SPY), n=14, maType="SMA") #relative strength index dpo <- DPO(Cl(SPY)) #de-trended price oscillator dat <- cbind(ret, lag(atr, 1), lag(rsi, 1), lag(dpo, 1)) colnames(dat) <- c("ret", "atr", "rsi", "dpo") dat <- as.data.frame(dat[complete.cases(dat), ]) #get rid of rows with NA, NaN etc ## Set up train-test windows # create indexes for TSCV windows init = 200 #initial window horiz = 50 #prediction horizon wdw <- createTimeSlices(1:nrow(dat), initialWindow = init, horizon = horiz, skip = horiz-1, fixedWindow = TRUE) trainSlices <- wdw[[1]] testSlices <- wdw[[2]] # verify visually correct window setup: trainSlices[[length(trainSlices)]] testSlices[[length(testSlices)]] # train and test linear regression model on rolling window ModelReturn <- list() #store return of each out-of-sample window BHReturn <- list() #store buy and hold returns for comparison for(i in c(1:length(testSlices))) { model <- lm(ret ~., data=dat[trainSlices[[i]], ]) preds <- predict(model, newdata=dat[testSlices[[i]], -1]) returns <- ifelse(preds>0, dat[testSlices[[i]], 1], -dat[testSlices[[i]], 1]) ModelReturn[[i]] <- returns BHReturn[[i]] <- dat[testSlices[[i]], "ret"] } # plot results results <- data.frame(unlist(ModelReturn), unlist(BHReturn)) colnames(results) <- c("Model", "BuyHold") plot(cumprod(1+results$Model)-1, type='l', col='blue', ylab="Cum.Return")


And the output:

The DPO() function shifts the series into the future, thus creating the look-ahead bias that causes the outlandish results. Setting shift to zero takes care of this.

From the ?DPO documentation:

### Description

The Detrended Price Oscillator (DPO) removes the trend in prices - or other series - by subtracting a moving average of the price from the price.

DPO(x, n = 10, maType, shift = n/2 + 1, percent = FALSE, ...)

### Arguments:

shift: The number of periods to shift the moving average.