Market data in pandas, and where look-ahead hides
PY · Chapter 413 min readAsked at Citadel Securities, Two Sigma, Jump, IMC
After this lesson you should be able to
- Align two time series without leaking the future.
- Name the four biases that live in a naive backtest.
- Say when
merge_asofis the right join and why an ordinary merge is not.
Most backtests that look too good are not wrong about the model; they are wrong about the timestamps. Handling market data well is mostly a discipline about what was knowable when, and the pandas idioms exist to encode that discipline rather than to be clever.
Definition 4.1
Look-ahead bias
Look-ahead bias — Using information in a decision that was not available at the time of the decision. It rarely arrives as an obvious mistake; it arrives as a shift in the wrong direction, a close-to-close signal traded at the same close, a restated fundamental, or an index membership list downloaded today and applied to 2015.
| Bias | What it is | The fix |
|---|---|---|
| Look-ahead | Using data not yet published at decision time | Lag by the publication delay, not by one bar |
| Survivorship | A universe containing only firms that still exist | Point-in-time universe including delistings |
| Restatement | Fundamentals as revised, not as first reported | Point-in-time fundamentals with an as-of date |
| Selection | A universe or period chosen after seeing results | Fix the universe before the test, and count the variants tried |
import pandas as pd
# WRONG: the signal at time t is multiplied by the return realised at time t.
pnl = signal * returns
# RIGHT: act on the signal at t, earn the return from t to t+1.
pnl = signal.shift(1) * returns
# RIGHT, and honest about execution: a signal computed on the close
# of day t is traded at the open of t+1 at the earliest.
position = signal.shift(1)
pnl = position * returns - position.diff().abs() * cost_per_turnProposition 4.5
Why merge_asof exists
Market data arrives on irregular timestamps, and an ordinary merge on time joins only exact matches — which for tick data means almost nothing joins. merge_asof joins each row to the most recent row on the other side at or before its timestamp, which is exactly the "what did I know at this instant" semantics a backtest needs.
Holds when
- Both frames must be sorted by the join key, or the result is silently wrong.
- The default direction is backward. Using
direction="forward"in a backtest is look-ahead written explicitly. tolerancecaps how stale a match may be, which is how you avoid quoting off an hour-old print.
The rest of this lesson is in Premium
You have read the opening. 9 more sections follow, including 4 worked examples and 3 quick checks.
Nothing is charged for 7 days, and you can cancel before then. Or read Complexity: reading it off, and deriving it in full, free.