En savoir trop
Le biais d'anticipation (look-ahead) apparaît quand un backtest utilise une information indisponible au moment de la décision. C'est la raison la plus courante pour qu'une stratégie soit brillante sur le papier et perde de l'argent en réel. Il s'agit rarement d'une seule ligne évidente ; il se cache dans l'alignement (un signal de la clôture d'aujourd'hui négocié à cette clôture), dans des statistiques calculées sur tout l'échantillon, dans des données révisées plus tard, et dans des indices choisis avec le recul. Le remède est une habitude : pour chaque nombre utilisé par une décision, demandez quand il est devenu connu.
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%L'alignement : le décalage d'un jour
Le remède au biais d'alignement est un décalage : une valeur calculée au jour t ne peut servir qu'à partir du jour t + 1. Dans les tableaux, c'est un décalage avec des zéros au début, out[t] = signal[t - periods]. Le test est brutal. Une série de bruit n'a aucun avantage, donc toute règle qui fait fortune sur du bruit triche. Comparez la version biaisée et la version décalée sur les mêmes données : l'écart est la taille du biais.
# 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 itLes statistiques doivent grandir avec le temps
La deuxième fuite est subtile. Standardiser une série avec la moyenne et l'écart-type de tout l'échantillon utilise des nombres qui n'existent qu'à la fin : les premières valeurs sont mises à l'échelle avec de l'information du futur. N'utilisez que le passé : une fenêtre glissante (les n dernières valeurs jusqu'à aujourd'hui) ou croissante (tout jusqu'à aujourd'hui). Le test pour toute variable de ce genre : la calculer sur les k premiers jours seulement doit donner les k premières valeurs du calcul complet.
# 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))- Le biais d'anticipation est tout usage d'une information avant qu'elle soit connue ; l'alignement en est la forme classique : un signal du jour
tse négocie à partir du jourt + 1. - Les variables doivent être construites avec le passé seul : fenêtres glissantes ou croissantes, jamais la moyenne ou l'écart-type de tout l'échantillon.
- Test du préfixe : une variable ou un backtest sur les
kpremiers jours doit égaler leskpremiers résultats de l'exécution complète ; une série de bruit ne doit pas rapporter.
À suivre : les coûts de transaction, la première chose qu'un backtest propre doit payer.
Lab : ce qui est noté
- Retarder un signal avec
lag_signal25 pts - Mesurer la triche avec
biased_vs_honest25 pts - Moyenner uniquement le passé avec
trailing_mean25 pts - Standardiser sans tricher avec
expanding_zscore25 pts