---
title: "2.5.1 Sesgos de backtest: anticipación, supervivencia y más"
description: "Un museo de sesgos con datos sintéticos: ejecutar en la misma barra, normalizar con toda la muestra, supervivencia, el mejor de mil y la narrativa."
url: "https://pythonparatrading.com/curso-python-trading/nivel-2/sesgos-de-backtest/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-2/sesgos-de-backtest.png"
---

# 2.5.1 · 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.

- Curso: [Nivel 2: Estrategias sistemáticas y backtesting riguroso](https://pythonparatrading.com/curso-python-trading/nivel-2/)
- Módulo: 2.5 · Sesgos y validación
- Duración: 60 min
- Requisitos: [1.4.4 · Universos sin sesgo de supervivencia: deslistados, fusiones y cambios de símbolo](https://pythonparatrading.com/curso-python-trading/nivel-1/sesgo-de-supervivencia-y-point-in-time/), [1.7.4 · Multi-timeframe sin anticipación](https://pythonparatrading.com/curso-python-trading/nivel-1/multi-timeframe-sin-look-ahead/), [0.7.2 · Probabilidad y distribuciones, correlación frente a causalidad y regresión simple](https://pythonparatrading.com/curso-python-trading/nivel-0/probabilidad-distribuciones-y-regresion/)

## Al terminar esta lección

- Reconocer los cinco sesgos que más inflan un backtest (anticipación, supervivencia, selección, data snooping y narrativa) y en qué paso del proceso entra cada uno.
- Provocar y medir el sesgo de anticipación de dos formas: ejecutar en la barra de la señal y normalizar con estadísticos de toda la muestra.
- Cuantificar el sesgo de supervivencia comparando un índice point-in-time con los miembros de hoy proyectados hacia atrás.
- Medir cuánto se degrada el mejor de mil candidatos fuera de muestra y por qué usar la muestra de validación como filtro la convierte en muestra de entrenamiento.
- Aplicar pruebas automáticas (retrasar la señal, cortar la historia) y una lista de control contra el sesgo de narrativa.

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

```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 |

<aside class="callout" data-kind="aviso">

Los datos son sintéticos y los efectos están plantados: el momentum de series temporales de los futuros (Sharpe
≈ 0,15 por mercado, más la prima de la renta variable en los índices), las quiebras del universo de acciones y las
1.000 estrategias de las que solo cinco tienen ventaja están documentados en el README del nivel 2. Por eso se puede decir cuánto de cada
resultado es sesgo. Ninguna cifra de esta lección es evidencia de que una estrategia funcione en un mercado real.

</aside>

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

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

```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())
```
```output
(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.

```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}")
```
```output
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:

```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)
```
```output
                           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.

<aside class="callout" data-kind="riesgo">

La misma barra no es solo `shift(0)`. Aparece cuando se usan el máximo o el mínimo del día para decidir una entrada
dentro de ese día, cuando una barra diaria se une con un dato publicado esa misma noche (resultados tras el cierre,
lección 1.5.1), cuando se mezclan marcos temporales sin desplazar la barra larga (lección 1.7.4) o cuando una orden
limitada «se ejecuta» porque el precio tocó su nivel, sin cola ni horquilla (lección 2.4.8).

</aside>

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

```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}")
```
```output
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.

```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}")
```
```output
 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).

<aside class="callout" data-kind="nota">

Hay un caso parecido que no es anticipación: el ajuste proporcional hacia atrás de las series continuas (y el de
dividendos de las acciones) cambia los precios del pasado cada vez que hay un roll nuevo, pero no cambia las
**rentabilidades**. Una señal que solo use rentabilidades o cocientes de precios pasa la prueba del corte; una que use
niveles absolutos (`precio > 100`) no, y además no tiene sentido en una serie ajustada (lección 1.4.6).

</aside>

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

```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())
```
```output
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.

```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)))
```
```output
  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.* Dos curvas de capital mensuales que empiezan en 1 en enero de 2006. La de los miembros de hoy (destacada) termina en 3,83, un 6,95 % anual; la point-in-time, con las quiebras y sus rentabilidades de deslistado, en 3,05, un 5,75 % anual. Las dos caen en 2008 y en 2020; la distancia crece poco a poco y se abre más desde 2019.

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.

