This post will outline an easy-to-make mistake in writing vectorized backtests–namely in using a signal obtained at the end of a period to enter (or exit) a position in that same period. The difference in results one obtains is massive.
Today, I saw two separate posts from Alpha Architect and Mike Harris both referencing a paper by Valeriy Zakamulin on the fact that some previous trend-following research by Glabadanidis was done with shoddy results, and that Glabadanidis’s results were only reproducible through instituting lookahead bias.
The following code shows how to reproduce this lookahead bias.
First, the setup of a basic moving average strategy on the S&P 500 index from as far back as Yahoo data will provide.
require(quantmod) require(xts) require(TTR) require(PerformanceAnalytics) getSymbols('^GSPC', src='yahoo', from = '1900-01-01') monthlyGSPC <- Ad(GSPC)[endpoints(GSPC, on = 'months')] # change this line for signal lookback movAvg <- SMA(monthlyGSPC, 10) signal <- monthlyGSPC > movAvg gspcRets <- Return.calculate(monthlyGSPC)
And here is how to institute the lookahead bias.
lookahead <- signal * gspcRets correct <- lag(signal) * gspcRets
These are the “results”:
compare <- na.omit(cbind(gspcRets, lookahead, correct)) colnames(compare) <- c("S&P 500", "Lookahead", "Correct") charts.PerformanceSummary(compare) rbind(table.AnnualizedReturns(compare), maxDrawdown(compare), CalmarRatio(compare)) logRets <- log(cumprod(1+compare)) chart.TimeSeries(logRets, legend.loc='topleft')
Of course, this equity curve is of no use, so here’s one in log scale.
As can be seen, lookahead bias makes a massive difference.
Here are the numerical results:
S&P 500 Lookahead Correct Annualized Return 0.0740000 0.15550000 0.0695000 Annualized Std Dev 0.1441000 0.09800000 0.1050000 Annualized Sharpe (Rf=0%) 0.5133000 1.58670000 0.6623000 Worst Drawdown 0.5255586 0.08729914 0.2699789 Calmar Ratio 0.1407286 1.78119192 0.2575219
Again, absolutely ridiculous.
Note that when using Return.Portfolio (the function in PerformanceAnalytics), that package will automatically give you the next period’s return, instead of the current one, for your weights. However, for those writing “simple” backtests that can be quickly done using vectorized operations, an off-by-one error can make all the difference between a backtest in the realm of reasonable, and pure nonsense. However, should one wish to test for said nonsense when faced with impossible-to-replicate results, the mechanics demonstrated above are the way to do it.
Now, onto other news: I’d like to thank Gerald M for staying on top of one of the Logical Invest strategies–namely, their simple global market rotation strategy outlined in an article from an earlier blog post.
Up until March 2015 (the date of the blog post), the strategy had performed well. However, after said date?
It has been a complete disaster, which, in hindsight, was evident when I passed it through the hypothesis-driven development framework process I wrote about earlier.
So, while there has been a great deal written about not simply throwing away a strategy because of short-term underperformance, and that anomalies such as momentum and value exist because of career risk due to said short-term underperformance, it’s never a good thing when a strategy creates historically large losses, particularly after being published in such a humble corner of the quantitative financial world.
In any case, this was a post demonstrating some mechanics, and an update on a strategy I blogged about not too long ago.
Thanks for reading.
NOTE: I am always interested in hearing about new opportunities which may benefit from my expertise, and am always happy to network. You can find my LinkedIn profile here.