L'apprentissage automatique sur les rendements
L'apprentissage supervisé ajuste une fonction des variables X vers une cible y. En finance, la cible est typiquement un rendement futur et les variables sont des choses connues avant : rendements passés, volatilité, volumes. scikit-learn offre une interface uniforme : chaque modèle a fit(X, y) et predict(X). Une différence clé avec les problèmes de manuel est le rapport signal/bruit : les rendements sont presque du pur bruit, donc même un bon modèle n'en explique qu'une infime part, et une mise en place négligente trouve des régularités qui n'existent pas.
import numpy as np
returns = np.array([0.01, -0.02, 0.015, 0.005, -0.01, 0.02])
n_lags = 2
# Row i holds the two returns before the target; y[i] is the return that follows them
windows = np.lib.stride_tricks.sliding_window_view(returns, n_lags)
X, y = windows[:-1], returns[n_lags:]
print(X)
print(y) # the targets are one step ahead of the featuresDes variables à partir des retards
Les variables les plus simples d'un rendement sont les rendements qui le précèdent. Construisez une matrice de retards : la ligne i contient returns[i : i + n_lags], et la cible est returns[i + n_lags], la valeur suivante. Une vue en fenêtre glissante la donne sans boucle. Tout ce qui figure dans une ligne doit être connu au moment de la prévision, c'est la règle d'anticipation appliquée à un modèle : une variable décalée d'une position dans le mauvais sens laisserait le modèle voir la réponse.
import numpy as np
from sklearn.linear_model import Ridge
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
# Pipeline: scale each feature, then a ridge regression (a linear model with a penalty on large coefficients)
pipe = Pipeline([("scale", StandardScaler()), ("model", Ridge(alpha=10.0))])
print([name for name, _ in pipe.steps])
rng = np.random.default_rng(1)
returns = np.zeros(400)
noise = rng.normal(0, 0.01, size=400)
for t in range(1, 400):
returns[t] = 0.3 * returns[t - 1] + noise[t] # a series with a little memory
windows = np.lib.stride_tricks.sliding_window_view(returns, 3)
X, y = windows[:-1], returns[3:]
pipe.fit(X[:300], y[:300]) # fit on the first 300 rows
print(np.round(pipe.named_steps["model"].coef_, 4)) # the last lag weighs mostPipelines et fuite d'information
Un Pipeline regroupe le normalisateur et le modèle de sorte que fit apprend les statistiques de mise à l'échelle (moyenne et écart-type de chaque variable) sur les seules lignes d'entraînement et que predict les applique telles quelles aux nouvelles lignes. Normaliser tout le jeu de données avant de le découper ferait fuir moyenne et dispersion de la période de test dans l'entraînement : le même biais d'anticipation, à un nouvel endroit. Dans une série temporelle, le découpage est aussi chronologique : entraîner sur les premières lignes, tester sur les suivantes, jamais un tirage aléatoire.
import numpy as np
from sklearn.linear_model import Ridge
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
def build_pipeline(alpha):
return Pipeline([("scale", StandardScaler()), ("model", Ridge(alpha=alpha))])
rng = np.random.default_rng(1)
returns = np.zeros(600)
noise = rng.normal(0, 0.01, size=600)
for t in range(1, 600):
returns[t] = 0.3 * returns[t - 1] + noise[t]
windows = np.lib.stride_tricks.sliding_window_view(returns, 3)
X, y = windows[:-1], returns[3:]
model = build_pipeline(10.0).fit(X[:400], y[:400]) # chronological split: early rows to train
pred = model.predict(X[400:]) # later rows to test
print(round(float(np.mean(np.sign(y[400:]) == np.sign(pred))), 3)) # direction accuracy on unseen dataJuger le résultat honnêtement
Pour un signal de trading, la direction compte souvent le plus, donc un score simple est la précision directionnelle : la part des jours où le rendement prévu et le rendement réel ont le même signe. 50 % équivaut à pile ou face. Les vrais prédicteurs de rendements atteignent 51 à 53 %, et 60 % sur des données de test est plus probablement une fuite qu'une découverte. Comparez aux références triviales (toujours prévoir à la hausse, prévoir zéro) avant de croire quoi que ce soit.
import numpy as np
y_true = np.array([0.01, -0.02, 0.015, -0.005, 0.02, -0.01])
y_pred = np.array([0.004, 0.001, 0.002, -0.001, 0.003, 0.002]) # the model is right on 4 of 6 signs
accuracy = float(np.mean(np.sign(y_true) == np.sign(y_pred)))
always_up = float(np.mean(np.sign(y_true) == 1)) # the trivial baseline: always predict a rise
print(round(accuracy, 3), round(always_up, 3)) # compare the model with the baselinealpha est un réglage à ajuster, mais sur une période de validation, jamais sur la période de test. Les sujets suivants construisent les variables et le test glissant (walk-forward) qui rendent cela honnête.- Une matrice de retards (
sliding_window_view) donne des variables à partir des rendements précédant chaque cible ; tout ce qui est dans une ligne doit être connu au moment de la prévision. - Un
Pipeline([("scale", StandardScaler()), ("model", Ridge(alpha=alpha))])apprend la mise à l'échelle sur les seules lignes d'entraînement ; découpez chronologiquement, jamais au hasard. - Notez la direction (part de signes égaux) face à des références triviales ; 51 à 53 % est réaliste, 60 % suggère une fuite ; les modèles linéaires régularisés conviennent aux données bruitées.
À suivre : l'ingénierie de variables pour les données financières, construire de meilleures entrées que les rendements retardés bruts.
Lab : ce qui est noté
- Transformer une série en variables avec
lag_matrix25 pts - Enchaîner les étapes avec
build_pipeline25 pts - Entraîner sur le passé, prévoir la suite avec
fit_predict25 pts - Noter les directions avec
direction_accuracy25 pts