Combinatorial Purged Cross-Validation: der CV-Standard für Zeitreihen-Backtests.
Klassische k-fold Cross-Validation ist bei Zeitreihen eine Einladung zum Look-Ahead-Bias. Walk-Forward liefert genau einen Pfad — zu wenig für ernste Inferenz. Combinatorial Purged CV löst beides: viele Pfade, sauber gegen Leakage abgeschirmt. Für Strategien mit ML-Komponente ist es heute mein Default.
Wenn ich Backtest-Code aus Mandanten-Projekten öffne, finde ich oft sklearn.model_selection.KFold
mit shuffle=True auf Renditen-Daten. Das ist methodisch ein Totalschaden: die Folds enthalten
beliebig viele Zukunfts-Beobachtungen im Training. Out-of-Sample-Sharpe 2,4 — und drei Wochen nach Live-Schaltung
wundert sich der PM, warum die Equity fällt.
Warum klassische k-fold CV bei Zeitreihen scheitert.
Cross-Validation in der klassischen Form unterstellt i.i.d. (independent, identically distributed). Bei Marktdaten gilt davon kein Wort. Drei konkrete Bruchstellen:
- Look-Ahead durch Shuffle: Wer Daten mischt, trainiert auf Werten, die zeitlich nach den Test-Werten liegen. Das Modell „weiß“ implizit die Zukunft.
- Label-Overlap: Ein Label auf Tag t basiert oft auf Returns über die nächsten n Tage. Liegt das Train-Label auf t und das Test-Label auf t+2, teilen sie reale Marktinformation.
- Serial Correlation: Features wie Volatilität, Volumen, Spread sind hoch autokorreliert. Train und Test in Nachbarschaft sind faktisch dieselbe Beobachtung.
Marcos Lopez de Prado hat das in Advances in Financial Machine Learning (2018) systematisch ausgearbeitet. Sein Befund: die meisten publizierten ML-Trading-Strategien sind Overfitting-Artefakte schlechter CV-Designs.
Walk-Forward ist nicht genug.
Walk-Forward löst das Look-Ahead-Problem sauber: man trainiert auf der Vergangenheit, testet auf der Zukunft, rollt vorwärts. Aber: bei einer 10-Jahres-Historie mit 2 Jahren In-Sample und 6 Monaten Out-of-Sample bekomme ich 16 OOS-Fenster — und das ist ein Equity-Pfad. Auf Basis eines einzigen Pfads sind Konfidenzintervalle um Sharpe oder Drawdown breit. Die Streuung der echten Strategie über mögliche Realisierungen kenne ich nicht.
Was ich für statistische Inferenz brauche: viele plausible Pfade, jeder davon ohne Leakage. Genau das liefert CPCV.
Wie CPCV funktioniert.
Die Idee ist simpel und elegant. Statt die Historie in einen Train- und einen Test-Block zu teilen, partitioniert CPCV die Daten in N gleich große, zeitlich geordnete Gruppen. Aus diesen N Gruppen werden alle Kombinationen von k Gruppen als Test-Set verwendet — der Rest ist Training.
N = 10 Gruppen k = 2 Test-Gruppen pro Split Anzahl Splits = C(10, 2) = 45 Anzahl Backtest-Pfade pro Strategie = 9 Jede Gruppe taucht in mehreren Test-Sets auf, nie aber gleichzeitig in Train und Test desselben Splits.
Aus den 45 Splits lassen sich 9 disjunkte Backtest-Pfade zusammensetzen — jeder Pfad nutzt jede Gruppe genau einmal als Test. Sie erhalten also nicht eine Equity-Kurve, sondern neun. Daraus lässt sich eine ehrliche Verteilung der Performance ableiten.
Purging und Embargoing: die Leakage-Schutzschicht.
Die Trennung in Train- und Test-Gruppen reicht nicht. Zwei Mechanismen sichern CPCV gegen Information Leakage:
Purging
Beispiel: ein Label auf Tag t ist „Return über die nächsten 5 Tage“. Liegt t kurz vor einer Test-Gruppe, überlappt das Label mit dem Test-Bereich. Purging entfernt aus dem Trainings-Set jede Beobachtung, deren Label sich mit dem Test-Set zeitlich überschneidet. Die Lücke ist genau so groß wie der Label-Horizont.
Embargoing
Nach dem Test-Set wird zusätzlich eine kleine Pufferzone (oft 1–2 % der Datenpunkte) ausgeschlossen. Grund: serielle Korrelation in Features. Eine Volatilität direkt nach dem Test-Set ist statistisch fast identisch mit der Volatilität am Ende des Test-Sets — das wäre verdeckter Test-Leak.
Beide Mechanismen kosten Datenpunkte. Bei einer typischen Daily-Strategie mit 10 Jahren Historie und 5-Tage-Labels bleiben nach Purge/Embargo etwa 95 % der Daten nutzbar — das ist akzeptabel.
Python-Implementation.
Es gibt zwei Wege: mlfinlab (kommerziell, robust) oder eine eigene Implementation. Für Kunden-Code
nehme ich meist die eigene — leichter zu auditieren und Lizenz-frei.
# Vereinfachte CPCV-Implementation import numpy as np import pandas as pd from itertools import combinations def cpcv_splits(n_samples, n_groups=10, k_test=2, label_horizon=5, embargo_pct=0.01): group_size = n_samples // n_groups groups = [(i * group_size, (i + 1) * group_size) for i in range(n_groups)] embargo = int(n_samples * embargo_pct) for test_combo in combinations(range(n_groups), k_test): test_idx = [] for g in test_combo: test_idx.extend(range(groups[g][0], groups[g][1])) test_idx = sorted(test_idx) # Purging + Embargo purged = set() for t in test_idx: purged.update(range(t - label_horizon, t + embargo + 1)) train_idx = [i for i in range(n_samples) if i not in set(test_idx) and i not in purged] yield np.array(train_idx), np.array(test_idx), test_combo def cpcv_backtest(X, y, prices, model_fn, n_groups=10, k_test=2): paths = {} for tr_idx, te_idx, combo in cpcv_splits(len(X), n_groups, k_test): model = model_fn() model.fit(X.iloc[tr_idx], y.iloc[tr_idx]) preds = model.predict(X.iloc[te_idx]) for g in combo: paths.setdefault(g, []).append((te_idx, preds)) return paths
Aus dem paths-Dict konstruieren Sie die einzelnen Equity-Kurven, indem Sie für jeden Pfad eine
disjunkte Auswahl der Test-Splits zusammensetzen.
Praxisbeispiel: Mean-Reversion auf S&P 500-Konstituenten.
Ein typischer Fall aus einem Mandanten-Projekt 2031. Mean-Reversion-Signal: 5-Tage-Z-Score des Returns, Long bei Z < -1,5, Short bei Z > 1,5, Halten 3 Tage. ML-Komponente: ein Gradient Boosting filtert Signale anhand von Volatilitäts-Regime, Sektor-Beta und Liquidität.
Walk-Forward auf 8 Jahren Historie ergab Sharpe 1,82. Sah gut aus. CPCV mit N=10, k=2, 9 Pfaden:
- Median Sharpe über Pfade: 1,21
- 5 %-Quantil: 0,38
- 95 %-Quantil: 1,71
- Maximum Drawdown: Median 14 %, 95 %-Quantil 23 %
Der Walk-Forward-Sharpe von 1,82 lag über dem 95 %-Quantil der CPCV-Verteilung — er war ein optimistischer Ausreißer, kein typischer Pfad. Die ehrliche Erwartung für Live-Trading lag bei Sharpe ~1,2, nicht ~1,8. Das hat die Risiko-Budgetierung des Mandanten massiv verändert.
Wann CPCV nicht passt.
- Sehr kurze Historien: bei < 3 Jahren Daten und Daily-Bars wird N=10 dünn. Dann lieber Walk-Forward mit großzügigerem OOS.
- Strukturbrüche im Markt: CPCV mischt Test-Gruppen aus verschiedenen Regimes. Wer explizit Regime-spezifisch testen will, braucht zusätzliche Schichten.
- Sehr langer Label-Horizont: bei 60-Tage-Labels werden Purge-Zonen so groß, dass Train-Set schrumpft. Hier hilft kein CPCV, sondern Label-Re-Design (Triple-Barrier-Methode).
Meine Praxis.
Für jede Strategie mit ML-Komponente — sei es Filter, Position-Sizing oder Signal-Generierung — läuft bei mir CPCV. Walk-Forward bleibt als Sanity-Check und für die finale Equity-Visualisierung. Aber die Entscheidung „live schalten oder nicht“ fällt auf der CPCV-Verteilung. Konkret: ich verlange ein 5 %-Quantil des Sharpe über 0,7 und ein Drawdown-95 %-Quantil unter dem Risikobudget des Mandanten. Erst dann darf die Strategie in Paper-Trading wechseln.
CPCV ist kein magischer Garant gegen Overfitting — aber es ist die einzige Methode, die ich kenne, mit der ich verteidigen kann, dass eine ML-Strategie über mehr als einen Zufallspfad funktioniert hat.
Sie wollen ein ML-basiertes Trading-Modell sauber validieren — ohne Selbsttäuschung? Erstgespräch buchen — wir bauen das CPCV-Setup gemeinsam auf.