Maschinelles Lernen auf Renditen
Überwachtes Lernen passt eine Funktion von Merkmalen X an ein Ziel y an. In der Finanzwelt ist das Ziel meist eine künftige Rendite, und die Merkmale sind vorher Bekanntes: vergangene Renditen, Volatilität, Volumen. scikit-learn bietet eine einheitliche Schnittstelle: Jedes Modell hat fit(X, y) und predict(X). Ein wesentlicher Unterschied zu Lehrbuchproblemen ist das Signal-Rausch-Verhältnis: Renditen sind fast reines Rauschen, selbst ein gutes Modell erklärt nur einen Bruchteil, und ein nachlässiger Aufbau findet Muster, die es nicht gibt.
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 featuresMerkmale aus Lags
Die einfachsten Merkmale einer Rendite sind die Renditen davor. Bauen Sie eine Lag-Matrix: Zeile i enthält returns[i : i + n_lags], das Ziel ist returns[i + n_lags], der nächste Wert. Eine Sliding-Window-Ansicht liefert sie ohne Schleife. Alles in einer Zeile muss zum Prognosezeitpunkt bekannt sein, die Look-ahead-Regel auf ein Modell angewandt: Ein um eine Position falsch verschobenes Merkmal ließe das Modell die Antwort sehen.
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 und Datenleck
Eine Pipeline bündelt Skalierer und Modell, sodass fit die Skalierungsstatistiken (Mittel und Standardabweichung jedes Merkmals) nur aus den Trainingszeilen lernt und predict sie unverändert auf neue Zeilen anwendet. Würde man erst den ganzen Datensatz skalieren und dann teilen, flössen Mittel und Streuung der Testperiode ins Training: derselbe Look-ahead-Bias an neuer Stelle. Bei Zeitreihen ist die Teilung außerdem chronologisch: auf frühen Zeilen trainieren, auf späteren testen, nie zufällig mischen.
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 dataDas Ergebnis ehrlich beurteilen
Bei einem Handelssignal zählt oft die Richtung, ein einfaches Maß ist daher die Richtungstrefferquote: der Anteil der Tage, an denen vorhergesagte und tatsächliche Rendite dasselbe Vorzeichen haben. 50 % ist Münzwurf. Echte Renditeprädiktoren erreichen 51 bis 53 %, und 60 % auf Testdaten sind eher ein Leck als eine Entdeckung. Vergleichen Sie mit den trivialen Basislinien (immer steigend vorhersagen, null vorhersagen), bevor Sie etwas glauben.
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 ist ein Regler zum Einstellen, aber auf einem Validierungszeitraum, nie auf dem Testzeitraum. Die nächsten Themen bauen die Merkmale und das Walk-forward-Testen, die das ehrlich machen.- Eine Lag-Matrix (
sliding_window_view) liefert Merkmale aus den Renditen vor jedem Ziel; alles in einer Zeile muss zum Prognosezeitpunkt bekannt sein. - Eine
Pipeline([("scale", StandardScaler()), ("model", Ridge(alpha=alpha))])lernt die Skalierung nur auf den Trainingszeilen; chronologisch teilen, nie zufällig. - Bewerten Sie die Richtung (Anteil gleicher Vorzeichen) gegen triviale Basislinien; 51 bis 53 % sind realistisch, 60 % deuten auf ein Leck; regularisierte lineare Modelle passen zu verrauschten Daten.
Als Nächstes: Feature Engineering für Finanzdaten, bessere Eingaben als rohe verzögerte Renditen bauen.
Lab: Was bewertet wird
- Mit
lag_matrixeine Reihe in Merkmale umwandeln25 Pkt. - Mit
build_pipelinedie Schritte verketten25 Pkt. - Mit
fit_predictan der Vergangenheit trainieren, den Rest vorhersagen25 Pkt. - Mit
direction_accuracydie Richtungen bewerten25 Pkt.