Zu viel wissen
Look-ahead-Bias entsteht, wenn ein Backtest Informationen nutzt, die zum Entscheidungszeitpunkt nicht verfügbar waren. Er ist der häufigste Grund, warum eine Strategie auf dem Papier glänzt und live Geld verliert. Selten ist es eine einzelne offensichtliche Zeile; er versteckt sich in der Ausrichtung (ein Signal vom heutigen Schluss, gehandelt zu diesem Schluss), in über die ganze Stichprobe berechneten Statistiken, in später revidierten Daten und in rückblickend gewählten Indizes. Das Gegenmittel ist eine Gewohnheit: Frage bei jeder Zahl einer Entscheidung, wann sie bekannt wurde.
import numpy as np
rng = np.random.default_rng(5)
returns = rng.normal(0, 0.01, size=250) # pure noise: no strategy can really make money here
signal = returns.copy() # "signal" = the return of the same day
position = np.sign(signal)
biased = np.prod(1 + position * returns) - 1 # trades on the return it is about to earn
honest_position = np.concatenate([[0.0], position[:-1]])
honest = np.prod(1 + honest_position * returns) - 1
print(round(float(biased), 2), round(float(honest), 2)) # 6.0 0.01: a 600% "return" on noise, against 1%Ausrichtung: die Ein-Tages-Verzögerung
Das Mittel gegen Ausrichtungs-Bias ist eine Verzögerung: Ein an Tag t berechneter Wert darf erst ab Tag t + 1 genutzt werden. In Arrays ist das eine Verschiebung mit Nullen am Anfang, out[t] = signal[t - periods]. Der Test ist grob. Eine Rauschreihe hat keinen Vorteil, also schummelt jede Regel, die mit Rauschen ein Vermögen macht. Vergleichen Sie die verzerrte und die verzögerte Version auf denselben Daten: Der Abstand ist die Größe des Bias.
# lag_signal: delay a series, with zeros where there is nothing to look back at
import numpy as np
def lag_signal(signal, periods=1):
out = np.zeros(len(signal))
if periods < len(signal):
out[periods:] = signal[:len(signal) - periods]
return out
s = np.array([1.0, -1.0, 1.0, 1.0, -1.0])
print(lag_signal(s)) # [ 0. 1. -1. 1. 1.]
print(lag_signal(s, 2)) # [ 0. 0. 1. -1. 1.]# trailing_mean: the mean of the last `window` values, using only today and the past
import numpy as np
def trailing_mean(values, window):
total = np.cumsum(values)
out = np.full(len(values), np.nan)
out[window - 1:] = (total[window - 1:] - np.concatenate([[0.0], total[:len(values) - window]])) / window
return out
x = np.array([1.0, 2.0, 6.0, 7.0])
print(trailing_mean(x, 2)) # [ nan 1.5 4. 6.5]
print(trailing_mean(x[:3], 2)) # [ nan 1.5 4. ]: adding day 4 changed nothing before itStatistiken müssen mit der Zeit wachsen
Das zweite Leck ist subtil. Eine Reihe mit Mittelwert und Standardabweichung der ganzen Stichprobe zu standardisieren, nutzt Zahlen, die erst am Ende existieren: Frühe Werte werden mit Informationen aus der Zukunft skaliert. Nehmen Sie nur die Vergangenheit: ein gleitendes Fenster (die letzten n Werte bis heute) oder ein wachsendes (alles bis heute). Der Test für jedes solche Merkmal: Nur über die ersten k Tage berechnet, muss es den ersten k Werten der vollen Rechnung entsprechen.
# Whole-sample z-scores leak the future; expanding ones do not
import numpy as np
x = np.array([1.0, 3.0, 2.0, 8.0, 4.0])
whole = (x - x.mean()) / x.std()
n = np.arange(1, len(x) + 1)
mean = np.cumsum(x) / n
var = np.cumsum(x ** 2) / n - mean ** 2
with np.errstate(divide="ignore", invalid="ignore"):
expanding = (x - mean) / np.sqrt(var)
expanding[0] = np.nan
print(np.round(whole, 3))
print(np.round(expanding, 3))
# Prefix test: computing on x[:3] gives the first three expanding values, but not the first three whole-sample ones
print(np.round((x[:3] - x[:3].mean()) / x[:3].std(), 3))- Look-ahead-Bias ist jede Nutzung von Information vor ihrer Bekanntheit; die Ausrichtung ist die klassische Form: Ein Signal von Tag
twird ab Tagt + 1gehandelt. - Merkmale dürfen nur aus der Vergangenheit gebaut werden: gleitende oder wachsende Fenster, nie Mittelwert oder Standardabweichung der ganzen Stichprobe.
- Präfixtest: Ein Merkmal oder Backtest über die ersten
kTage muss den erstenkErgebnissen des vollen Laufs entsprechen; eine Rauschreihe darf kein Geld verdienen.
Als Nächstes: Transaktionskosten, das Erste, wofür ein sauberer Backtest zahlen muss.
Lab: Was bewertet wird
- Ein Signal mit
lag_signalverzögern25 Pkt. - Mit
biased_vs_honestden Schwindel messen25 Pkt. - Mit
trailing_meannur die Vergangenheit mitteln25 Pkt. - Mit
expanding_zscoreohne Spähen standardisieren25 Pkt.