Saber demasiado
El sesgo de anticipación (look-ahead) ocurre cuando un backtest usa información que no estaba disponible en el momento de decidir. Es la razón más común de que una estrategia luzca brillante en papel y pierda dinero en real. Rara vez es una línea obvia; se esconde en el alineamiento (una señal del cierre de hoy operada a ese cierre), en estadísticas calculadas sobre toda la muestra, en datos revisados después y en índices elegidos con retrospectiva. El remedio es un hábito: por cada número que usa una decisión, pregunta cuándo se supo.
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%Alineación: el retraso de un día
El remedio del sesgo de alineación es un retraso: un valor calculado el día t solo puede usarse desde el día t + 1. En arrays es un desplazamiento con ceros al principio, out[t] = signal[t - periods]. La prueba es contundente. Una serie de ruido no tiene ventaja, así que cualquier regla que haga fortuna con ruido hace trampa. Compara la versión sesgada con la retrasada sobre los mismos datos y la diferencia es el tamaño del sesgo.
# 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 itLas estadísticas deben crecer con el tiempo
La segunda fuga es sutil. Estandarizar una serie con la media y la desviación de toda la muestra usa números que solo existen al final: los primeros valores se escalan con información del futuro. Usa solo el pasado: una ventana móvil (los últimos n valores hasta hoy) o creciente (todo hasta hoy). La prueba para cualquier variable así: calcularla solo con los primeros k días debe dar los primeros k valores del cálculo completo.
# 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))- El sesgo de anticipación es cualquier uso de información antes de conocerse; la alineación es la forma clásica: una señal del día
tse opera desde el díat + 1. - Las variables deben construirse solo con el pasado: ventanas móviles o crecientes, nunca la media o desviación de toda la muestra.
- Prueba del prefijo: una variable o backtest con los primeros
kdías debe igualar los primeroskresultados de la ejecución completa; una serie de ruido no debe dar dinero.
A continuación: los costes de transacción, lo primero que debe pagar un backtest limpio.
Lab: qué se califica
- Retrasar una señal con
lag_signal25 pts - Medir la trampa con
biased_vs_honest25 pts - Promediar solo el pasado con
trailing_mean25 pts - Estandarizar sin espiar con
expanding_zscore25 pts