Table of contents
Information has a timestamp
A vectorized backtest becomes dangerous when a row mixes values that were known at different times. The fix is not to avoid vectorization; it is to encode the information boundary explicitly.
Suppose a signal is computed using today’s close and the strategy trades at the next open. The position applied to that next return must be lagged.
prices["signal"] = (
prices["close"].rolling(20).mean()
> prices["close"].rolling(100).mean()
).astype(float)
prices["position"] = prices["signal"].shift(1)
prices["next_return"] = prices["open"].shift(-1) / prices["open"] - 1
prices["strategy_return"] = prices["position"] * prices["next_return"]
Align by meaning, not convenience
The names make the convention reviewable: the signal is observed, the position is delayed, and the return points forward. Dropping missing values should happen only after those transformations, otherwise alignment errors can be hidden by a tidy index.
Tests worth keeping
I like three small tests: perturb the final price and confirm earlier signals do not change; compare a few rows to a hand-worked event loop; and rerun with an extra lag to see whether the result behaves plausibly.
No individual test proves the absence of look-ahead bias. Together they make the information flow legible—which is the more durable objective.