Lesson 6 · 28 min
Performance metrics & debugging
Sharpe, drawdown, CAGR — and fixing the errors you will see in the lab.
A backtest without metrics is just a chart. Quant Buffet reports risk-adjusted statistics so you can compare strategies honestly — and gives you an AI debugger with line numbers when the code breaks.
Drag the sliders to see how Sharpe and Calmar react. These are the same families of stats Quant Buffet reports after a lab run.
Weak — check overfitting or costs
| Lab error | Likely fix |
|---|---|
| Signal never ready | Shorten lookback or extend start date; check ASSETS history. |
| Import blocked | Only backtest.*, numpy, pandas — see API docs. |
| Symbol not in whitelist | Use ETFs from backtest.universes (SPY, TLT, GLD…). |
| Equity curve too short | Ensure on_day actually calls set_target_weights. |
Metrics glossary
| Metric | What it tells you | Healthy scepticism |
|---|---|---|
| CAGR | Average yearly growth if the path repeated | One great decade can inflate it |
| Volatility | How bumpy the ride is | Low vol can hide leverage or illiquidity |
| Sharpe | Return per unit of risk | Meaningless on two years of data |
| Sortino | Return per unit of *downside* risk | Kinder to strategies with upside spikes |
| Max drawdown | Worst peak-to-trough loss | The number you must survive emotionally |
| Alpha / Beta | Excess return vs SPY; market sensitivity | A proxy benchmark is not your live book |
| Win rate | Percentage of positive days | A 70% win rate can still lose money |
Reference points so you know what "good" means
| Benchmark | Approx. long-run CAGR | Approx. vol | Approx. Sharpe | Worst drawdown |
|---|---|---|---|---|
| SPY buy & hold | ≈ 10% | ≈ 15–19% | ≈ 0.4–0.5 | ≈ -55% |
| Classic 60/40 | ≈ 8% | ≈ 10% | ≈ 0.5–0.6 | ≈ -30% |
BIL (cash) | Policy rate | ≈ 0.3% | n/a | ≈ -0.1% |
Calibrate against these before getting excited. A long-only daily ETF strategy with a Sharpe of 0.8 is genuinely good. A Sharpe of 1.5 is exceptional and demands scrutiny. A Sharpe above 2 in a simple daily backtest almost always means look-ahead bias, an unrealistic cost assumption, or a survivor-picked universe. For context: Madoff's fabricated returns implied a Sharpe of roughly 2.5, and that implausibility was one of the earliest public clues.
Why drawdown matters more than CAGR
Drawdown is where backtests meet human beings. Two facts make it concrete. A 50% loss requires a 100% gain to recover, not 50%. And recovery can take an investing lifetime: the Nasdaq Composite peaked in March 2000 and did not durably reclaim that level until 2015 — roughly fifteen years of holding an underwater position. Meanwhile the fastest crash in modern history, February to March 2020, took only 33 days to fall about 34%.
| Drawdown | Gain needed to recover | Realistic reaction |
|---|---|---|
| -10% | +11% | Uncomfortable |
| -20% | +25% | You question the model |
| -35% | +54% | Most people abandon the strategy here |
| -50% | +100% | Career or capital risk |
Multiple testing: the statistics of trying again
If you test one strategy, a Sharpe of 1.0 is evidence. If you test a thousand and report the best, a Sharpe of 1.0 is close to what pure randomness delivers — the maximum of many noisy draws is large by construction. This is why the literature developed the deflated Sharpe ratio (Bailey and López de Prado), which penalises a result by how many attempts produced it.
- Count your attempts. Ten IDE re-runs with different lookbacks is ten tests, even if you only screenshot one.
- Prefer parameter plateaus. If 150, 200 and 250 days all work, the effect is probably real. If only 187 works, it is noise.
- Hold back a period. Fit your intuition on 2000–2015, then look at 2016–today once.
- Demand a reason. Every library strategy carries an economic rationale precisely because "the numbers liked it" is not one.
Debugging workflow
- Read the error message and line number highlighted in the IDE gutter.
- Compare your file against the contract: module-level
ASSETS, plusmake_on_day(prices)returning(on_day, ready). - Click Ask AI for syntax — you get where, why, and paste-ready snippets.
- Consult API docs for imports, whitelist symbols, and template examples.
- Re-run with a shorter `ASSETS` list and a later
startto isolate data problems.
The five errors you will actually hit
| Error | Real cause | Fix |
|---|---|---|
NameError: ASSETS is not defined | ASSETS defined inside a function | Move it to module level, top of file |
Symbol not in whitelist | Ticker not simulatable in the lab | Swap to a whitelisted proxy, e.g. EEM for a single EM stock |
TypeError: cannot unpack non-sequence | make_on_day returned only on_day | Return the tuple (on_day, ready) |
KeyError: <date> | Indexing a series that has no row for dt | Reindex to prices.index or use .reindex(close.index).ffill() |
Timeout: exceeded 90s | Per-day loops over a long history | Vectorise with rolling/shift outside on_day |
# Slow: recomputes a 200-day mean on every single day (times out)
def on_day(engine, dt):
window = prices.loc[:dt, "SPY"].tail(200)
if prices.loc[dt, "SPY"] > window.mean():
engine.set_target_weights(dt, {"SPY": 1.0})
# Fast: compute once, then look up
sma = prices["SPY"].rolling(200, min_periods=200).mean()
def on_day(engine, dt):
if prices.loc[dt, "SPY"] > sma.loc[dt]:
engine.set_target_weights(dt, {"SPY": 1.0})# What the platform reports after a successful run
metrics = compute_metrics(result.equity, benchmark=spy_bh, trades_count=len(result.trades))
print(metrics["sharpe"], metrics["max_drawdown"], metrics["cagr"])Before Lesson 7 — you should be able to
- State roughly what Sharpe and drawdown look like for SPY and 60/40.
- Explain why a Sharpe above 2 in a daily ETF backtest is a red flag.
- Convert a drawdown into the gain required to recover.
- Diagnose the five common lab errors without help.
- Describe how multiple testing inflates the best result you find.