Building an honest backtest — anatomy, equity curve, drawdown, turnover
▸ Pretest — guess, even if you don't know
A vectorized backtest computes daily P&L as position times return. To avoid look-ahead bias, which position must be multiplied by today's return?
The five-line backtest
Every vectorized backtest is a variation of this numpy skeleton. Each line is a place where the backtest can lie:
import numpy as np
returns = prices[1:] / prices[:-1] - 1 # r_t. Adjusted for splits/dividends? Point-in-time universe?
signal = signal_from(returns) # one value per day. Does it peek at future bars? Tuned on this data?
position = np.sign(signal) # what you hold: +1 long, -1 short, 0 flat
pnl = position[:-1] * returns[1:] # THE SHIFT: yesterday's position earns today's return. Costs missing!
equity = np.cumprod(1 + pnl) # compound growth of one unit of capital
The annotations are the Big Six from D4-01 mapped onto code. Look-ahead bias hides in lines 2–4, survivorship bias in line 1's universe, cost optimism in line 4, and overfitting in whatever signal_from does. The shift in line 3 is the same fix you implemented in the D4-01 exercise — signals lag returns by one period, always.
The equity curve
equity = np.cumprod(1 + pnl) turns per-period returns into the growth of one unit of capital:
This is what compounding actually does to your account, and it is the input to the risk metric that matters most in practice.
Max drawdown, precisely
Max drawdown is the largest peak-to-trough fall of the equity curve — the worst loss you would have suffered buying at a peak. Define the running maximum , then:
In numpy the running maximum is one call: np.maximum.accumulate(equity). Drawdown matters because it is what makes you (or your investors) quit. A strategy with Sharpe 1.2 and a 60% drawdown will not survive contact with a human being.
Turnover — the cost model's input
Turnover measures how much you trade: with position ,
Going from flat to long is 1 unit; flipping long to short is 2. Total turnover, summed over the backtest, is exactly what transaction costs multiply. A backtest that doesn't report turnover cannot be cost-adjusted, which means it cannot be believed. D4-03 builds the cost model on this number.
Vectorized vs event-driven — the honest trade-off
- Vectorized (this lesson): the whole history as numpy arrays, one pass, milliseconds. Perfect for research iteration. But it silently assumes fills at the close, no partial fills, no order queue, and it makes look-ahead bugs easy to write.
- Event-driven: a loop that feeds the strategy one bar (or one tick) at a time, so it physically cannot see the future, and can model fills, latency, and margin. Slower to run and much slower to build.
The honest workflow: research vectorized, then confirm the survivors event-driven before real money. Neither replaces the other.
Try it
Implement max_drawdown(equity): the maximum peak-to-trough decline of an equity curve, as a fraction. Compute the running maximum with np.maximum.accumulate, then return the max of 1 - equity / running_max.
⧉ Review cardIn a vectorized backtest, how do you pair positions with returns honestly?
⧉ Review cardHow do you compute an equity curve from a return series?
⧉ Review cardWhat is max drawdown and how is it computed?
⧉ Review cardWhat is turnover and why must a backtest report it?
⧉ Review cardVectorized vs event-driven backtests — when to use which?
Draw the machine
Your generative activity: on paper, draw the backtest pipeline as boxes and arrows — prices, signal, shifted position, P&L, equity curve — and mark with an X each spot where one of the Big Six can enter. Then sketch an equity curve that rises, dips, and recovers; draw its running maximum on top and shade the max drawdown. Don't peek at the lesson while drawing.
Predict before the next lesson
D4-03 puts numbers on transaction costs. Predict:
- A strategy earns 10% per year gross and does 250 round trips per year. At 5 bps of cost per round trip, is it still profitable?
- Which do you think is usually larger for a retail trader in liquid stocks: commission or spread?
◈ Calibration check
Could you write the five-line vectorized backtest from memory, with the position shift in the right place, and compute max drawdown and turnover from its outputs?
1 = guessing · 5 = could teach it
⏻ End of lesson
Mark it read to book its 5 review cards into your deck.
Sources & further reading
- bookChan (2009), Quantitative Trading — §3
- bookLópez de Prado (2018), Advances in Financial Machine Learning — §11, 14
- bookBailey, Borwein, López de Prado, Zhu (2014), Pseudo-Mathematics and Financial Charlatanism link