← Alle Insights

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:

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:

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.

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.