The most common bug in vectorised backtests is trading on a signal using the same bar that produced it. Here are six closes and a toy rule: be long if the bar's close is above the previous close.
Code: Select all
close = pd.Series([1.0850, 1.0856, 1.0852, 1.0861, 1.0858, 1.0866, 1.0860])
ret = close.diff() * 1e4 # pips per bar
sig = (close.diff() > 0).astype(int)
bad = (sig * ret).sum() # +23.0 pips
good = (sig.shift(1) * ret).sum() # -13.0 pips
With shift(1) the signal can only be acted on during the next bar. The three positions then earn minus 4, minus 3 and minus 6, a total of minus 13 pips. The strategy went from a genius to a loser by one line.
The check I use now is to shuffle the future: change the last bar's close to a random value and confirm the position and result on earlier bars don't change. If they do, something is reading ahead. Another test is to compare results from running on a series cut at bar N with the same series run on all data, looking at the decisions up to bar N.
If a result looks too good, assume a leak before you assume an edge.