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.

Fig. — Cuánto infla el Sharpe anual cada sesgo provocado en los datos sintéticos del nivel 2: sin el sesgo (círculo) y con él (cuadrado)
Lecciones del nivel 2
  1. 2.1 · Estadística y econometría para trading

    1. 2.1.1 Contrastes de hipótesis, p-valores, bootstrap y regresión con errores robustos
    2. 2.1.2 Estacionariedad: ADF, KPSS, diferenciación y diferenciación fraccional
    3. 2.1.3 Modelos ARMA y ARIMA: autocorrelación simple (ACF) y parcial (PACF)
    4. 2.1.4 Modelos de volatilidad: ARCH, GARCH, EGARCH y GJR con la librería arch
    5. 2.1.5 Cointegración: Engle-Granger, Johansen y vida media de la reversión
    6. 2.1.6 Filtro de Kalman para hedge ratios dinámicos
    7. 2.1.7 Detección de regímenes: modelos de Markov ocultos y cambios estructurales
    8. 2.1.8 El problema de las pruebas múltiples: Bonferroni, FDR y el «factor zoo»
  2. 2.2 · Anatomía de un sistema de trading

    1. 2.2.1 Hipótesis de ventaja: riesgo, comportamiento, estructura e información
    2. 2.2.2 Universo, señal, filtro, entrada, salida, sizing y gestión del riesgo
    3. 2.2.3 Frecuencias: intradía, swing y posicional, y la capacidad de una estrategia
    4. 2.2.4 Diario de investigación y registro de experimentos contra el data snooping
  3. 2.3 · Familias de estrategias

    1. 2.3.1 Seguimiento de tendencia y momentum
    2. 2.3.2 Reversión a la media: RSI y Bollinger, reversión intradía y huecos
    3. 2.3.3 Pairs trading y arbitraje estadístico: distancia, cointegración y Kalman
    4. 2.3.4 Carry en divisas, futuros y bonos; estacionalidad y efectos calendario
    5. 2.3.5 Factores (value, calidad, baja volatilidad, tamaño) y estrategias de eventos
    6. 2.3.6 Introducción al market making: inventario, spread y selección adversa (Avellaneda-Stoikov)
    7. 2.3.7 Estrategias específicas de cripto: momentum, arbitraje de funding y basis
  4. 2.4 · Backtesting: motores y paradigmas

    1. 2.4.1 Backtest vectorizado desde cero: matrices, desfase, costes y límites
    2. 2.4.2 Backtest por eventos desde cero: bucle, cartera, órdenes y fills
    3. 2.4.3 vectorbt: barridos masivos de parámetros y simulación de carteras
    4. 2.4.4 backtesting.py y backtrader: dos motores didácticos y su estado
    5. 2.4.5 NautilusTrader: arquitectura, datos, estrategias y el mismo código en backtest y en real
    6. 2.4.6 LEAN/QuantConnect y Zipline Reloaded; bt para asignación de activos
    7. 2.4.7 Modelado de costes: comisiones, horquilla, impacto, financiación y préstamo de títulos
    8. 2.4.8 Modelado de fills: órdenes limitadas, cola, fills parciales, huecos y stops
  5. 2.5 · Sesgos y validación

    1. 2.5.1 Sesgos de backtest: anticipación, supervivencia, selección y narrativa
    2. 2.5.2 In-sample y out-of-sample: walk-forward anclado y móvil
    3. 2.5.3 Validación cruzada combinatoria purgada para estrategias
    4. 2.5.4 Deflated Sharpe Ratio y Probability of Backtest Overfitting (PBO)
    5. 2.5.5 Monte Carlo: remuestreo de trades, bootstrap de bloques y permutaciones de señales
    6. 2.5.6 Sensibilidad de parámetros: mesetas frente a picos, robustez entre activos y periodos
    7. 2.5.7 Datos sintéticos para pruebas de estrés: GBM, GARCH y bootstrapping
    8. 2.5.8 Checklist de «backtest honesto» antes de pasar a paper trading
  6. 2.6 · Métricas de rendimiento

    1. 2.6.1 CAGR, Sharpe, Sortino, Calmar, Omega, drawdown y Ulcer Index
    2. 2.6.2 Win rate, payoff, expectativa, alpha, beta, information ratio, turnover y capacidad
    3. 2.6.3 Informes con QuantStats o pyfolio-reloaded y tearsheets propios

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.

