Building an honest backtest — anatomy, equity curve, drawdown, turnover
▸ Pretest — guess, even if you don't know
A vectorized backtest computes daily P&L (profit and loss) as position times return. To avoid look-ahead bias, which position must be multiplied by today's return?
The five-line backtest
A vectorized backtest processes the whole price history at once, as arrays, instead of looping through it day by day. 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 comments are the Big Six from D4-01 mapped onto code. Look-ahead bias hides in lines 2–4. Survivorship bias hides in line 1's universe — the set of assets you chose to include. Cost optimism hides in line 4. Overfitting hides in whatever signal_from does. The shift in line 3 — pairing position[:-1] with returns[1:] — is the same fix you implemented in the D4-01 exercise. Signals lag returns by one period, always.
The equity curve
The equity curve is the running value of your account over time, starting from one unit of capital. equity = np.cumprod(1 + pnl) turns per-period returns into that curve:
In words: the equity at time t, "E sub t," is the product of (1 plus r sub s) over every period s up to and including t. The big Π symbol (capital Greek pi) is the multiplication cousin of Σ — a for-loop that multiplies the terms together instead of adding them.
Small example: returns of +10%, −5%, +2% give equity 1.10 × 0.95 × 1.02 ≈ 1.066 — a total gain of about 6.6%, not the 7% you'd get by adding. Compounding multiplies; it does not add.
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. First define the running maximum, (read: "M sub t is the highest value the equity curve has reached at any period s up to time t"). Then:
In words: at each time t, measure how far equity has fallen from its peak so far — that's 1 minus E sub t divided by M sub t. Max drawdown (MDD) is the largest of those falls anywhere in the history.
Small example: an equity curve that goes 1.0 → 2.0 → 1.0 → 3.0 peaks at 2.0, then dips to 1.0. That dip is 1 − 1/2 = 0.5, a 50% drawdown — even though the curve ends at an all-time high.
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 — the amount of trading you do — is the number transaction costs get multiplied by. With position (what you hold on day t: +1 long, −1 short, 0 flat):
In words: the turnover at time t is the absolute value of today's position minus yesterday's position. The vertical bars mean absolute value — drop any minus sign — because buying and selling both cost money.
Going from flat to long is |1 − 0| = 1 unit; flipping long to short is |−1 − 1| = 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