<aside class="callout" data-kind="nota">

En datos reales, la lista point-in-time de un índice y las rentabilidades de deslistado son productos de pago o
requieren reconstruir la historia a partir de los anuncios de cambios (lecciones 1.3.4 y 1.4.4). Si un proveedor
gratuito no las da, la cifra de este ejemplo es la razón para no fiarse del backtest de una cartera de acciones hecho
con él.

</aside>

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

```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}")
```
```output
            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.* Gráfico de pendientes: diez líneas que van del Sharpe de 2022-2023, entre 1,88 y 2,98, al de 2024-2025, entre −1,47 y 0,92. La media (destacada) cae de 2,27 a −0,07. Las dos estrategias con ventaja real, e0133 y e0518, van de 2,98 a −0,04 y de 2,28 a 0,85.

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 $\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
$\gamma \approx 0{,}5772$ (la constante de Euler-Mascheroni) y $\Phi^{-1}$ la inversa de la normal:

$$
\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]
$$

```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}")
```
```output
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:

```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")
```
```output
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):

| 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

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

```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)
```
```output
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).

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

```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%}")
```
```output
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.

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

## 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?

   - A. Que el momentum funciona mejor a corto plazo
   - 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
   - C. Que los costes de transacción son menores con retraso 0
   - D. Que el ajuste proporcional de las series continuas favorece el retraso 0

   *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)?

   - A. Si la señal tiene demasiados parámetros
   - B. Si algún valor de la señal en una fecha cambia al añadir datos posteriores, es decir, si usa el futuro
   - C. Si la señal es estacionaria
   - D. Si hay huecos en los datos

   *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?

   - A. De que los miembros de hoy son empresas más grandes
   - 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
   - C. De los dividendos
   - D. De un error de redondeo

   *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?

   - A. Las siete: superar dos periodos es una validación
   - 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
   - C. Exactamente cinco
   - D. No se puede saber

   *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?

   - A. Usar datos de noticias en una estrategia
   - B. Construir después de ver el resultado una explicación convincente que lo hace parecer inevitable, y tomarla como prueba
   - C. Preferir estrategias de tendencia
   - D. Publicar solo las estrategias que funcionan

   *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

- [Introduccion al Quant Research](https://pythonparatrading.com/quant-research/) · Research-0023 · 8 min

### Libros

- [White, H. (2000): A Reality Check for Data Snooping. Econometrica 68(5), 1097-1126](https://doi.org/10.1111/1468-0262.00152) · doi.org
- [Bailey, Borwein, López de Prado y Zhu (2014): Pseudo-Mathematics and Financial Charlatanism. Notices of the AMS 61(5), 458-471](https://doi.org/10.1090/noti1105) · doi.org
- [Harvey, Liu y Zhu (2016): … and the Cross-Section of Expected Returns. Review of Financial Studies 29(1), 5-68](https://doi.org/10.1093/rfs/hhv059) · doi.org

### Datos

- [Datos del nivel 2: futuros continuos, 24 mercados, 2000-2025 (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/futuros2_continuos.parquet) · Descarga · futuros2_continuos.parquet
- [Datos del nivel 2: pertenencia point-in-time al índice (CSV)](https://pythonparatrading.com/curso/datos/nivel-2/universo2_miembros.csv) · Descarga · universo2_miembros.csv
- [Datos del nivel 2: 1.000 estrategias de ruido y cinco con ventaja (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/ruido2_estrategias.parquet) · Descarga · ruido2_estrategias.parquet

Lección anterior: [2.4.8 · Modelado de fills: órdenes limitadas, cola, fills parciales, huecos y stops](https://pythonparatrading.com/curso-python-trading/nivel-2/modelado-de-fills/)

Lección siguiente: [2.5.2 · In-sample y out-of-sample: walk-forward anclado y móvil](https://pythonparatrading.com/curso-python-trading/nivel-2/in-sample-out-of-sample-y-walk-forward/)
