Runbook: CPCV backtest with PSR and deflated Sharpe
Notebook: notebooks/python/12_cpcv_deflated_sharpe.ipynb,
committed with its outputs and executed in CI. Run it with just notebooks-run --only 12; set
OPENQUANT_RUNBOOK_RUNS=1000 for more datasets per strength (CI runs 200).
The question
Section titled “The question”A researcher backtests all 74 configurations of a moving-average crossover family, reports the best one, and asks whether its Sharpe ratio is evidence of skill. AFML §11.6 says a best-of-N backtest mostly reports luck. §12.4 offers combinatorial purged cross-validation (CPCV) as a distribution of out-of-sample paths instead of one. §14.7 offers the deflated Sharpe ratio (DSR), which tests the best Sharpe ratio against SR0, the best that N skill-less trials would reach. This runbook checks those claims on data where the right answer is known.
Design
Section titled “Design”- Data. Each dataset is 10 years of daily returns with Student- noise and, optionally, a
persistent latent drift (an AR(1) with coefficient 0.99). The drift’s strength is the signal.
- At strength 0 there is no drift: the no-signal control.
- At strength 0.2 (the planted signal) the best configuration has a true annualised Sharpe ratio of 1.73. Each configuration’s true Sharpe ratio is measured on one 250,000-bar path.
- A power scan runs six strengths × 200 datasets.
- Strategies. A crossover of a fast moving average (2–34 bars) and a slow one (10–120 bars),
long/short or long/flat: 74 configurations. Costs are 5 bps per unit of position change.
- Every configuration is recorded in an
evaluation.TrialRegistrybefore anything is selected. - DSR takes N and σ(SR) from the registry.
- Every configuration is recorded in an
- Validation.
cross_validation.cpcv_splitswith N = 10 groups and k = 3 test groups gives 120 splits and φ[10, 3] = 36 paths.- In each split the procedure selects the configuration with the best training Sharpe ratio and trades it on the test groups.
backtesting_engine.run_cpcvscores the splits and paths.assemble_cpcv_pathsstitches them together.- For comparison there is one anchored walk-forward path: 2 years to start, re-selected yearly.
- No lookahead, tested.
- Labels span one bar. A 126-bar embargo follows each test block, which covers the 120-bar lookback (AFML Snippet 7.3, as fixed in #170).
- A test cell corrupts every test-period return in every split and rebuilds all 74 strategies. It asserts that every training sample’s net return is bit-identical, and that the same check fails with the embargo off.
Hypotheses and results (200 datasets per strength, SYNTHETIC)
Section titled “Hypotheses and results (200 datasets per strength, SYNTHETIC)”| Rule, fixed before the run | Result | ||
|---|---|---|---|
| H1a | With no signal, naive PSR(0) > 0.95 on the in-sample best fires in more than 5% of datasets | 60 / 200 = 30% | supported |
| H1b | With no signal, DSR > 0.95 fires in no more than 5% | 0 / 200 | supported |
| H2 | With the planted signal (true SR 1.73), DSR > 0.95 in more than half | 161 / 200 = 80.5% | supported |
Power scan, means over 200 datasets per strength (Sharpe ratios annualised):
| Best config’s true SR | Naive PSR > 0.95 | DSR > 0.95 | In-sample best SR | True SR of the selected config | CPCV mean path SR | Walk-forward SR |
|---|---|---|---|---|---|---|
| 0.00 (control) | 30% | 0% | 0.39 | −0.09 | −0.09 | −0.08 |
| 0.17 | 51% | 0% | 0.52 | 0.06 | 0.04 | 0.03 |
| 0.74 | 91% | 20% | 0.95 | 0.56 | 0.50 | 0.49 |
| 1.19 | 99.5% | 49% | 1.37 | 1.02 | 0.93 | 0.91 |
| 1.73 (planted) | 100% | 81% | 1.91 | 1.58 | 1.51 | 1.49 |
| 2.87 | 100% | 95% | 2.94 | 2.68 | 2.52 | 2.53 |
What it shows
Section titled “What it shows”- Deflation works, and on correlated trials it is conservative.
- The naive test finds skill in 30% of no-signal datasets. DSR finds it in none, where 5% would be allowed.
- The 74 configurations are highly correlated, so the effective number of trials is far below the N that DSR uses.
- The cost is power: 20% at a true Sharpe ratio of 0.74, and 49% at 1.19.
- With a real signal, the trials’ Sharpe ratios also differ for real reasons. That raises σ(SR), and so SR0 (0.93 against 0.27 on the headline datasets).
- CPCV and walk-forward both remove the selection bias.
- Both land within about 0.1 of the true Sharpe ratio of what was selected. The in-sample best overstates it by 0.3 to 0.5.
- CPCV’s mean varies a little less across datasets than the single walk-forward path: s.d. 0.31 against 0.38 on the controls.
- The spread of CPCV paths is not a confidence interval.
- Within one dataset, the 36 paths have a standard deviation of about 0.21 at every strength.
- The CPCV mean varies more than that across datasets: 0.30 to 0.77.
- The paths reuse the same returns and differ only in which training set picked each group’s configuration. Their spread measures how unstable the selection is, not the sampling error.
- Headline datasets.
- Control, dataset 0. The in-sample best has SR 0.33, PSR(0) 0.85 and DSR 0.57. MinTRL is 24.7 years, against 10 in the sample. The CPCV median is −0.33, with 3 of 36 paths positive, and the walk-forward is −0.48.
- Planted, dataset 0. The in-sample best has SR 2.23 (true 1.69), and DSR is 1.00. The CPCV median is 1.80, with all 36 paths positive, and the walk-forward is 1.92.
This notebook also delivers the CPCV Sharpe-distribution figure (OQ-nbr.8) that the notebook runner (#45) left pending until the CPCV bindings existed.
Decision
Section titled “Decision”Promote the evaluation protocol, not a strategy. Every strategy-selection runbook should:
- register every configuration in a
TrialRegistry; - report DSR with the registry’s N next to any PSR;
- report a CPCV (or walk-forward) Sharpe ratio instead of the in-sample best;
- state MinTRL.
Keep two caveats with it. A DSR below 0.95 on a correlated grid is weak evidence of no skill, and CPCV path spread is not a confidence interval. An effective-number-of-trials estimate for the registry is a worthwhile follow-up. The MA-crossover family itself has no edge on the data-layer sample, which is random walks.
Audit: features with memory (#217)
Section titled “Audit: features with memory (#217)”Runbook 14 found that purged k-fold and CPCV flatter features with memory
(cross_validation).
This runbook fits no model on a feature: each split selects the configuration with the best
training Sharpe ratio, and positions use past closes only. Its no-signal control is a null
through the same CPCV splits and shows no inflation: CPCV mean path Sharpe ratio −0.088,
walk-forward −0.077, true Sharpe ratio of the selection −0.091. Not affected.
Run it on your own data
Section titled “Run it on your own data”The notebook’s last section runs the same pipeline on bars read by
openquant.data.fetch. The environment variables are the same as
runbook 09’s. Keep a trial registry across runs:
OPENQUANT_RUNBOOK_SOURCE=/path/to/daily_ohlcv.parquet \OPENQUANT_RUNBOOK_SYMBOLS=ES,NQ,CL \OPENQUANT_RUNBOOK_RANGE=2010-01-01:2024-12-31 \OPENQUANT_RUNBOOK_REGISTRY_DIR=$HOME/.openquant/trials \OPENQUANT_FIGURE_DIR=/tmp/cpcv-figures \ uv run --python .venv/bin/python python notebooks/python/scripts/execute_notebook_cells.py \ notebooks/python/12_cpcv_deflated_sharpe.ipynb --out /tmp/12_on_my_data.ipynbThat section uses N = 6, k = 2 and an embargo of at least 120 bars. The committed sample covers only two years, so most of each training set is embargoed: use many years of data. Every grid you try on the same data goes into the same registry, so the deflation counts every trial.