1
$\begingroup$

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

# download data from yahoo/google
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")
lines(cumprod(1+results$BuyHold)-1, col='red')

And the output: Output

$\endgroup$
1
$\begingroup$

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.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.