Basic Prerequisites: None. Comfort with Python and the idea of a price series is enough. This page is the beginner entry point to the folder.
1. Intuition & Practical Objective
You have a price series, you have a moving average, and you want to know if “buy when price is above its 20-day average” makes money. The fast way is one line of vectorized arithmetic:
That is the backtest everybody writes first. It is also wrong in a way that produces spectacular fake profits, and this page exists to show you exactly why - with a runnable experiment and no hand-waving.
The objective is one idea: a backtest must answer “what could I have earned given only what I knew, and only at prices I could have paid?” The vectorized line above answers a different question: “what would I have earned if I had known the bar’s close, and could trade at that same close, for free, in any size?” Those are not the same question and they do not have the same answer.
2. Mathematical Ground Truth & Derivations
Where the free money comes from. Look at the definition of again: it is a function of . And is also a function of (it is ). So the product correlates a quantity with itself. Write for a small deviation ; then
where is the bar’s own return. On a pure random walk is i.i.d. zero-mean - and yet by construction, because the condition (price above its own recent average) is partly satisfied by this bar being up. You are not detecting momentum; you are detecting your own index. Formally, conditional on the signal being on, the return is not zero-mean:
This is the look-ahead identity: signal.shift(0) in a vectorized backtest is not “using today’s close to trade today’s close” - it is “using tomorrow’s information to trade yesterday,” because the close is not knowable until the bar is over.
The fix in one token. Shift the signal one bar:
Now the weight that multiplies is measurable at , and on a random walk. The whole edge vanishes - which is the point, because there never was one.
The execution reality the shift still hides. Even shift(1) assumes you get filled at the next bar’s close, at zero cost, at unlimited size. A real engine replaces that with a fill condition and a cost:
where is unavoidable (the signal needs a completed bar) and is the quoted spread: crossing the half-spread takes you from the mid to the ask (so the fully-crossed fill price is just - the mid form is the bookkeeping identity, not an extra cost). The event-driven pages make each term of this equation an explicit number.
3. Computational Implementation - the lie, measured
The cleanest possible demonstration: generate pure random walks (zero predictability by construction), run both backtests, average over 300 paths. A correct backtest must report a Sharpe of .
Read it slowly. The data is a coin flip with no memory. The naive backtest reports a Sharpe of 3.44 - a number that would clear almost any fund’s bar. The honest backtest reports 0.03, i.e. nothing. The entire “edge” was the off-by-one-bar index error, and nothing else.
That is what “vectorized backtests are lethal illusions” means, stated as a reproducible number: the bug is not a rounding error, it is the whole signal.
4. Failure Modes & First-Principles Breakdowns
shift(0)look-ahead. Any weight that depends on while multiplying is reading the future. This is the single most common backtest bug in existence and it is invisible in the code - the vector operation looks like ordinary arithmetic.- The bar-close ambiguity. “Trade at the close of the signal bar” is undefined in real markets: at the moment the close is known, that price is gone. The minimum honest assumption is the next bar (the structural of 03 · The Event Loop).
- Vector operations compose look-ahead. A rolling
mean, apct_change, and ashiftcan interact so that a later shift undoes the fix applied to an earlier step. This is why the discipline is architectural (an event loop that literally cannot see the future), not a matter of remembering to call.shift(1). - Zero cost is a free option. Mid-price fills and free execution mean every strategy looks better than it is; the correction is the execution shortfall of §02 of the hub lookup.
- A good backtest is not a good strategy. Fixing the look-ahead is necessary and not sufficient: the remaining number is still the maximum of a search. That is Backtesting Hygiene’s deflated-Sharpe problem, and it is why this folder cross-links there.
5. References
- Hilpisch, Yves, Python for Algorithmic Trading (O’Reilly, 2020)
- Halls-Moore, Michael, *QuantStart
- López de Prado, Marcos, Advances in Financial Machine Learning (Wiley, 2018)
- Chan, Ernie, Quantitative Trading (Wiley, 2nd ed., 2021)
6. Connected Graph Bridges
- Continue: 02 · Architecture - the five components that replace the one-line backtest · Index Hub
- Cause: Backtesting Hygiene - why the honest Sharpe is still too high
- Effect: 04 · Vectorized vs Event-Driven - the same signal under three engines
- Sibling: Execution, Backtesting & Simulation - the impact models the engine will call