Tester comme on aurait négocié
Un découpage aléatoire en lignes d'entraînement et de test, comme en apprentissage automatique ordinaire, échoue sur les séries temporelles : l'entraînement contient des jours postérieurs aux jours de test, et les jours voisins sont corrélés, si bien que le test est en partie une copie de l'entraînement. La validation walk-forward respecte le temps. On entraîne sur un bloc du passé, on teste sur le bloc qui le suit, on fait glisser la fenêtre et on recommence. Chaque prévision vient d'un modèle qui n'a jamais vu le futur, exactement comme en trading réel.
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 blockGlissant ou croissant, et l'embargo
Deux variantes. Une fenêtre glissante garde la longueur d'entraînement fixe en écartant les plus anciennes lignes : elle s'adapte aux changements de régime à coût constant. Une fenêtre croissante part toujours de la ligne 0, donc l'entraînement grossit : elle utilise toutes les observations, mais les vieilles données peuvent être périmées. Un troisième ingrédient est l'écart (ou embargo) : si une variable ou la cible regarde h jours en arrière ou en avant, les lignes juste avant la période de test partagent de l'information avec elle ; on saute donc quelques lignes entre entraînement et 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 skippedNoter chaque pli
Pour chaque pli, on ajuste sur les lignes d'entraînement et on note sur les lignes de test, ce qui donne une erreur par pli plutôt qu'un seul nombre : leur dispersion dit la stabilité du modèle dans le temps. Une référence utile est la prévision triviale, ici la moyenne d'entraînement pour chaque jour de test. Affichez les erreurs pli par pli : un modèle bon en moyenne mais terrible dans un régime n'est pas équivalent à un modèle régulièrement correct.
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 foldDétecter une fuite
Un découpage n'est propre que si chaque ligne d'entraînement précède strictement chaque ligne de test. Vérifier train.max() < test.min() tient en une ligne et attrape le bug le plus courant : un index non trié ou mélangé, une tranche décalée d'un cran, un découpage issu d'une fonction de bibliothèque qui mélange par défaut. Placez cette vérification dans le code qui construit les plis, pour qu'une fuite arrête l'exécution au lieu de produire un résultat flatteur.
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 : entraîner sur un bloc du passé, tester sur le bloc suivant, faire glisser ; chaque ligne de test vient après toutes ses lignes d'entraînement.
- Les fenêtres glissantes gardent la taille d'entraînement fixe ; les fenêtres croissantes partent de la ligne 0 ; un écart (embargo) sépare entraînement et test quand les variables couvrent plusieurs jours.
- Notez chaque pli séparément et regardez la dispersion ; vérifiez
train.max() < test.min(); n'ajustez jamais sur les plis que vous rapportez.
À suivre : le carnet d'ordres, la structure de données où les prix se forment réellement.
Lab : ce qui est noté
- Faire glisser une fenêtre dans le temps avec
walk_forward_splits25 pts - Garder tout l'historique avec
expanding_splits25 pts - Tester une prévision pli par pli avec
forecast_mse25 pts - Détecter une fuite avec
leaks_future25 pts