So testen, wie man gehandelt hätte
Eine zufällige Aufteilung in Trainings- und Testzeilen wie im üblichen maschinellen Lernen scheitert bei Zeitreihen: Das Training enthält Tage nach den Testtagen, und benachbarte Tage sind korreliert, der Test ist also teils eine Kopie des Trainings. Die Walk-forward-Validierung respektiert die Zeit. Man trainiert auf einem Block der Vergangenheit, testet auf dem direkt folgenden Block, schiebt das Fenster weiter und wiederholt. Jede Vorhersage stammt von einem Modell, das die Zukunft nie gesehen hat, genau wie im Live-Handel.
import numpy as np
n, train_size, test_size = 10, 4, 2
start = 0
while start + train_size + test_size <= n:
train = np.arange(start, start + train_size)
test = np.arange(start + train_size, start + train_size + test_size)
print("train", train.tolist(), "test", test.tolist())
start += test_size # slide forward by one test blockRollierend oder wachsend, und der Embargo
Zwei Varianten. Ein rollierendes Fenster hält die Trainingslänge fest und wirft die ältesten Zeilen ab: Es passt sich Regimewechseln an und kostet gleichbleibend. Ein wachsendes Fenster beginnt immer bei Zeile 0, das Training wächst also: Es nutzt jede Beobachtung, aber alte Daten können veraltet sein. Eine dritte Zutat ist die Lücke (der Embargo): Schaut ein Merkmal oder das Ziel h Tage zurück oder voraus, teilen Zeilen kurz vor dem Testzeitraum Information mit ihm, man überspringt daher einige Zeilen zwischen Training und Test.
def walk_forward_splits(n, train_size, test_size, gap=0):
splits, start = [], 0
while start + train_size + gap + test_size <= n:
train = list(range(start, start + train_size))
test = list(range(start + train_size + gap, start + train_size + gap + test_size))
splits.append((train, test))
start += test_size
return splits
for train, test in walk_forward_splits(12, 5, 2, gap=1):
print(train, "->", test) # the row between them is skippedJeden Fold bewerten
Für jeden Fold passt man auf den Trainingszeilen an und bewertet auf den Testzeilen, erhält also je Fold einen Fehler statt einer einzigen Zahl: Ihre Streuung zeigt, wie stabil das Modell über die Zeit ist. Eine nützliche Basislinie ist die triviale Prognose, hier der Trainingsmittelwert für jeden Testtag. Geben Sie die Fehler Fold für Fold aus: Ein im Schnitt gutes, aber in einem Regime schreckliches Modell ist nicht dasselbe wie ein durchgehend ordentliches.
import numpy as np
def walk_forward_splits(n, train_size, test_size):
out, start = [], 0
while start + train_size + test_size <= n:
out.append((np.arange(start, start + train_size), np.arange(start + train_size, start + train_size + test_size)))
start += test_size
return out
rng = np.random.default_rng(2)
returns = rng.normal(0.0004, 0.01, size=300)
errors = []
for train, test in walk_forward_splits(len(returns), 100, 50):
errors.append(np.mean((returns[test] - returns[train].mean()) ** 2))
print(np.round(np.array(errors) * 1e4, 3)) # squared error in basis points squared, fold by foldEin Leck aufspüren
Eine Aufteilung ist nur sauber, wenn jede Trainingszeile strikt vor jeder Testzeile liegt. Die Prüfung train.max() < test.min() braucht eine Zeile und fängt den häufigsten Fehler: einen unsortierten oder gemischten Index, einen um eins verschobenen Slice, eine Aufteilung aus einer Bibliotheksfunktion, die standardmäßig mischt. Setzen Sie diese Prüfung in den Code, der die Folds baut, damit ein Leck den Lauf stoppt, statt ein schmeichelhaftes Ergebnis zu liefern.
import numpy as np
def leaks_future(train_idx, test_idx):
return bool(np.max(train_idx) >= np.min(test_idx))
train = np.arange(0, 100)
print(leaks_future(train, np.arange(100, 120))) # False: clean
shuffled = np.random.default_rng(1).permutation(120) # what a shuffling splitter does
print(leaks_future(shuffled[:100], shuffled[100:])) # True: training rows after test rows- Walk-forward: auf einem Block der Vergangenheit trainieren, auf dem nächsten testen, weiterschieben; jede Testzeile liegt nach all ihren Trainingszeilen.
- Rollierende Fenster halten die Trainingsgröße fest; wachsende beginnen bei Zeile 0; eine Lücke (Embargo) trennt Training und Test, wenn Merkmale mehrere Tage umfassen.
- Bewerten Sie jeden Fold einzeln und betrachten Sie die Streuung; prüfen Sie
train.max() < test.min(); stimmen Sie nie auf den berichteten Folds ab.
Als Nächstes: das Limit-Orderbuch, die Datenstruktur, in der Preise tatsächlich entstehen.
Lab: Was bewertet wird
- Mit
walk_forward_splitsein Fenster durch die Zeit schieben25 Pkt. - Mit
expanding_splitsdie ganze Historie behalten25 Pkt. - Mit
forecast_mseeine Prognose Fold für Fold testen25 Pkt. - Mit
leaks_futureein Leck aufspüren25 Pkt.