CPCV in autoinv: concrete implementation in validation.py, and the minimum trade count for a trustworthy deflated-Sharpe
The question
Direct build-spec input for the autoinv validation layer (GitHub RayDataCo/automated-investing). Two parts: (1) concretely, how does combinatorial purged cross-validation (CPCV) get implemented in validation.py — function signatures, the path-count math, purge/embargo mechanics; and (2) what is the minimum trade (observation) count below which a deflated-Sharpe number is not trustworthy and should not be reported. This feeds the backtest-thesis harness's honest-statistics requirement (no 2-decimal Sharpe on tiny trade counts) and the Phase-2 anti-data-mining GATE in the strategy-pipeline architecture.
Load-bearing finding up front: the current autoinv/validation.py does NOT implement CPCV, purging, embargo, or a deflated Sharpe at all. It wraps sklearn TimeSeriesSplit (plain walk-forward, single path) plus a four-bias audit checklist; there is no DSR/PBO anywhere in the repo (metrics.py, stats.py, tests confirm). So "how does it concretely implement" is answered as a gap + a spec, not a description of existing code. [verified from source, 2026-06-01]
What we already know (from the vault)
- The parent [[2026-05-31-ensembles-systematic-trading-overfitting]] brief already encodes the de Prado machinery: purging (drop training rows whose event times overlap test-fold trade times — financial labels are path-dependent), embargoing (drop rows near fold boundaries to kill lookback leakage), CPCV (generates multiple backtest paths from one history), and the False-Strategy framing E[max SR] ≈ √(2·ln N)·σ(trial Sharpes) so best-of-N looks good by construction.
- [[2026-05-29-strategy-pipeline-architecture-v0]] specs the validation layer as GATES not reports: GATE 1 = Deflated Sharpe (DSR<0.5 reject / DSR>1.0 confidence; N auto-counted cumulatively across sessions; honest caveat that correlated grid cells make effective N < counted N so naive DSR under-deflates), GATE 2 = PBO via CSCV, GATE 0 = economic prior. The sealed holdout is the real arbiter; DSR is "necessary not sufficient."
- [[investing-backtest-thesis]] (v2 SKILL) is the source of the honest-stats rule: trade-count-aware precision — <10 trades → no decimals; 10-50 → 1 decimal; 50+ → 2 decimals; bootstrap CIs and leave-one-out mandatory; "Sharpe on ~3-6 trades reported with 2-decimal precision implied false rigor."
- The repo itself ([[01-projects/automated-investing/index]]) is Phase-1 only: event-driven spine (
engine,data,validation,metrics,portfolio,pricing) + no-lookahead guard. PBO/DSR/holdout were explicitly deferred to Phase-2 ("the gate") — so the gap below is expected, not a regression. - The single existing in-repo nod:
experiments/level-2b-time-series-cv.mdnames "purged k-fold (López de Prado's approach)" as the thing future backtests need — aspirational, not built.
What the web says
- CPCV core algorithm (Wikipedia / quantinsti / Towards AI, de Prado AFML lineage): divide the series into N sequential, non-overlapping, time-ordered groups; choose k groups as the test set (k<N, typically k=2); train on the remaining N−k. The number of train/test splits = C(N,k) (N-choose-k). Each observation lands in multiple test sets, so the number of distinct backtest paths is φ[N,k] = (k/N)·C(N,k) (equivalently C(N−1,k−1)).
- Worked example (verbatim): N=6, k=2 → C(6,2)=15 splits → φ[6,2]=(2/6)·15=5 paths; each group appears in exactly 5 test splits, reassembled into 5 full-history out-of-sample equity paths. This is the key advantage over walk-forward (one path) and plain k-fold (ignores time): CPCV yields a distribution of OOS Sharpes, not a point estimate.
- Purging (verbatim): "Any training observations whose label horizon overlaps with the test period are excluded." Mechanically: for a label at time t with an event/exit time t1, drop any training observation whose [t, t1] span intersects any test observation's span. Necessary because financial labels are path-dependent (a trade opened before the test window can resolve inside it).
- Embargo (verbatim): after each test period, "a fixed number of observations (typically a small percentage) are removed from the training set" that immediately follow the test block — kills leakage from serial correlation / delayed market reaction. Embargo size is set as a fraction h of total observations (de Prado's default is on the order of ~1%, scaled up when features use long lookbacks). Order: embargo first, then purge.
- Deflated Sharpe Ratio (Bailey & López de Prado 2014, verbatim): DSR = Φ( (SR*−SR₀)·√(T−1) / √(1 − γ̂₃·SR₀ + ((γ̂₄−1)/4)·SR₀²) ), where SR* = observed (non-annualized) Sharpe, T = number of return observations, γ̂₃ = skew, γ̂₄ = kurtosis. The benchmark SR₀ = √V[SR_n] · ( (1−γ)·Φ⁻¹[1−1/N] + γ·Φ⁻¹[1−1/(N·e)] ), with N = number of (effective, independent) trials, V[SR_n] = cross-sectional variance of the trial Sharpes, γ ≈ 0.5772 (Euler-Mascheroni), e ≈ 2.718. So DSR explicitly deflates the observed Sharpe for (a) how many strategies you tried, (b) how spread-out the trial Sharpes are, (c) skew/fat-tails, and (d) sample length T.
- The minimum-count answer is the Minimum Track Record Length (MinTRL), verbatim: MinTRL = 1 + (1 − γ̂₃·SR* + ((γ̂₄−1)/4)·SR²) · ( Φ⁻¹(confidence) / (SR−SR₀) )². This is the minimum number of return observations T required before a Sharpe is statistically distinguishable from SR₀ at a chosen confidence. It blows up as SR* approaches SR₀ (a barely-above-benchmark strategy needs an enormous track record) and inflates with fat tails / negative skew.
- N is effective, not literal (DSR Wikipedia + parent brief): "N is the effective number of independent trials … not always equal to the literal count of backtests if many are highly correlated." A dense parameter grid of correlated cells inflates literal N but adds little independent information, so naive DSR under-deflates. CPCV's near-independent paths are a cleaner basis for estimating V[SR_n] and N than a correlated grid.
Convergences and contradictions
- Strong convergence (web + vault): CPCV = purge + embargo + combinatorial test-group selection; its product is a distribution of OOS paths feeding DSR/PBO. The architecture doc's GATE-1/GATE-2 design is exactly the de Prado stack; this brief just supplies the formulas and the φ[N,k] path math the doc gestured at.
- Convergence on the "effective N" caveat: both the DSR literature and the architecture doc's red-team independently flag that correlated grid cells make effective N < counted N. CPCV is the mitigation the vault didn't fully name — its paths are a more honest N than grid-cell count.
- Apparent contradiction — "trade count" vs "observation count": the founder's question says "minimum trade count," but DSR/MinTRL are defined over T = number of return observations (e.g. daily bars), not number of round-trip trades. For a position/capital-cycle strategy these diverge by orders of magnitude: ~5-6 trades but hundreds-to-thousands of daily returns. This is the crux and is resolved in Synthesis: DSR can be computed on the return series even with few trades, but it is meaningful only if the returns are reasonably iid-ish and the edge isn't carried by one or two cycle-trades — which for RDCO's horizon it usually is. So the honest answer is not a single number; see below.
- No real contradiction in the sources — the quant-blog tier and the de Prado primary tier agree on mechanics. The only divergence is presentation depth (quantinsti is conceptual; Wikipedia/AFML give the formulas).
Synthesis for RDCO
1. Concrete validation.py implementation sketch (Phase-2 build). The current module exposes only timeseries_cv(n_splits) → sklearn TimeSeriesSplit and a BiasAudit dataclass. CPCV is a net-new addition that should sit alongside (not replace) the walk-forward path, since the architecture wants both walk-forward and the combinatorial holdout distribution. Proposed signatures:
# autoinv/validation.py (Phase-2 additions)
@dataclass
class CPCVSplit:
train_idx: np.ndarray # post-purge, post-embargo training rows
test_idx: np.ndarray # the k test-group rows
test_groups: tuple[int, ...] # which of the N groups are test here
path_id: int # which reassembled backtest path this feeds
def combinatorial_purged_cv(
n_obs: int,
label_spans: np.ndarray, # shape (n_obs, 2): [event_start, event_end] per obs
n_groups: int = 6, # N
k_test: int = 2, # k -> C(6,2)=15 splits, phi=5 paths
embargo_frac: float = 0.01, # h: fraction of n_obs embargoed after each test block
) -> list[CPCVSplit]:
# 1. partition [0..n_obs) into N contiguous, time-ordered groups
# 2. for each of C(N,k) combinations of test groups:
# a. test_idx = union of those k groups' rows
# b. embargo: drop the next ceil(embargo_frac * n_obs) rows after each test block
# c. purge: drop any train row whose label_span overlaps any test row's span
# d. assign path_id round-robin so phi[N,k]=(k/N)*C(N,k) paths reassemble
...
def n_paths(n_groups: int, k_test: int) -> int:
return (k_test * math.comb(n_groups, k_test)) // n_groups # phi[N,k]
def deflated_sharpe(
returns: np.ndarray, # the realized return series (T observations)
n_trials: int, # effective N (cumulative trial counter, not grid-cell count)
trial_sharpe_var: float, # V[SR_n] across trials -> from the CPCV path Sharpes
benchmark_sr: float = 0.0,
) -> DSRResult: # returns observed SR, SR0, DSR, and MinTRL
# SR0 = sqrt(trial_sharpe_var) * ((1-EM)*Phi_inv(1-1/N) + EM*Phi_inv(1-1/(N*e)))
# DSR = Phi( (SR_obs - SR0)*sqrt(T-1) / sqrt(1 - skew*SR_obs + (kurt-1)/4 * SR_obs**2) )
# MinTRL = 1 + (1 - skew*SR_obs + (kurt-1)/4*SR_obs**2) * (Phi_inv(conf)/(SR_obs-SR0))**2
...
Reuse what exists: metrics.sharpe_ratio for per-path Sharpes, stats.normality_report to source skew/kurtosis honestly (it already computes both), and feed the CPCV per-path Sharpes as the empirical estimate of trial_sharpe_var. PBO/CSCV is a sibling function (probability_of_backtest_overfitting) over the same path matrix. Keep the existing shuffle=True refusal rule; CPCV is the correct multi-path replacement people reach KFold for. Pair each function with a test the way Phase-2's V-model demands: a pure-noise strategy through a C(6,2) CPCV + DSR must be REJECTED (DSR low, MinTRL exceeding available T).
2. The minimum-trade-count answer (and why it's the wrong tool for RDCO's horizon). DSR is defined over T = number of return observations, and the honest gate is MinTRL: report a DSR only if your available T exceeds the MinTRL implied by your observed Sharpe, skew, kurtosis and benchmark. As a rule of thumb, a strategy whose edge sits near the benchmark needs thousands of daily observations; a clearly-separated edge can qualify in low-hundreds. There is no universal "N trades" cutoff — MinTRL is the principled cutoff and it depends on effect size. For a practical floor, marry it to the existing vault rule: <10 trades → no decimals on Sharpe and no DSR claim at all; 10-50 trades → DSR reportable only with its MinTRL and a wide bootstrap CI; 50+ → DSR as a gate. Below ~30 independent observations the skew/kurtosis estimates feeding the deflation term are themselves too noisy to trust, so the deflation is theater.
The capital-cycle caveat is decisive. RDCO is a position/capital-cycle investor: the chip-fab/memory thesis produces ~5-6 trades across ~40 years and the realized P&L is carried by one or two cycle-bottom entries. On that surface, neither CPCV nor deflated-Sharpe is the right instrument — CPCV needs many near-independent blocks (a thin 5-event history can't supply non-degenerate C(N,k) paths; purging eats most of the training set), and DSR over a handful of trade-returns is dominated by estimation noise in skew/kurtosis. This matches the architecture doc's own ruling: route CPCV/DSR/PBO at the large-sample liquid surfaces (cross-sectional equity momentum, the SPY vol-regime overlay) where C(6,2)=15 splits and 5 paths are well-populated, and keep the rare-state cyclical theses on the v2 multi-cycle harness — leave-one-out by stress episode, bootstrap CIs, dual buy-and-hold benchmarks, integer-Sharpe precision — NOT on DSR.
What to report instead for the capital-cycle book: per-cycle return distribution (not a mean), leave-one-out-by-episode (if dropping the AI-boom cycle flips the verdict, that's the headline), bootstrap CIs on aggregate return, both buy-and-hold benchmarks, and an explicit trade count with integer-precision Sharpe. The single honest number is "we have 5 cycles; here is each one and here is the leave-one-out," not a deflated-Sharpe to two decimals. DSR belongs on the swarm; the cycle thesis belongs on the v2 harness. Wiring DSR to a 5-trade thesis would be exactly the false-rigor the harness was built to forbid.
Open follow-ups
- Read the AFML primary (de Prado, Advances in Financial Machine Learning, ch. 7 purged CV + ch. 12 CPCV) and Bailey/de Prado SSRN 2460551 to confirm the embargo-fraction default and the exact PBO/CSCV partition count (the blogs assert "lower PBO / higher DSR" for CPCV but I did not verify magnitudes against the primary). [parent brief open-follow-up #1 still open]
- Decide the effective-N estimator. Counted grid-cell N under-deflates (correlated cells); CPCV path Sharpes give a cleaner V[SR_n]. Pre-register whether GATE-1's N comes from the cumulative trial counter, the CPCV path count, or a correlation-adjusted effective-N — this changes every DSR the pipeline ever reports.
- Set the MinTRL guard as a hard gate, not a report line. Wire
validation.pyto REFUSE to emit a DSR when available T < MinTRL(observed SR, skew, kurt, benchmark, confidence), returning "insufficient track record — report distribution instead." This operationalizes the founder's honest-stats rule mechanically. - Pick N and k for the swarm surfaces. N=6,k=2 (15 splits / 5 paths) is the canonical default; confirm it against the available train/validation span length per universe so each of the N groups still holds enough bars after purge+embargo.
Sources
Vault:
~/rdco-vault/06-reference/research/2026-05-31-ensembles-systematic-trading-overfitting.md— parent brief; purge/embargo/CPCV definitions, E[max SR] false-strategy framing, effective-N caveat~/rdco-vault/01-projects/investing/2026-05-29-strategy-pipeline-architecture-v0.md— GATE-1 DSR / GATE-2 PBO-via-CSCV design, DSR<0.5/>1.0 thresholds, "effective N < counted N," route-swarm-at-large-sample ruling, sealed-holdout discipline~/rdco-vault/01-projects/investing/plugin/skills/backtest-thesis/skill.md(/investing:backtest-thesisv2) — trade-count-aware Sharpe precision (<10/10-50/50+), leave-one-out + bootstrap-CI honest-stats rules~/rdco-vault/01-projects/automated-investing/index.md— repo pointer (Phase-2 gate deferred)- Source code verified 2026-06-01:
~/Projects/automated-investing/autoinv/validation.py(TimeSeriesSplit + BiasAudit only — no CPCV/DSR),metrics.py,stats.py(skew/kurtosis available via normality_report; no DSR)
Web:
- Combinatorial Purged Cross-Validation — https://en.wikipedia.org/wiki/Purged_cross-validation (φ[N,k]=(k/N)·C(N,k); N=6,k=2 → 15 splits / 5 paths; purge + embargo definitions)
- Deflated Sharpe Ratio — https://en.wikipedia.org/wiki/Deflated_Sharpe_ratio (DSR formula, SR₀ benchmark, MinTRL formula, effective-N caveat, False-Strategy Theorem)
- The Deflated Sharpe Ratio (Bailey & López de Prado 2014) — https://www.davidhbailey.com/dhbpapers/deflated-sharpe.pdf and https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2460551 (primary, cited not deep-read)
- Cross Validation in Finance: Purging, Embargoing, Combinatorial — https://blog.quantinsti.com/cross-validation-embargo-purging-combinatorial/ (conceptual CPCV/purge/embargo, embargo-first-then-purge order; points to AFML + timeseriescv for formulas)
- The Combinatorial Purged Cross-Validation method — https://towardsai.net/p/l/the-combinatorial-purged-cross-validation-method (N groups, C(N,k) test combinations, distribution-of-paths benefit)