Lesson 6 · 28 min

Performance metrics & debugging

Sharpe, drawdown, CAGR — and fixing the errors you will see in the lab.

SharpeDrawdownDebugOverfitting

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.

Sharpe (approx.)
0.44
(CAGR − RF) / Vol
Calmar (approx.)
0.55
CAGR / |Max DD|
Interpretation

Weak — check overfitting or costs

Lab errorLikely fix
Signal never readyShorten lookback or extend start date; check ASSETS history.
Import blockedOnly backtest.*, numpy, pandas — see API docs.
Symbol not in whitelistUse ETFs from backtest.universes (SPY, TLT, GLD…).
Equity curve too shortEnsure on_day actually calls set_target_weights.

Metrics glossary

MetricWhat it tells youHealthy scepticism
CAGRAverage yearly growth if the path repeatedOne great decade can inflate it
VolatilityHow bumpy the ride isLow vol can hide leverage or illiquidity
SharpeReturn per unit of riskMeaningless on two years of data
SortinoReturn per unit of *downside* riskKinder to strategies with upside spikes
Max drawdownWorst peak-to-trough lossThe number you must survive emotionally
Alpha / BetaExcess return vs SPY; market sensitivityA proxy benchmark is not your live book
Win ratePercentage of positive daysA 70% win rate can still lose money

Reference points so you know what "good" means

BenchmarkApprox. long-run CAGRApprox. volApprox. SharpeWorst 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%.

DrawdownGain needed to recoverRealistic 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

  1. Read the error message and line number highlighted in the IDE gutter.
  2. Compare your file against the contract: module-level ASSETS, plus make_on_day(prices) returning (on_day, ready).
  3. Click Ask AI for syntax — you get where, why, and paste-ready snippets.
  4. Consult API docs for imports, whitelist symbols, and template examples.
  5. Re-run with a shorter `ASSETS` list and a later start to isolate data problems.

The five errors you will actually hit

ErrorReal causeFix
NameError: ASSETS is not definedASSETS defined inside a functionMove it to module level, top of file
Symbol not in whitelistTicker not simulatable in the labSwap to a whitelisted proxy, e.g. EEM for a single EM stock
TypeError: cannot unpack non-sequencemake_on_day returned only on_dayReturn the tuple (on_day, ready)
KeyError: <date>Indexing a series that has no row for dtReindex to prices.index or use .reindex(close.index).ffill()
Timeout: exceeded 90sPer-day loops over a long historyVectorise with rolling/shift outside on_day
python
# 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})
python
# 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.