Why does this Co-integrated basket look too good to be true?

You need quantmod & tseries in R to run this:

library(quantmod)
library(tseries)

pairs <- c(
"EUR/USD",
"GBP/USD",
"AUD/USD",
"USD/CHF",
"NZD/USD"
)

name <- function(n) {
gsub("/","",n,fixed=TRUE)
}

getFrame <- function(p) {
result <- NULL
as.data.frame(lapply(p, function(x) {
if(!exists(name(x))) {
getSymbols(x, src="oanda")
}
if(is.null(result)) {
result <- get(name(x))
} else {
result <- merge(result, get(name(x)))
}
}))
}

isStationary <- function(frame) {
model <- lm(frame[,1] ~ as.matrix(frame[,-1]) + 0)
spread <- frame[,1] - rowSums(coef(model) * frame[, -1])
if(results$p.value < 0.05) { coefficients <- coef(model) names(coefficients) <- gsub("as.matrix.frame.......", "", names(coefficients)) plot(spread[1:100], type = "b") cat("Minimum spread: ", min(spread), "\n") cat("Maximum spread: ", max(spread), "\n") cat("P-Value: ", results$p.value, "\n")
cat("Coeficients: \n")
print(coefficients)
}
}

frame <- getFrame(pairs)
isStationary(frame)


I get FX daily data from Oanda, do a simple linear regression to find the hedging ratios, and then use the Augmented DF test to test for the P-value of mean reversion in the spread.

When I run it I get this:

Minimum spread:  -1.894506
P-Value:  0.03781909
Coeficients:
0.59862816  0.48810239 -0.12900886  0.04337268  0.02713479


EUR.USD coefficient is 1.

When I plot the spread the first 100 days look like this:

Surely something must be wrong. The holy grail shouldn't be so easy to find.

Can someone help me find what is wrong?

I tried backtesting on Dukascopy with the above coefficients as lot sizes of a basket, but I run into loses. And the spread has a different order of magnitude in dukascopy. Why is that?

-
Since you are modeling currency pairs across the globe there might be time effects in the model. For example, one currency pair may contain information about another currency pair because of timings of when markets open and close but the marks all take place on the same date. –  Quant Guy Aug 17 '11 at 23:56
"the marks all take place on the same date", what does that mean? –  louzer Aug 18 '11 at 0:16
By marks I mean when the currency pairs are quoted. Since there is a USD on each contract, if these currency pairs trade co-terminously then my hypothesis is false. On the other hand if one currency pair's quote for the day closes ahead of another currency pair but they are both marked on the same date then you might have a spurious result that is simply based on calendar/time effects. –  Quant Guy Aug 18 '11 at 3:06

@Sergey correctly identified the problem. The explanation is that coef(model) is a vector, frame is a data.frame, and element-by-element multiplication takes place in column-major order. The shorter vector (coef(model)) is recycled along the longer vector (each column in frame). For example:

frame <- data.frame(V1=1:5)
frame$V2 <- 2 frame$V3 <- 5
coef.model <- 1:3
frame * coef.model
#   V1 V2 V3
# 1  1  6 10
# 2  4  2 15
# 3  9  4  5
# 4  4  6 10
# 5 10  2 15


What you intended was something like this:

sweep(frame,2,coef.model,"*")
#   V1 V2 V3
# 1  1  4 15
# 2  2  4 15
# 3  3  4 15
# 4  4  4 15
# 5  5  4 15

-

The main problem in your code is this line:

rowSums(coef(model) * frame[, -1])

I'm not sure exactly what is does, perhaps some matrix multiplication, but definitely not what you expect it to do. Try to replace it with manual multiplication

spread <- frame[,1] - (coef(model)[1]*frame[,2] + coef(model)[2]*frame[,3] + coef(model)[3]*frame[,4] + coef(model)[4]*frame[,5] + coef(model)[5]*frame[,6])

And holy grail will disappear

I can see a couple of other errors as well:

1. You cant sell on ASK price. With getSymbols.oanda you always get ASK
2. You'd better separate testing and training data sets
-
Wow. Thanks! Thanks a lot! I know I shouldn't be glad that you've taken the grail from me. But your logic feels so good. –  louzer Aug 18 '11 at 19:26