Sesgos de backtestInformaciónUniversoElecciónInterpretaciónMisma barraMuestra completaSupervivenciaSelecciónData snoopingNarrativa
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]
SesgoQué se le da al pasadoDónde entraSíntomaPrueba
Anticipación (misma barra)El precio con el que se decideEjecuciónSharpe enorme en señales rápidasRetrasar una sesión más
Anticipación (muestra completa)Medias, máximos o escalas del futuroPreparación de datosLa señal cambia al añadir datosCortar la historia
SupervivenciaLa lista de quién sigue vivo hoyUniversoSin quiebras ni deslistadosUniverso point-in-time
SelecciónLa mejor de muchas variantesElecciónGran caída fuera de muestraContar los intentos
Data snoopingLa muestra de validación, mirada muchas vecesIteración«Validado» y luego planoMuestra final intacta
NarrativaUna explicación hecha a medidaInterpretaciónHistoria perfecta, sin refutaciónHipó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).

Cierre de tSeñal y ordenEjecución en t+1Gana r de t+1
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).

IN [01]python
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())
OUT [01]Futuros y una cartera con volatilidad objetivo
(6539, 24) 2000-01-03 2025-12-31

retraso 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.

IN [02]python
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}")
OUT [02]La misma señal, dos ejecuciones
Ejecución en t+1:          Sharpe 1.00
Ejecución en la misma barra: Sharpe 3.16

El 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:

IN [03]python
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)
OUT [03]Cuanto más rápida la señal, mayor el engaño
                           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.92

Con 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:

IN [04]python
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}")
OUT [04]z-puntuación con toda la muestra frente a z expandida
muestra completa: Sharpe 0.67
       expandida: Sharpe -0.65

La 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.

IN [05]python
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}")
OUT [05]La prueba del corte
 f_total: 2.975494
   f_exp: 0.000000
 f_tsmom: 0.000000

La 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 %.

IN [06]python
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())
OUT [06]Rentabilidades con deslistados y pertenencia por fecha
115 empresas pasaron por el índice
evento
adquisicion_efectivo    4
deslistado_quiebra      4
exclusion_voluntaria    1
fusion_acciones         1

Dos 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.

IN [07]python
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)))
OUT [07]Índice point-in-time frente a supervivientes
  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']
Fig. — Índice equiponderado del universo sintético, 2006-2025: point-in-time con deslistados frente a los 100 miembros de hoy proyectados hacia atrás

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.

IN [08]python
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}")
OUT [08]Mil estrategias, dos mitades
            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.07
Fig. — Las 10 mejores de 1.000 estrategias sintéticas en 2022-2023 y su Sharpe en 2024-2025

El 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 252/501≈0,71\sqrt{252/501} \approx 0{,}71: 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 γ≈0,5772\gamma \approx 0{,}5772 (la constante de Euler-Mascheroni) y Φ−1\Phi^{-1} la inversa de la normal:

E[max⁡nSR^n]≈σSR[(1−γ) Φ−1 ⁣(1−1N)+γ Φ−1 ⁣(1−1Ne)]\mathbb{E}\left[\max_n \widehat{SR}_n\right] \approx \sigma_{SR}\left[(1-\gamma)\,\Phi^{-1}\!\left(1-\tfrac{1}{N}\right)+\gamma\,\Phi^{-1}\!\left(1-\tfrac{1}{N e}\right)\right]
IN [09]python
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}")
OUT [09]El máximo esperado del ruido
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.91

Con 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:

IN [10]python
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")
OUT [10]Filtrar con las dos mitades
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 real

Con 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):

PreguntaRespuesta 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.

SesgoSin sesgoCon sesgoDefensa automática
Misma barra (momentum 252)1,003,16Prueba del retraso
Muestra completa (reversión)−0,650,67Prueba del corte
Supervivencia (índice)0,380,44Universo point-in-time y deslistados
Selección (top 10 de 1.000)−0,07 fuera2,3 dentroContar 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

  1. 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
IN [11]python
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)
OUT [11]Prueba del retraso con varios retrasos
0    10.95
1     0.55
2     0.50
3     0.54
4     0.58
5     0.53
Name: Sharpe, dtype: float64

De 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).

  1. 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
IN [12]python
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%}")
OUT [12]Point-in-time sin rentabilidades de deslistado
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.

  1. 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

  1. 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.

  2. ¿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.

  3. 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.

  4. 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.

  5. ¿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