Lección 2.5.1 · 60 min
Sesgos de backtest: anticipación, supervivencia, selección y narrativa
Un backtest no miente por accidente: miente siempre en la misma dirección, a favor de la estrategia. Esta lección monta un museo de sesgos con datos sintéticos en los que se conoce la respuesta, provoca cada uno a propósito y mide cuánto infla el resultado.
- Módulo
- 2.5 · Sesgos y validación
- Duración
- 60 min
Lecciones del nivel 2
2.1 · Estadística y econometría para trading
- 2.1.1 Contrastes de hipótesis, p-valores, bootstrap y regresión con errores robustos
- 2.1.2 Estacionariedad: ADF, KPSS, diferenciación y diferenciación fraccional
- 2.1.3 Modelos ARMA y ARIMA: autocorrelación simple (ACF) y parcial (PACF)
- 2.1.4 Modelos de volatilidad: ARCH, GARCH, EGARCH y GJR con la librería arch
- 2.1.5 Cointegración: Engle-Granger, Johansen y vida media de la reversión
- 2.1.6 Filtro de Kalman para hedge ratios dinámicos
- 2.1.7 Detección de regímenes: modelos de Markov ocultos y cambios estructurales
- 2.1.8 El problema de las pruebas múltiples: Bonferroni, FDR y el «factor zoo»
2.2 · Anatomía de un sistema de trading
- 2.2.1 Hipótesis de ventaja: riesgo, comportamiento, estructura e información
- 2.2.2 Universo, señal, filtro, entrada, salida, sizing y gestión del riesgo
- 2.2.3 Frecuencias: intradía, swing y posicional, y la capacidad de una estrategia
- 2.2.4 Diario de investigación y registro de experimentos contra el data snooping
2.3 · Familias de estrategias
- 2.3.1 Seguimiento de tendencia y momentum
- 2.3.2 Reversión a la media: RSI y Bollinger, reversión intradía y huecos
- 2.3.3 Pairs trading y arbitraje estadístico: distancia, cointegración y Kalman
- 2.3.4 Carry en divisas, futuros y bonos; estacionalidad y efectos calendario
- 2.3.5 Factores (value, calidad, baja volatilidad, tamaño) y estrategias de eventos
- 2.3.6 Introducción al market making: inventario, spread y selección adversa (Avellaneda-Stoikov)
- 2.3.7 Estrategias específicas de cripto: momentum, arbitraje de funding y basis
2.4 · Backtesting: motores y paradigmas
- 2.4.1 Backtest vectorizado desde cero: matrices, desfase, costes y límites
- 2.4.2 Backtest por eventos desde cero: bucle, cartera, órdenes y fills
- 2.4.3 vectorbt: barridos masivos de parámetros y simulación de carteras
- 2.4.4 backtesting.py y backtrader: dos motores didácticos y su estado
- 2.4.5 NautilusTrader: arquitectura, datos, estrategias y el mismo código en backtest y en real
- 2.4.6 LEAN/QuantConnect y Zipline Reloaded; bt para asignación de activos
- 2.4.7 Modelado de costes: comisiones, horquilla, impacto, financiación y préstamo de títulos
- 2.4.8 Modelado de fills: órdenes limitadas, cola, fills parciales, huecos y stops
2.5 · Sesgos y validación
- 2.5.1 Sesgos de backtest: anticipación, supervivencia, selección y narrativa
- 2.5.2 In-sample y out-of-sample: walk-forward anclado y móvil
- 2.5.3 Validación cruzada combinatoria purgada para estrategias
- 2.5.4 Deflated Sharpe Ratio y Probability of Backtest Overfitting (PBO)
- 2.5.5 Monte Carlo: remuestreo de trades, bootstrap de bloques y permutaciones de señales
- 2.5.6 Sensibilidad de parámetros: mesetas frente a picos, robustez entre activos y periodos
- 2.5.7 Datos sintéticos para pruebas de estrés: GBM, GARCH y bootstrapping
- 2.5.8 Checklist de «backtest honesto» antes de pasar a paper trading
2.6 · Métricas de rendimiento
Un museo de sesgos
Un backtest es una simulación de lo que habría pasado si alguien hubiera seguido unas reglas en el pasado. Los errores de una simulación así no son aleatorios: casi todos consisten en darle a ese alguien algo que no tenía (información del futuro, una lista de empresas que sobrevivieron, la elección entre mil variantes ya probadas) y por eso todos empujan en la misma dirección, a favor de la estrategia. Un backtest con un Sharpe de 3 no es una buena noticia que haya que celebrar, es una anomalía que hay que explicar.
Esta lección es un museo: cada sala provoca un sesgo a propósito sobre los datos del nivel 2 y mide cuánto infla el resultado. La figura de la cabecera resume las cuatro salas con código; la quinta, la narrativa, no se mide con código y cierra la lección.
Fuente Mermaid
flowchart LR S[Sesgos de backtest] --> I[Información] S --> U[Universo] S --> E[Elección] S --> N[Interpretación] I --> A1[Misma barra] I --> A2[Muestra completa] U --> SV[Supervivencia] E --> SE[Selección] E --> DS[Data snooping] N --> NA[Narrativa]
| Sesgo | Qué se le da al pasado | Dónde entra | Síntoma | Prueba |
|---|---|---|---|---|
| Anticipación (misma barra) | El precio con el que se decide | Ejecución | Sharpe enorme en señales rápidas | Retrasar una sesión más |
| Anticipación (muestra completa) | Medias, máximos o escalas del futuro | Preparación de datos | La señal cambia al añadir datos | Cortar la historia |
| Supervivencia | La lista de quién sigue vivo hoy | Universo | Sin quiebras ni deslistados | Universo point-in-time |
| Selección | La mejor de muchas variantes | Elección | Gran caída fuera de muestra | Contar los intentos |
| Data snooping | La muestra de validación, mirada muchas veces | Iteración | «Validado» y luego plano | Muestra final intacta |
| Narrativa | Una explicación hecha a medida | Interpretación | Historia perfecta, sin refutación | Hipótesis escrita antes |
Anticipación I: ejecutar en la barra de la señal
El sesgo de anticipación (look-ahead bias) es usar en la fecha t un dato que solo se conoce después de t. La forma más común es la más simple: calcular la señal con el cierre de hoy y aplicarla a la rentabilidad de hoy. En el mundo real, el cierre se conoce cuando la sesión ya ha terminado; la orden que ese cierre dispara se ejecuta, como pronto, en la sesión siguiente (o en la subasta de cierre si la señal se calcula unos minutos antes, con un precio que no es exactamente el cierre).
Fuente Mermaid
flowchart LR C[Cierre de t] --> S[Señal y orden] S --> X[Ejecución en t+1] X --> R[Gana r de t+1]
Los datos son los 24 futuros del nivel 2: series continuas con ajuste proporcional hacia atrás (lección 1.4.6), sesiones de 2000 a 2025. La cartera de prueba da a cada mercado la misma volatilidad objetivo, estimada con una media exponencial de 60 sesiones, y la señal es el signo de la rentabilidad de las últimas 252 sesiones: momentum de series temporales, el efecto que el generador planta en todos los mercados (lección 2.3.1).
import numpy as np
import pandas as pd
fut = pd.read_parquet("futuros2_continuos.parquet")
precio = fut.pivot(index="fecha", columns="mercado",
values="cierre_ajustado")
r = precio.pct_change(fill_method=None)
vol = r.ewm(span=60, min_periods=60).std() * np.sqrt(252)
def sharpe(x):
x = x.dropna()
return x.mean() / x.std() * np.sqrt(252)
def cartera(senal, retraso=1):
"""Cada mercado con un 40 % / 24 de volatilidad objetivo."""
peso = senal * 0.40 / vol / precio.shape[1]
return (peso.shift(retraso) * r).sum(axis=1, min_count=1)
print(precio.shape, precio.index[0].date(), precio.index[-1].date())(6539, 24) 2000-01-03 2025-12-31retraso es el número de sesiones entre la fecha de la señal y la rentabilidad que gana. Con retraso=1, el peso
calculado al cierre de t multiplica la rentabilidad de t+1: es la ejecución honesta. Con retraso=0, multiplica la de
t, la misma que ya ha entrado en la señal y en la volatilidad.
tsmom = np.sign(precio.pct_change(252, fill_method=None))
bien = cartera(tsmom, retraso=1).loc["2001":]
mal = cartera(tsmom, retraso=0).loc["2001":]
print(f"Ejecución en t+1: Sharpe {sharpe(bien):.2f}")
print(f"Ejecución en la misma barra: Sharpe {sharpe(mal):.2f}")Ejecución en t+1: Sharpe 1.00
Ejecución en la misma barra: Sharpe 3.16El momentum de 252 sesiones tiene ventaja de verdad en estos datos, y la ejecución honesta la recoge. Ejecutar en la
misma barra multiplica el Sharpe por más de tres. La señal solo cambia de signo cuando la rentabilidad de un año cruza
el cero, así que en la mayoría de los días el retraso no importa; el engaño se concentra en los días en que la
rentabilidad de hoy es la que provoca el cruce, y en el escalado por volatilidad, que con retraso=0 ya «sabe» si hoy
ha sido un día agitado. Cuanto más rápida es la señal, mayor es la parte de ella que la rentabilidad de hoy explica:
filas = {}
for n in (20, 50, 100, 200):
cruce = np.sign(precio / precio.rolling(n).mean() - 1)
filas[f"precio frente a media {n}"] = {
f"retraso {k}": sharpe(cartera(cruce, k).loc["2001":])
for k in (0, 1, 2)}
pd.DataFrame(filas).T.round(2) retraso 0 retraso 1 retraso 2
precio frente a media 20 17.04 0.26 0.37
precio frente a media 50 10.95 0.55 0.50
precio frente a media 100 7.83 0.74 0.66
precio frente a media 200 5.67 0.84 0.92Con una media de 20 sesiones, la ejecución en la misma barra da un Sharpe de dos cifras: ningún sistema real se
acerca a eso, y es la primera pista de que algo está mal. La tabla enseña también la prueba del retraso: entre
retraso 1 y retraso 2 el Sharpe cambia poco, porque una señal honesta no depende de un único día; entre retraso 0
y retraso 1 hay un precipicio. Si el backtest que estás revisando se parece a la columna retraso 0, retrasa la
señal una sesión: si el resultado se hunde, la ventaja era el precio de un día que no tenías.
Anticipación II: normalizar con toda la muestra
La segunda forma es más sutil, porque no hay ningún shift mal puesto. Ocurre en la preparación de los datos: se
tipifica una variable con la media y la desviación típica de toda la muestra, se escala con el máximo histórico, se
recortan los extremos con los percentiles de toda la serie o se ajusta un StandardScaler o un PCA con todos los
datos antes de separar entrenamiento y prueba. Cada uno de esos estadísticos contiene el futuro.
El ejemplo extremo: una regla de reversión a la media que compra cuando el precio está por debajo de «su media» y vende cuando está por encima. Con la media de toda la muestra, la regla sabe en 2001 dónde estará el precio en promedio hasta 2025. Con una media expandida (solo la historia hasta cada día), no:
lp = np.log(precio)
z_total = (lp - lp.mean()) / lp.std()
z_exp = (lp - lp.expanding(252).mean()) / lp.expanding(252).std()
for nombre, z in (("muestra completa", z_total),
("expandida", z_exp)):
regla = -z.clip(-2, 2) # compra barato, vende caro
print(f"{nombre:>16}: Sharpe {sharpe(cartera(regla)['2001':]):.2f}")muestra completa: Sharpe 0.67
expandida: Sharpe -0.65La ejecución es honesta en los dos casos (retraso=1), y aun así la versión con toda la muestra gana y la expandida
pierde. En estos datos el spot de las materias primas, las divisas y los bonos sí revierte hacia un ancla, pero la
curva de futuros ya lo descuenta: en los picos cotiza en backwardation y en los suelos en contango, así que la serie
continua, que es lo que se opera, no conserva esa reversión (solo un 10 % del carry de reversión tiene prima, según el
README). Lo que le queda es momentum, y una regla contraria pierde. Con la media de toda la muestra, la regla conoce
el nivel al que «volverá» el precio porque ese nivel se ha calculado con los precios a los que volvió. Es un resultado
imposible disfrazado de estadística.
Este sesgo no se ve leyendo el código del backtest, porque está antes. Se ve con una prueba mecánica: cortar la historia. Una señal honesta en la fecha t solo depende de datos hasta t, así que calcularla con toda la serie o con la serie cortada en t tiene que dar exactamente lo mismo hasta t.
def prueba_corte(fn, datos, corte="2010-12-31"):
"""Diferencia máxima hasta `corte` entre la señal calculada con
toda la historia y con la historia cortada en `corte`."""
completa = fn(datos).loc[:corte]
cortada = fn(datos.loc[:corte])
return float((completa - cortada).abs().max().max())
def f_total(lp):
return (lp - lp.mean()) / lp.std()
def f_exp(lp):
return (lp - lp.expanding(252).mean()) / lp.expanding(252).std()
def f_tsmom(p):
return np.sign(p.pct_change(252, fill_method=None))
for fn, datos in ((f_total, lp), (f_exp, lp), (f_tsmom, precio)):
print(f"{fn.__name__:>8}: {prueba_corte(fn, datos):.6f}") f_total: 2.975494
f_exp: 0.000000
f_tsmom: 0.000000La z-puntuación con toda la muestra cambia hasta en varias desviaciones típicas al añadir quince años de datos; la
expandida y el momentum no cambian nada. La prueba cuesta tres líneas y conviene pasarla a toda función que fabrique
señales, en varias fechas de corte, como un test de pytest más (lección 0.4.5).
Supervivencia: los miembros de hoy proyectados hacia atrás
El sesgo de supervivencia es elegir el universo con información del final: «las acciones del índice», cuando la lista que se descarga es la de hoy. Las empresas que quebraron, las que fueron compradas y las que salieron del índice por haberse hundido no están en esa lista, así que una cartera construida con ella no puede tenerlas en 2008. La lección 1.4.4 lo trató con datos del nivel 1; el universo del nivel 2 tiene un índice de 100 miembros con historia point-in-time y diez deslistados documentados, cuatro de ellos quiebras con rentabilidad de deslistado del −100 %.
aj = pd.read_parquet("universo2_ajustados.parquet").pivot(
index="fecha", columns="ticker", values="cierre_ajustado")
miembros = pd.read_csv("universo2_miembros.csv",
parse_dates=["alta", "baja"])
eventos = pd.read_csv("universo2_eventos.csv", parse_dates=["fecha"])
ru = aj.pct_change(fill_method=None)
dentro = pd.DataFrame(False, index=ru.index, columns=ru.columns)
fin_datos = ru.index[-1] + pd.Timedelta(days=1)
for m in miembros.itertuples():
hasta = m.baja if pd.notna(m.baja) else fin_datos
dentro.loc[(ru.index >= m.alta) & (ru.index < hasta),
m.ticker] = True
# la rentabilidad de deslistado entra la sesión siguiente a la última
salidas = eventos.dropna(subset=["rentabilidad_deslistado"])
for e in salidas.itertuples():
i = ru.index.get_loc(aj[e.ticker].last_valid_index()) + 1
if i < len(ru):
ru.iloc[i, ru.columns.get_loc(e.ticker)] = (
e.rentabilidad_deslistado)
dentro.iloc[i, ru.columns.get_loc(e.ticker)] = True
print(miembros["ticker"].nunique(), "empresas pasaron por el índice")
print(salidas.groupby("evento").size().to_string())115 empresas pasaron por el índice
evento
adquisicion_efectivo 4
deslistado_quiebra 4
exclusion_voluntaria 1
fusion_acciones 1Dos detalles de implementación cuentan tanto como la lista. El primero, la rentabilidad de deslistado: cuando una
acción deja de cotizar, la última rentabilidad no es la del último cierre, sino la que va de ese cierre al pago final
(−100 % en una quiebra; en una compra en efectivo, el diferencial que quedaba hasta el precio de la oferta, entre el
0,03 % y el 0,22 % en estos datos). Sin ella, la quiebra desaparece de la cartera con su último precio de 0,85 USD
como si se hubiera vendido a ese precio. El segundo, la fecha: la pertenencia se consulta en cada sesión, con la
alta y la baja de cada intervalo. Una cartera que rebalancee cada mes con universo2_caracteristicas.parquet
tiene el mismo problema resuelto de otra forma: cada deslistado tiene una última fila en el fin de mes de su salida,
sin precio y con una rentabilidad_mes que ya incluye la de deslistado. Esas filas no sirven para formar señales
(se filtran por cierre no vacío), pero tienen que entrar en la rentabilidad de la cartera.
def anual(x):
return (1 + x.fillna(0)).prod() ** (252 / len(x)) - 1
pit = ru.where(dentro).mean(axis=1).iloc[1:]
hoy = miembros.loc[miembros["baja"].isna(), "ticker"]
superv = ru[hoy].mean(axis=1).iloc[1:]
for nombre, x in (("point-in-time", pit), ("miembros de hoy", superv)):
print(f"{nombre:>15}: {anual(x):.2%} anual, Sharpe {sharpe(x):.2f}")
fuera = set(miembros["ticker"]) - set(hoy)
quiebras = salidas.loc[salidas["evento"] == "deslistado_quiebra",
"ticker"]
print(len(fuera), "empresas fuera de la lista de hoy; quiebras:",
sorted(fuera & set(quiebras))) point-in-time: 5.75% anual, Sharpe 0.38
miembros de hoy: 6.95% anual, Sharpe 0.44
15 empresas fuera de la lista de hoy; quiebras: ['QXKV', 'QXQT', 'QXYX']Son 1,2 puntos de rentabilidad al año, que en veinte años se acumulan: 1 USD de 2006 termina en 3,83 USD con los miembros de hoy y en 3,05 USD con el índice point-in-time. La diferencia viene de las 15 empresas que pasaron por el índice y no están en la lista de hoy: tres de las cuatro quiebras, con su −100 % (la cuarta, QXZT, nunca llegó a entrar), las que salieron del índice tras hundirse y las compradas. Y el sesgo no solo afecta a una cartera del índice: cualquier estrategia que compre perdedoras (reversión, value) parece mejor con supervivientes, porque las perdedoras que siguieron cayendo hasta desaparecer no están en la muestra. El momentum, al revés, puede parecer peor de lo que es por el mismo motivo.
Selección y data snooping: el mejor de mil
El sesgo de selección aparece al elegir a posteriori qué se enseña: la mejor variante de una regla, el mercado donde funcionó, el periodo en que salió bien. El data snooping es su versión iterativa: reutilizar los mismos datos para probar idea tras idea hasta que una sale. Los dos tienen la misma aritmética. Si se prueban N candidatos sin ventaja, cada uno con un Sharpe medido que es ruido, el mejor de ellos no es ruido «medio»: es el máximo de N ruidos.
ruido2_estrategias.parquet tiene los rendimientos diarios de 1.000 estrategias ficticias en las sesiones de 2022 a
2025, en 50 familias de 20 variantes correlacionadas. Todas tienen ventaja cero salvo cinco, cuyo Sharpe verdadero
está en ruido2_verdad.csv. La prueba: elegir con 2022-2023 y mirar 2024-2025.
est = pd.read_parquet("ruido2_estrategias.parquet").pivot(
index="fecha", columns="estrategia", values="rendimiento")
verdad = pd.read_csv("ruido2_verdad.csv", index_col="estrategia")
def sharpe_cols(df):
return df.mean() / df.std() * np.sqrt(252)
sh_is = sharpe_cols(est.loc[:"2023"])
sh_oos = sharpe_cols(est.loc["2024":])
top = sh_is.nlargest(10).index
tabla = pd.DataFrame({"IS 2022-23": sh_is[top],
"OOS 2024-25": sh_oos[top],
"real": verdad.loc[top, "sharpe_real"]})
print(tabla.round(2))
print(f"Media del top 10: IS {sh_is[top].mean():.2f}, "
f"OOS {sh_oos[top].mean():.2f}") IS 2022-23 OOS 2024-25 real
estrategia
e0133 2.98 -0.04 0.8
e0542 2.91 -0.02 0.0
e0462 2.40 0.51 0.0
e0781 2.36 -1.47 0.0
e0518 2.28 0.85 1.5
e0565 2.08 -0.67 0.0
e0060 1.98 -0.26 0.0
e0124 1.91 0.92 0.0
e0950 1.89 -0.57 0.0
e0835 1.88 0.09 0.0
Media del top 10: IS 2.27, OOS -0.07El top 10 de la primera mitad tiene un Sharpe medio de más de 2, y en la segunda su media es negativa. Dos de los diez tienen ventaja real (e0133 y e0518, con Sharpe verdadero 0,8 y 1,5); los otros ocho son ruido que tuvo dos buenos años. La muestra de cada mitad, 501 sesiones, da un error típico del Sharpe anual de : con mil candidatos, el máximo del ruido está en torno a tres errores típicos por encima de cero. La teoría de valores extremos da una aproximación para el máximo esperado de N Sharpes sin ventaja e independientes, con (la constante de Euler-Mascheroni) y la inversa de la normal:
from scipy.stats import norm
def max_esperado(n, error_tipico):
g = np.euler_gamma
z = ((1 - g) * norm.ppf(1 - 1 / n)
+ g * norm.ppf(1 - 1 / (n * np.e)))
return z * error_tipico
et = np.sqrt(252 / 501)
for n in (1, 10, 100, 1000):
m = max_esperado(n, et) if n > 1 else 0.0
print(f"N = {n:>4}: máximo esperado {m:.2f}")
ruido = verdad.index[verdad["sharpe_real"] == 0]
print(f"Máximo del ruido en 2022-23: {sh_is[ruido].max():.2f}")N = 1: máximo esperado 0.00
N = 10: máximo esperado 1.12
N = 100: máximo esperado 1.79
N = 1000: máximo esperado 2.31
Máximo del ruido en 2022-23: 2.91Con un solo candidato, lo esperado de una estrategia sin ventaja es un Sharpe de 0; con mil, alrededor de 2,3. El máximo observado del ruido, 2,91, está aún más arriba: es una sola realización, y el máximo de mil ruidos tiene mucha dispersión. La fórmula supone candidatos independientes; aquí no lo son (20 variantes por familia, correlación 0,5), lo que reduce el número efectivo de intentos y el máximo esperado. La lección 2.5.4 convierte esta cuenta en un contraste, el Deflated Sharpe Ratio, y la 2.1.8 trata las correcciones por pruebas múltiples.
El snooping es más traicionero, porque parece prudente. Un investigador que ya sabe que el mejor de mil engaña decide exigir que la estrategia sea buena en las dos mitades:
for umbral in (0.5, 1.0):
pasan = sh_is.index[(sh_is > umbral) & (sh_oos > umbral)]
reales = int((verdad.loc[pasan, "sharpe_real"] > 0).sum())
print(f"Sharpe > {umbral} en las dos mitades: {len(pasan)} "
f"estrategias, {reales} con ventaja real")Sharpe > 0.5 en las dos mitades: 56 estrategias, 3 con ventaja real
Sharpe > 1.0 en las dos mitades: 7 estrategias, 0 con ventaja realCon un umbral de 0,5 pasan 56 estrategias, y solo tres tienen ventaja. Con un umbral de 1 pasan siete, y ninguna: las cinco que sí la tienen se quedan fuera en una de las dos mitades, porque con 501 sesiones hasta un Sharpe verdadero de 1,5 puede medir menos de 1. La segunda mitad dejó de ser una muestra de validación en el momento en que se usó para elegir. Una muestra fuera de muestra solo es independiente si se mira una vez, al final, con la estrategia ya decidida. Cada vez que se mira y se corrige algo, pasa a formar parte del entrenamiento. Las lecciones 2.5.2 y 2.5.3 organizan esa separación (walk-forward y validación cruzada purgada) y la 2.2.4 explica cómo registrar cada intento para poder contar N. White (2000) propuso uno de los primeros contrastes formales del data snooping, el Reality Check, que compara la mejor regla con la distribución del máximo de todas las probadas.
Sesgo de narrativa
El último sesgo no está en el código. Es la tendencia a construir, después de ver un resultado, una explicación que lo hace parecer inevitable, y a tomarla como prueba. Una vez que el backtest de una variante sale bien, siempre hay una historia: «los fondos rebalancean a fin de mes», «los minoristas sobrerreaccionan a las noticias», «el carry paga el riesgo de crisis». Puede que la historia sea cierta. El problema es el orden: se eligió porque encajaba con el dato, y con 1.000 resultados distintos habría habido 1.000 historias distintas, todas convincentes.
Los síntomas son reconocibles: la explicación llega después del resultado; nadie escribió antes qué resultado la refutaría; los parámetros elegidos tienen una justificación económica inventada a medida («63 sesiones porque es un trimestre»); los periodos malos se explican como excepciones («2022 fue un año raro») y los buenos, como la regla; y la historia no predice nada que no se haya mirado ya. En la literatura académica tiene nombre, HARKing (hypothesizing after the results are known), y es una de las razones por las que Harvey, Liu y Zhu (2016) piden umbrales más exigentes para los factores nuevos.
La defensa es procedimental. Esta lista se rellena antes de mirar los datos de prueba, y se guarda con fecha en el diario de investigación (lección 2.2.4):
| Pregunta | Respuesta mínima aceptable |
|---|---|
| ¿Cuál es la hipótesis? | Una frase con el efecto y su signo, escrita antes del backtest |
| ¿Quién paga la ventaja y por qué seguiría pagándola? | Un mecanismo: prima de riesgo, restricción de otros, error de comportamiento (lección 2.2.1) |
| ¿Qué resultado la refutaría? | Un umbral concreto: «Sharpe < 0,3 en 2015-2025», «IC con signo contrario en dos sectores» |
| ¿Qué predice además del resultado principal? | Algo no mirado: otro mercado, otro periodo, una relación con una variable |
| ¿Cuántas variantes se han probado? | El número, con las que fallaron (para el Deflated Sharpe, lección 2.5.4) |
| ¿Qué datos no se han tocado? | Un periodo o un universo reservado, que se mira una sola vez |
| ¿Qué dice el placebo? | El mismo método aplicado a una variable sin efecto (en el nivel 2, puntuacion_esg) |
Una prueba útil: escribir también la historia contraria. Si para el resultado opuesto («la regla pierde porque…») se encuentra con la misma facilidad una explicación igual de convincente, la explicación no está aportando información.
Cuánto infla cada sesgo
La figura de la cabecera pone las cuatro salas con código en la misma escala, el Sharpe anual: la ejecución en la misma barra pasa el momentum de 1,00 a 3,16; la normalización con toda la muestra, una regla perdedora (−0,65) a una ganadora (0,67); los supervivientes suben el Sharpe del índice de 0,38 a 0,44; y la selección del mejor de mil da un top 10 de 2,3 que fuera de muestra vale −0,07. Las magnitudes son de estos datos sintéticos, pero el orden es habitual: la anticipación produce los resultados más absurdos (y por eso es la más fácil de detectar), mientras que la supervivencia y la selección dan resultados plausibles, que son los más peligrosos.
| Sesgo | Sin sesgo | Con sesgo | Defensa automática |
|---|---|---|---|
| Misma barra (momentum 252) | 1,00 | 3,16 | Prueba del retraso |
| Muestra completa (reversión) | −0,65 | 0,67 | Prueba del corte |
| Supervivencia (índice) | 0,38 | 0,44 | Universo point-in-time y deslistados |
| Selección (top 10 de 1.000) | −0,07 fuera | 2,3 dentro | Contar N y deflactar (2.5.4) |
La lección 2.5.8 recoge estas pruebas en una lista de control de «backtest honesto» antes de pasar a paper trading.
Ejercicios
- Aplica la prueba del retraso a la regla «precio frente a media de 50 sesiones» con retrasos de 1 a 5 sesiones. ¿Se comporta como una señal honesta?
Solución
cruce50 = np.sign(precio / precio.rolling(50).mean() - 1)
pd.Series({k: sharpe(cartera(cruce50, k).loc["2001":])
for k in range(0, 6)}, name="Sharpe").round(2)0 10.95
1 0.55
2 0.50
3 0.54
4 0.58
5 0.53
Name: Sharpe, dtype: float64De 1 a 5 sesiones el Sharpe oscila entre 0,50 y 0,58, sin tendencia ni saltos: es lo esperable en una señal de tendencia, cuya ventaja se reparte en muchas sesiones. El único salto está entre 0 y 1, que es el sesgo de la misma barra. Una caída brusca entre 1 y 2 sería sospechosa por otro motivo: una ventaja que vive en una sola sesión suele ser un error de alineación de datos, o una ventaja que los costes se comen (lección 2.4.7).
- Repite el índice point-in-time sin las rentabilidades de deslistado (es decir, sin el −100 % de las quiebras). ¿Qué parte del sesgo de supervivencia explica ese detalle?
Solución
ru_sin = aj.pct_change(fill_method=None)
dentro_sin = dentro.copy()
for e in salidas.itertuples():
i = ru.index.get_loc(aj[e.ticker].last_valid_index()) + 1
if i < len(ru):
dentro_sin.iloc[i, ru.columns.get_loc(e.ticker)] = False
pit_sin = ru_sin.where(dentro_sin).mean(axis=1).iloc[1:]
print(f"Point-in-time con deslistados: {anual(pit):.2%}")
print(f"Point-in-time sin deslistados: {anual(pit_sin):.2%}")
print(f"Miembros de hoy: {anual(superv):.2%}")Point-in-time con deslistados: 5.75%
Point-in-time sin deslistados: 5.96%
Miembros de hoy: 6.95%Sin la rentabilidad de deslistado, las cuatro quiebras salen de la cartera a su último precio, entre 0,66 y 1,12 USD, en lugar de perderlo todo. El índice gana algo de rentabilidad, pero sigue lejos de los supervivientes: la mayor parte del sesgo no está en el último día de las quiebras, sino en las 320 sesiones de caída previa y en las empresas que salieron del índice por hundirse, que un universo de supervivientes nunca incluye.
- Un compañero te enseña una estrategia de reversión semanal en acciones con Sharpe 1,8 entre 2010 y 2020 y te explica que funciona «porque los minoristas sobrerreaccionan a las noticias del fin de semana». Escribe tres preguntas de la lista de control que le harías antes de mirar el código y qué respuesta te haría desconfiar.
Solución
Primera: «¿cuántas variantes probaste (horizontes, umbrales, universos) antes de esta?». Desconfiaría de «no lo sé» o de un número grande sin deflactar (lección 2.5.4). Segunda: «¿qué universo usaste y de cuándo es la lista?». Si es la lista actual de un índice, la reversión está inflada por supervivencia: las perdedoras que siguieron cayendo no están. Tercera: «¿qué resultado habría refutado lo de los minoristas?». Si la explicación llegó después del backtest y no predice nada que no se haya mirado (por ejemplo, que el efecto sea mayor en las acciones con más negociación minorista o tras fines de semana con noticias), es narrativa. También pediría la prueba del retraso: la reversión semanal es muy sensible a ejecutar al mismo cierre que calcula la señal.
Respuesta plegada en cada pregunta
Autoevaluación
Una cartera de momentum de 252 sesiones da un Sharpe de 1,00 cuando la posición se toma al día siguiente de la señal y de 3,16 cuando se aplica a la rentabilidad de la misma sesión. ¿Qué explica la diferencia?
- AQue el momentum funciona mejor a corto plazo
- BQue con retraso 0 la posición gana la rentabilidad del día que ya ha entrado en la señal y en la volatilidad: información que no existía al decidir
- CQue los costes de transacción son menores con retraso 0
- DQue el ajuste proporcional de las series continuas favorece el retraso 0
Ver respuesta
Respuesta B. Que con retraso 0 la posición gana la rentabilidad del día que ya ha entrado en la señal y en la volatilidad: información que no existía al decidir
La señal y el escalado por volatilidad se calculan con el cierre de hoy. Aplicarlos a la rentabilidad de hoy es operar al cierre con un precio que solo se conoce después del cierre. El salto del 1,00 al 3,16 es todo anticipación.
¿Qué detecta la «prueba del corte» (calcular la señal con la historia hasta una fecha y compararla con la calculada con toda la historia)?
- ASi la señal tiene demasiados parámetros
- BSi algún valor de la señal en una fecha cambia al añadir datos posteriores, es decir, si usa el futuro
- CSi la señal es estacionaria
- DSi hay huecos en los datos
Ver respuesta
Respuesta B. Si algún valor de la señal en una fecha cambia al añadir datos posteriores, es decir, si usa el futuro
Una señal honesta en la fecha t solo depende de datos hasta t, así que no cambia al añadir datos de después. La z-puntuación con la media de toda la muestra cambia en todas las fechas: falla la prueba.
En el universo sintético, el índice equiponderado point-in-time rinde un 5,75 % anual y el de los 100 miembros de hoy proyectados hacia atrás, un 6,95 %. ¿De dónde sale la diferencia?
- ADe que los miembros de hoy son empresas más grandes
- BDe que la lista de hoy excluye a las que quebraron o salieron del índice por caer: el pasado se rellena con ganadoras conocidas a posteriori
- CDe los dividendos
- DDe un error de redondeo
Ver respuesta
Respuesta B. De que la lista de hoy excluye a las que quebraron o salieron del índice por caer: el pasado se rellena con ganadoras conocidas a posteriori
La lista de miembros actuales es información de 2025. Usarla en 2006 garantiza que ninguna de las cuatro quiebras (rentabilidad de deslistado −100 %) ni las expulsadas por pequeñas entren en la cartera.
De 1.000 estrategias sin ventaja salvo cinco, siete superan un Sharpe de 1 tanto en 2022-2023 como en 2024-2025. ¿Cuántas de esas siete tienen ventaja real?
- ALas siete: superar dos periodos es una validación
- BNinguna: con mil candidatos, pasar dos filtros por azar es esperable, y usar la segunda mitad como filtro la convierte en parte de la selección
- CExactamente cinco
- DNo se puede saber
Ver respuesta
Respuesta B. Ninguna: con mil candidatos, pasar dos filtros por azar es esperable, y usar la segunda mitad como filtro la convierte en parte de la selección
En estos datos se conoce la verdad (
ruido2_verdad.csv): las siete tienen Sharpe real 0. Un periodo de validación solo es independiente si se mira una vez, después de haber elegido.¿Qué es el sesgo de narrativa?
- AUsar datos de noticias en una estrategia
- BConstruir después de ver el resultado una explicación convincente que lo hace parecer inevitable, y tomarla como prueba
- CPreferir estrategias de tendencia
- DPublicar solo las estrategias que funcionan
Ver respuesta
Respuesta B. Construir después de ver el resultado una explicación convincente que lo hace parecer inevitable, y tomarla como prueba
La historia llega después del dato y siempre encaja, porque se ha elegido para que encaje. La defensa es escribir la hipótesis, el mecanismo y lo que la refutaría antes de mirar (lecciones 2.2.1 y 2.2.4).
Ideas clave
- Todos los sesgos de backtest empujan en la misma dirección: a favor de la estrategia. Un resultado bueno pide más pruebas, no menos.
- Anticipación: ejecutar en la barra de la señal sube el Sharpe del momentum de futuros de 1,00 a 3,16; normalizar con la media de toda la muestra convierte una regla perdedora (−0,65) en una ganadora (0,67).
- Dos pruebas automáticas atrapan casi toda la anticipación: retrasar la señal una sesión más y recalcularla con la historia cortada en una fecha.
- Supervivencia: los miembros de hoy proyectados hacia atrás rinden un 6,95 % anual frente al 5,75 % del índice point-in-time.
- Selección y snooping: el top 10 de mil estrategias (Sharpe medio 2,3 en 2022-2023) da −0,07 en 2024-2025, y filtrar por las dos mitades deja siete falsos positivos.
- La narrativa no se mide con código: se previene escribiendo hipótesis, mecanismo y criterio de refutación antes de mirar los datos.
Recursos
Research
Libros
- White, H. (2000): A Reality Check for Data Snooping. Econometrica 68(5), 1097-1126
- Bailey, Borwein, López de Prado y Zhu (2014): Pseudo-Mathematics and Financial Charlatanism. Notices of the AMS 61(5), 458-471
- Harvey, Liu y Zhu (2016): … and the Cross-Section of Expected Returns. Review of Financial Studies 29(1), 5-68