---
title: "5.1.1 Por qué el ML financiero es distinto"
description: "Poca señal, mucho ruido, relaciones que cambian y competidores que aprenden: el techo de un modelo medido sobre un panel con la respuesta conocida."
url: "https://pythonparatrading.com/curso-python-trading/nivel-5/por-que-el-ml-financiero-es-distinto/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-5/por-que-el-ml-financiero-es-distinto.png"
---

# 5.1.1 · Por qué el machine learning financiero es distinto

> En visión artificial un modelo acierta el 99 % de las veces; en un panel de acciones, el modelo perfecto acierta el 51 %. Esta lección mide ese techo con un oráculo que conoce el rendimiento esperado verdadero y enseña las cuatro diferencias que obligan a cambiar el método: la relación señal/ruido, la escasez de datos independientes, la no estacionariedad y la competencia.

- Curso: [Nivel 5: Machine learning y deep learning (Keras/PyTorch)](https://pythonparatrading.com/curso-python-trading/nivel-5/)
- Módulo: 5.1 · Fundamentos de ML aplicados a finanzas
- Duración: 60 min
- Requisitos: [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/), [2.1.8 · El problema de las pruebas múltiples: Bonferroni, FDR y el «factor zoo»](https://pythonparatrading.com/curso-python-trading/nivel-2/pruebas-multiples-bonferroni-fdr-y-factor-zoo/), [2.5.1 · Sesgos de backtest: anticipación, supervivencia, selección y narrativa](https://pythonparatrading.com/curso-python-trading/nivel-2/sesgos-de-backtest/), [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/)

## Al terminar esta lección

- Medir el techo de predictibilidad de un panel con un oráculo: IC semanal, acierto, varianza explicada y su variación entre semanas.
- Explicar por qué cientos de miles de filas son pocos datos independientes y calcular cuántos años hacen falta para distinguir una señal del ruido.
- Mostrar con los coeficientes verdaderos que la relación entre features y rendimientos cambia con el régimen y en el tiempo.
- Reconocer el sobreajuste y la fuga comparando el IC dentro y fuera de muestra con el del oráculo.
- Cuantificar cuánto de la señal se pierde por llegar tarde y relacionarlo con la competencia y el decaimiento de las anomalías publicadas.

## Lo que cambia respecto al ML de otros campos

Las técnicas del nivel —árboles, boosting, redes, transformers— vienen de campos donde funcionan muy bien:
clasificar imágenes, traducir textos, recomendar productos. Un gato es un gato hoy y dentro de diez años, la etiqueta
está en la imagen y hay millones de ejemplos. Ninguna de esas tres cosas es cierta cuando se predice un rendimiento.

| Rasgo | Visión, lenguaje | Rendimientos financieros |
|---|---|---|
| Relación señal/ruido | Alta: la etiqueta casi está en los datos | Bajísima: el modelo perfecto explica décimas de punto de la varianza |
| Estacionariedad | El fenómeno no cambia | Las relaciones cambian con el régimen y desaparecen al explotarse |
| Datos | Millones de ejemplos independientes; se pueden generar más | Una sola historia; observaciones correlacionadas en el tiempo y entre activos |
| Adversario | Ninguno: el gato no reacciona al clasificador | Otros participantes compiten por la misma señal y la gastan |
| Coste del error | Simétrico, medido con acierto o pérdida | Asimétrico y económico: costes, colas, capacidad |

La consecuencia práctica es que casi todo el método cambia: cómo se etiqueta (módulo 5.3), cómo se valida (5.4), qué
se mide (5.1.3) y qué resultado se considera creíble. Esta lección mide cada diferencia sobre datos donde se conoce la
respuesta.

## El techo: un oráculo que apenas acierta

El banco de pruebas del nivel es `panel5`: 400 acciones ficticias, 730 semanas de 2012 a 2025, 22 features
conocidas al cierre de cada viernes y el rendimiento de la semana siguiente. Son datos **sintéticos** con un modelo
generador publicado en el README: el rendimiento esperado extra de cada acción y semana, `alfa_verdadero`, sale de
unos efectos plantados (momentum, reversión, value, sorpresa de resultados, baja volatilidad, sentimiento) cuyo
tamaño se eligió para imitar al de un mercado real. Esa columna es el **oráculo**: la mejor predicción posible con la
información de cada viernes. Ningún modelo puede hacerlo mejor fuera de muestra.

```python
import numpy as np
import pandas as pd

f = pd.read_parquet("panel5_features.parquet")
et = pd.read_parquet("panel5_etiquetas.parquet")
ver = pd.read_parquet("panel5_verdad.parquet")
d = (f.merge(et[["fecha", "ticker", "ret_sig_semana"]],
             on=["fecha", "ticker"])
      .merge(ver[["fecha", "ticker", "alfa_verdadero"]],
             on=["fecha", "ticker"])
      .dropna(subset=["ret_sig_semana"]))
print(d.shape, d["fecha"].nunique(), "semanas")
d[["fecha", "ticker", "mom_12_1", "alfa_verdadero",
   "ret_sig_semana"]].head(3)
```
```output
(291600, 27) 729 semanas
       fecha ticker  mom_12_1  alfa_verdadero  ret_sig_semana
0 2012-01-13   BAGO   -0.2174       -0.000268       -0.059579
1 2012-01-13   BAKA    0.0510       -0.000103       -0.024737
2 2012-01-13   BAKU   -0.2482       -0.001394       -0.072296
```

La medida natural de una predicción transversal es el **coeficiente de información (IC)**: la correlación de rangos
(Spearman) entre la predicción y el rendimiento realizado, calculada cada semana entre las 400 acciones. Se usa el
rango porque lo que importa para formar una cartera es el orden, y porque los rendimientos tienen colas gruesas que
dominarían una correlación de Pearson. La lección 5.1.3 lo desarrolla; aquí basta con la función:

```python
def ic_semanal(df, col, y="ret_sig_semana"):
    """Correlación de rangos de col con y, semana a semana."""
    s = df[["fecha", col, y]].dropna()
    r = s.groupby("fecha")[[col, y]].rank()
    r = r - r.groupby(s["fecha"]).transform("mean")
    num = (r[col] * r[y]).groupby(s["fecha"]).sum()
    den = ((r[col] ** 2).groupby(s["fecha"]).sum()
           * (r[y] ** 2).groupby(s["fecha"]).sum()) ** 0.5
    return num / den


def resumen(ic):
    return pd.Series({"ic_medio": ic.mean(), "desv": ic.std(),
                      "t": ic.mean() / ic.std() * np.sqrt(len(ic)),
                      "semanas_pos": (ic > 0).mean()})


ic_or = ic_semanal(d, "alfa_verdadero")
resumen(ic_or).round(4)
```
```output
ic_medio       0.0292
desv           0.0929
t              8.4714
semanas_pos    0.6461
dtype: float64
```

El modelo perfecto tiene un IC medio de 0,029. Con 729 semanas, eso es muy significativo (t = 8,5), pero semana a
semana la desviación típica del IC (0,093) es tres veces su media: en un 35 % de las semanas el oráculo ordena las
acciones **peor que el azar**. La figura de la cabecera dibuja la distribución: una campana centrada casi en cero,
con la media apenas desplazada a la derecha.

El acierto y la varianza explicada cuentan lo mismo con otras palabras:

```python
med = d.groupby("fecha")[["ret_sig_semana", "alfa_verdadero"]]
dev = d[["ret_sig_semana", "alfa_verdadero"]] - med.transform(
    "median")
acierto = ((dev["ret_sig_semana"] > 0)
           == (dev["alfa_verdadero"] > 0)).mean()
print(f"Acierto frente a la mediana: {acierto:.2%}")
print(f"Varianza explicada (IC²): {ic_or.mean() ** 2:.3%}")
disp = d.groupby("fecha")["ret_sig_semana"].std().mean()
sd_alfa = d.groupby("fecha")["alfa_verdadero"].std().mean()
print(f"Dispersión semanal: {disp:.2%}; "
      f"desv. del alfa: {sd_alfa * 1e4:.1f} pb")
```
```output
Acierto frente a la mediana: 50.95%
Varianza explicada (IC²): 0.085%
Dispersión semanal: 3.62%; desv. del alfa: 12.3 pb
```

Un 50,95 % de acierto y un 0,09 % de la varianza. La dispersión de los rendimientos entre acciones en una semana
típica es del 3,6 %; la del rendimiento esperado, de 12 puntos básicos: treinta veces menos. Para comparar: un
clasificador de imágenes aceptable acierta más del 95 %. En finanzas, un modelo que acierte el 55 % de forma
estable en un panel como este no es bueno: es imposible.

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

El techo de los datos no es arbitrario. Gu, Kelly y Xiu (2020) comparan modelos lineales, árboles y redes sobre
acciones de EE. UU. de 1957 a 2016: en la versión de trabajo del artículo (NBER w25398, tabla 1), el mejor R²
mensual fuera de muestra a nivel de acción es del 0,40 %, y una regresión con todas las variables sin regularizar
da −3,46 %. Un R² de décimas de punto es lo normal en el mundo real; el del oráculo de `panel5` está en esa escala.

</aside>

Agrupar por deciles del oráculo enseña dónde está la señal y cuánto ruido la rodea:

```python
g = d.groupby("fecha")
exceso = d["ret_sig_semana"] - g["ret_sig_semana"].transform("mean")
decil = g["alfa_verdadero"].transform(
    lambda s: pd.qcut(s, 10, labels=False))
(exceso.groupby(decil).agg(["mean", "std"]) * 1e4).round(1)
```
```output
                mean    std
alfa_verdadero
0              -14.9  424.3
1              -13.4  384.3
2               -3.9  364.8
3               -1.6  363.0
4               -0.4  354.4
5               -1.4  350.7
6                2.3  349.6
7                6.2  345.7
8                7.2  345.3
9               19.9  369.1
```

*Fig. — Rendimiento de la semana siguiente respecto a la media del panel por decil del oráculo, en puntos básicos, 2012-2025.* Diez barras crecientes del decil 1 al 10: de −14,9 pb a +19,9 pb por semana (el decil 10, destacado). Un rótulo recuerda que la desviación típica dentro de cada decil es de 345 a 424 pb, unas veinte veces la diferencia entre medias.

La relación es monótona: el decil más alto rinde unos 20 pb por semana más que la media y el más bajo unos 15 pb
menos. Pero dentro de cada decil la desviación típica es de 345 a 424 pb. La diferencia entre extremos, unos 35 pb,
es una décima parte del ruido de una sola acción en una sola semana. Por eso una predicción financiera no se evalúa
con un caso ni con una semana, sino con muchas apuestas agregadas, y por eso la cartera, no la predicción, es la
unidad de medida (lección 5.1.3).

## Datos escasos: 291.600 filas no son 291.600 pruebas

El panel tiene casi 300.000 filas con etiqueta. Para un modelo de ML parecen muchas; para la inferencia son
bastantes menos, por dos motivos. El primero: las features lentas apenas cambian de una semana a la siguiente, así
que la fila de la semana 300 de una acción repite casi la información de la 299. La correlación de rangos de cada
feature consigo misma una semana después lo mide:

```python
def persistencia(col):
    p = d.pivot(index="fecha", columns="ticker", values=col)
    p = p.rank(axis=1)
    return p.corrwith(p.shift(1), axis=1).mean()


cols = ["valor_bp", "mom_12_1", "vol_63d", "sue", "ruido_3",
        "ret_1s", "ruido_1", "alfa_verdadero"]
pd.Series({c: persistencia(c) for c in cols}).round(3)
```
```output
valor_bp          0.999
mom_12_1          0.975
vol_63d           0.977
sue               0.941
ruido_3           0.977
ret_1s            0.019
ruido_1          -0.001
alfa_verdadero    0.515
dtype: float64
```

El value (`valor_bp`) tiene una persistencia de 0,999: en 14 años, el orden de las acciones por value cambia tan
despacio que aporta, a efectos prácticos, unas pocas decenas de observaciones independientes de su efecto, no
cientos de miles. El momentum, la volatilidad y el SUE están entre 0,94 y 0,98. Solo la reversión de una semana
(`ret_1s`) y el ruido i.i.d. se renuevan cada semana. Y `ruido_3`, un AR(1) con ρ = 0,98 sin ningún efecto, es tan
persistente como el momentum: una feature lenta y sin efecto puede correlacionar con el rendimiento durante años por
pura casualidad.

El segundo motivo es que todas las acciones de una semana comparten el mismo mercado y los mismos sectores: el
ruido de una semana no se cancela entre acciones tanto como si fueran independientes. Juntos, los dos efectos hacen
que contrastes con 14 años de datos tengan menos potencia de la que sugiere el número de filas. La tabla de IC de
las 22 features, con lo que dice el modelo generador de cada una, lo pone a prueba:

```python
efecto = {"ret_1s": "sí", "mom_12_1": "sí", "vol_63d": "sí",
          "valor_bp": "sí", "sue": "sí", "sentimiento": "sí",
          "volumen_rel": "interacción",
          "dias_desde_informe": "interacción",
          "revision_analistas": "FUGA"}
feats = [c for c in f.columns
         if c not in ("fecha", "ticker", "sector")]
tabla = pd.DataFrame({c: resumen(ic_semanal(d, c)) for c in feats}).T
tabla["efecto"] = [efecto.get(c, "no") for c in tabla.index]
tabla.sort_values("t")[["ic_medio", "t", "efecto"]].round(4)
```
```output
                    ic_medio        t       efecto
ret_1s               -0.0159  -4.2881           sí
rsi_14               -0.0083  -2.6280           no
ret_1m               -0.0077  -2.1404           no
ret_3m               -0.0050  -1.5616           no
ruido_1              -0.0024  -1.2751           no
dist_max_52s         -0.0031  -0.9992           no
volumen_rel          -0.0014  -0.7362  interacción
ruido_3              -0.0010  -0.5558           no
ruido_5              -0.0008  -0.4661           no
dias_desde_informe   -0.0005  -0.2746  interacción
log_capitalizacion   -0.0005  -0.2103           no
vol_21d               0.0020   0.3569           no
vol_63d               0.0032   0.5593           sí
ruido_2               0.0015   0.7851           no
ruido_4               0.0044   1.8392           no
mom_12_1              0.0077   2.4404           sí
roe                   0.0057   2.7132           no
sentimiento           0.0066   3.0800           sí
sue                   0.0079   3.7415           sí
valor_bp              0.0095   3.9281           sí
ey                    0.0087   4.0399           no
revision_analistas    0.1743  92.1366         FUGA
```

Tres lecturas de la tabla:

- **Efectos verdaderos que no se ven.** `vol_63d` tiene efecto plantado y su IC da t = 0,6: la baja volatilidad
  queda tapada porque las acciones volátiles tienen más beta y el mercado sube de media (el README lo mide con
  Fama-MacBeth). `sentimiento` y `sue`, con efecto, quedan por debajo de `ey`.
- **Features sin efecto propio que parecen buenas.** `ey` (t = 4,0) y `roe` (t = 2,7) heredan el efecto del value y
  del SUE, con los que están correlacionadas; `rsi_14` (t = −2,6) y `ret_1m` (t = −2,1), el de la reversión. Un
  contraste univariante no distingue causa de parecido. Las pruebas múltiples de la lección 2.1.8 corrigen el número
  de intentos, pero no esta confusión.
- **La fuga.** `revision_analistas` da un IC de 0,174 con t = 92: seis veces el techo del oráculo. No es una gran
  feature: el proveedor la fecha al inicio de la semana que cubre y contiene los dos primeros días del rendimiento
  que se predice. Un resultado por encima del techo es un error, no un hallazgo (la lección 5.2.2 enseña a auditar
  las fechas).

Hay una cuenta sencilla que mide la escasez de datos. Si una señal tiene un IC medio $\overline{IC}$ con desviación
semanal $s$, el estadístico t tras $n$ semanas es $\overline{IC}\sqrt{n}/s$, y para llegar a t = 2 hacen falta
$n = (2s/\overline{IC})^2$ semanas. Para una estrategia con Sharpe anual $\mathrm{SR}$, los años necesarios son
$(2/\mathrm{SR})^2$:

```python
sem = tabla.loc[["valor_bp", "sue", "sentimiento", "mom_12_1"],
                ["ic_medio", "desv"]].astype(float)
sem.columns = ["ic", "desv"]
sem.loc["oráculo"] = [ic_or.mean(), ic_or.std()]
sem["años_para_t2"] = (2 * sem["desv"] / sem["ic"]) ** 2 / 52
print(sem.round(3))
sharpe = pd.Series([0.3, 0.5, 1.0, 2.0], name="sharpe_anual")
print((2 / sharpe) ** 2)
```
```output
                ic   desv  años_para_t2
valor_bp     0.009  0.065         3.634
sue          0.008  0.057         4.006
sentimiento  0.007  0.058         5.911
mom_12_1     0.008  0.085         9.416
oráculo      0.029  0.093         0.781
0    44.444444
1    16.000000
2     4.000000
3     1.000000
Name: sharpe_anual, dtype: float64
```

El oráculo, que combina todos los efectos, se distingue del ruido en menos de un año. Una feature suelta con un IC
de 0,008 o 0,009 necesita de tres años y medio a seis, y el momentum, cuyo IC varía más entre semanas, más de nueve.
Y son cotas por abajo: la fórmula trata las semanas como independientes, y con features tan persistentes no lo son.
Una estrategia con Sharpe anual de 0,5
—buena en el mundo real— necesita 16 años de historia para dar t = 2, y eso suponiendo que la relación no cambie en
esos 16 años. Que es la siguiente diferencia.

## No estacionariedad: la relación cambia

En visión artificial, la función que lleva de los píxeles a la etiqueta es fija. En `panel5`, el coeficiente de cada
efecto cambia cada semana: es su valor base multiplicado por un factor de régimen y por un término AR(1) que lo
debilita y a veces lo invierte. Además, el momentum se multiplica por −2,5 en los **rebotes** tras una crisis: es el
*crash* del momentum, documentado en mercados reales (las acciones que más cayeron son las que más rebotan). El
archivo `panel5_coef_verdad.csv` da el coeficiente verdadero de cada semana:

```python
coef = pd.read_csv("panel5_coef_verdad.csv", parse_dates=["fecha"])
coef = coef.set_index("fecha")
c_mom = coef["c_mom"] * 1e4          # pb por semana y unidad
print(c_mom.groupby(coef["regimen"]).agg(["mean", "count"])
      .round(2))
ic_mom = ic_semanal(d, "mom_12_1")
anual = pd.DataFrame({"c_mom_pb": c_mom, "ic_mom": ic_mom})
anual.groupby(anual.index.year).mean().round(4)
```
```output
          mean  count
regimen
calma     4.71    576
estres    2.02     89
rebote  -10.44     65
       c_mom_pb  ic_mom
fecha
2012     4.9549  0.0133
2013     5.6347  0.0175
2014     5.5424  0.0207
2015     3.4455  0.0153
2016     0.7704 -0.0134
2017     6.0316  0.0188
2018     4.5862  0.0050
2019     0.7888 -0.0069
2020    -2.0040 -0.0101
2021     4.2767  0.0003
2022    -1.4078  0.0177
2023     4.0448  0.0057
2024     3.8732  0.0134
2025     2.0747  0.0107
```

*Fig. — Coeficiente verdadero del momentum en el panel, semana a semana, en puntos básicos por semana y unidad de la feature: cambia con el régimen y se invierte en los rebotes.* Línea semanal de 2012 a 2025 que oscila casi siempre entre 0 y 9 pb, con caídas bruscas por debajo de cero en los rebotes de 2016, 2019, 2020, 2022 y 2025. El mínimo, destacado, es de −25,0 pb en la semana del 15/05/2020, en el rebote de 2020.

En calma (576 semanas), el momentum rinde 4,7 pb por semana y unidad; en estrés, 2,0; en rebote, −10,4. El año 2020
tiene un coeficiente medio negativo (−2,0) por el rebote de marzo a junio, y 2022 otro (−1,4). El IC medido del
momentum sigue al coeficiente con ruido: es negativo en 2016, 2019 y 2020, años de coeficiente bajo o negativo, pero
en 2022, con el coeficiente verdadero negativo, sale positivo (0,018): un año de datos no basta ni para acertar el
signo. Un modelo entrenado con 2012-2015 aprende un momentum «bueno» que en 2016 y en
2020 hace daño, y nada en los datos de entrenamiento le avisa.

Hay dos tipos de cambio y conviene separarlos desde ahora. La **deriva de datos** cambia la distribución de las
features (una feature se desplaza o se dispersa), y la **deriva de concepto** cambia la relación entre features y
etiqueta, como aquí. La segunda es la peligrosa, porque no se ve mirando las features: solo se ve en los resultados,
tarde. La lección 5.10.2 la detecta con datos donde se conoce la fecha de cada cambio; las ventanas móviles frente a
expansivas del reentrenamiento, en la 5.4.3.

## Sobreajuste: un modelo flexible aprende el ruido

Con poca señal, mucho ruido y relaciones que cambian, un modelo flexible encuentra patrones en el ruido con
facilidad. El experimento: tres modelos con las 21 features sin la fuga, transformadas a rango transversal
(lección 5.2.4), entrenados con 2012-2018 y evaluados en 2019-2025. La etiqueta es el rendimiento de la semana
siguiente menos su media transversal. Una ridge (lineal y regularizada), un LightGBM moderado (8 hojas, al menos
2.000 filas por hoja) y un LightGBM flexible (512 hojas, 5 filas por hoja, 400 árboles). El entrenamiento es corto
a propósito: el flexible tarda unos 15 segundos en CPU.

```python
import lightgbm as lgb
from sklearn.linear_model import Ridge

feats_x = [c for c in feats if c != "revision_analistas"]
X = d.groupby("fecha")[feats_x].rank(pct=True).sub(0.5).fillna(0)
y = d["ret_sig_semana"] - g["ret_sig_semana"].transform("mean")
d["y"] = y
ent = d["fecha"] < "2019-01-01"
pru = d["fecha"] >= "2019-01-08"     # una semana de margen

comun = dict(random_state=0, n_jobs=4, deterministic=True,
             force_row_wise=True, verbose=-1)
modelos = {
    "ridge": Ridge(alpha=10.0),
    "lgbm_moderado": lgb.LGBMRegressor(
        n_estimators=200, learning_rate=0.02, num_leaves=8,
        min_child_samples=2000, subsample=0.5, subsample_freq=1,
        **comun),
    "lgbm_flexible": lgb.LGBMRegressor(
        n_estimators=400, learning_rate=0.1, num_leaves=512,
        min_child_samples=5, **comun),
}


def ic_medio(pred, mascara):
    s = d.loc[mascara, ["fecha", "y"]].assign(p=pred)
    return ic_semanal(s, "p", "y").mean()


filas = {"oráculo": [ic_medio(d.loc[ent, "alfa_verdadero"], ent),
                     ic_medio(d.loc[pru, "alfa_verdadero"], pru)]}
for nombre, m in modelos.items():
    m.fit(X[ent], y[ent])
    filas[nombre] = [ic_medio(m.predict(X[ent]), ent),
                     ic_medio(m.predict(X[pru]), pru)]
pd.DataFrame(filas, index=["ic_2012_2018", "ic_2019_2025"]).T.round(4)
```
```output
               ic_2012_2018  ic_2019_2025
oráculo              0.0327        0.0254
ridge                0.0258        0.0190
lgbm_moderado        0.0658        0.0141
lgbm_flexible        0.9626       -0.0022
```

*Fig. — IC medio de tres modelos y del oráculo fuera de muestra (2019-2025, barras) y dentro de muestra (2012-2018, columna derecha): cuanto más flexible el modelo, más se aleja lo de dentro de lo de fuera.* Barras horizontales del IC fuera de muestra: oráculo 0,025, ridge 0,019 (destacada), LightGBM moderado 0,014 y LightGBM flexible −0,002. A la derecha, el IC dentro de muestra de cada uno: 0,033, 0,026, 0,066 y 0,963.

El oráculo marca el techo de cada periodo: 0,033 en entrenamiento y 0,025 en prueba. La ridge queda por debajo en
los dos (0,026 y 0,019): aprende poco y lo que aprende se mantiene. El LightGBM moderado da 0,066 dentro de
muestra, el doble del techo, y 0,014 fuera: ha aprendido algo de señal y bastante ruido. El flexible da 0,963 dentro
de muestra —ordena casi perfectamente rendimientos que son, en un 99,9 %, ruido— y −0,002 fuera. No es que el
boosting no sirva: la lección 5.5.3 lo ajusta con validación honesta. Es que en estos datos la flexibilidad sin
control se convierte en memoria del ruido mucho antes que en señal.

De aquí sale una regla de cordura que se usa en todo el nivel. Si se conoce, aunque sea de forma aproximada, cuánto
puede dar como máximo una señal:

```mermaid
flowchart TB
  R[Resultado] --> A{¿IC dentro muy por encima del techo?}
  A -- sí --> S[Sobreajuste]
  A -- no --> B{¿IC fuera por encima del techo?}
  B -- sí --> F[Fuga o error]
  B -- no --> C{¿Gana tras costes?}
  C -- no --> N[Señal sin valor]
  C -- sí --> V[Validar más: 5.4]
```

En datos reales no hay oráculo, pero sí órdenes de magnitud: el R² de la literatura, el Sharpe de las mejores
estrategias conocidas, el IC de las anomalías publicadas. Un resultado muy por encima de esa escala se investiga
como un error antes de celebrarse.

## Competencia: la señal caduca y se reparte

La última diferencia es que el mercado reacciona. Si una señal da dinero, otros la encuentran, la operan y la
gastan. Un primer efecto se ve en los datos: el horizonte de la señal es corto, y quien la explota tarde se queda con
menos. La celda usa el oráculo de cada semana para predecir el rendimiento de la semana siguiente, como siempre, y
también el de una, dos, tres y cuatro semanas después:

```python
a = d.pivot(index="fecha", columns="ticker",
            values="alfa_verdadero").rank(axis=1)
r = d.pivot(index="fecha", columns="ticker",
            values="ret_sig_semana")
retraso = {}
for k in range(5):
    ic_k = a.corrwith(r.shift(-k).rank(axis=1), axis=1).dropna()
    retraso[k] = resumen(ic_k)
print(pd.DataFrame(retraso).T.rename_axis("semanas_tarde").round(4))
for col in ("ret_1s", "valor_bp"):
    x = d.pivot(index="fecha", columns="ticker",
                values=col).rank(axis=1)
    ics = [x.corrwith(r.shift(-k).rank(axis=1), axis=1).mean()
           for k in range(4)]
    print(col, np.round(ics, 4))
```
```output
               ic_medio    desv       t  semanas_pos
semanas_tarde
0                0.0292  0.0929  8.4714       0.6461
1                0.0110  0.0900  3.2974       0.5701
2                0.0111  0.0875  3.4129       0.5695
3                0.0105  0.0907  3.1194       0.5565
4                0.0075  0.0875  2.2963       0.5462
ret_1s [-0.0159 -0.0001  0.0004 -0.0012]
valor_bp [0.0095 0.0089 0.0087 0.0087]
```

Con una semana de retraso, el IC baja de 0,029 a 0,011: se pierden casi dos tercios. Lo que queda no se agota
enseguida (0,011 y 0,010 con dos y tres semanas de retraso) porque es la parte lenta de la señal: value, momentum y
SUE cambian despacio, y el rendimiento esperado de dentro de tres semanas se parece al de la siguiente. Las dos
últimas líneas lo separan: el IC del value apenas se mueve con el retraso (de 0,0095 a 0,0087), y el de la reversión
de una semana (`ret_1s`, −0,016) desaparece por completo con el primer retraso. En un mercado real, ese reparto es el de la
competencia: las señales rápidas las cobran quienes llegan antes, y a un sistema semanal le queda la parte lenta.

A escala de años, el efecto lo midieron McLean y Pontiff (2016) con 97 variables publicadas en la literatura
académica que predecían rendimientos: el rendimiento de sus carteras es un 26 % menor fuera de la muestra del
artículo original y un 58 % menor después de su publicación. Los autores leen el 26 % como cota superior de la parte
debida a la minería de datos y la diferencia (32 puntos) como el efecto de los inversores que aprenden de la
publicación. Para el ML, la lección es doble: lo que funcionó en el pasado documentado es lo que más competidores
conocen, y un modelo que «descubre» una anomalía conocida no ha descubierto nada.

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

La competencia tiene una segunda cara: los costes. Una señal de horizonte corto obliga a rotar mucho la cartera.
Según el README del nivel, la cartera larga-corta por deciles del oráculo rota un 259 % semanal, y con 10 pb por lado
su Sharpe pasa de 1,78 bruto a 0,38 neto; la ridge con todas las features queda en negativo. La lección 5.1.3 lo
calcula y enseña a medir modelos por lo que dejan tras costes.

</aside>

## Qué cambia en el método

Las cuatro diferencias se traducen en un flujo de trabajo que es el esqueleto del nivel:

```mermaid
flowchart TB
  D[Datos point-in-time] --> E[Etiquetas y pesos]
  E --> V[Validación purgada]
  V --> M[Modelo y baseline]
  M --> K[Métricas económicas]
  K --> B[Backtest y DSR]
  B --> W[Vigilar la deriva]
```

- **Datos point-in-time** (5.2.2): cada feature, con la fecha en que se conoce. La fuga de `revision_analistas` no
  la detecta ninguna validación; solo la auditoría de fechas.
- **Etiquetas y pesos** (5.3): etiquetas que solapan en el tiempo significan menos muestras independientes; la
  unicidad y el bootstrap secuencial lo corrigen.
- **Validación purgada** (5.4.1-5.4.3): el k-fold aleatorio mezcla vecinos en el tiempo y promete aciertos que no
  existen.
- **Modelo y baseline** (5.5-5.8): cada modelo se compara con una referencia simple —una ridge, una feature sola, el
  HAR en volatilidad— y con el techo cuando se conoce.
- **Métricas económicas** (5.1.3) y **backtest con Deflated Sharpe** (5.4.4): un modelo se juzga por la cartera que
  produce, tras costes, y descontando cuántos experimentos se hicieron (lección 2.5.4).
- **Vigilar la deriva** (5.10.2): la relación cambiará; hay que saber cuándo.

## Ejercicios

1. Calcula el IC medio del oráculo por régimen (`calma`, `estres`, `rebote`) con la columna `regimen` de
   `panel5_coef_verdad.csv`. ¿En qué régimen es más difícil predecir aunque se conozca el rendimiento esperado?

**Solución.**

```python
reg = coef["regimen"].reindex(ic_or.index)
ic_or.groupby(reg).agg(["mean", "std", "count"]).round(4)
```
```output
           mean     std  count
regimen
calma    0.0286  0.0889    575
estres   0.0301  0.0948     89
rebote   0.0330  0.1223     65
```

El IC medio del oráculo es parecido en los tres regímenes (0,029 en calma, 0,030 en estrés y 0,033 en rebote),
pero no su ruido: la desviación semanal del IC sube de 0,089 en calma a 0,095 en estrés y a 0,122 en los rebotes,
donde el momentum cambia de signo y los rendimientos comunes dominan. En relación con su ruido, el rebote es el
régimen más difícil (0,033/0,122 = 0,27 frente a 0,32 en calma), y además solo tiene 65 semanas. Conocer el
rendimiento esperado no evita el ruido: lo que cambia entre regímenes es cuánto pesa la señal frente a él.

2. Con la fórmula $n = (2s/\overline{IC})^2$, calcula cuántas semanas necesitaría una señal con el IC medio de la
   ridge fuera de muestra (0,019) y una desviación semanal de 0,09 para dar t = 2, y cuántas para t = 3, el umbral
   que propone la literatura de pruebas múltiples (lección 2.1.8).

**Solución.**

```python
ic_r, s_r = 0.019, 0.09
for t_obj in (2, 3):
    n = (t_obj * s_r / ic_r) ** 2
    print(f"t = {t_obj}: {n:.0f} semanas ({n / 52:.1f} años)")
```
```output
t = 2: 90 semanas (1.7 años)
t = 3: 202 semanas (3.9 años)
```

Con t = 2 basta un año y medio largo; con t = 3, casi cuatro años. Y la cuenta supone que el IC es estable y que
las semanas son independientes: con relaciones que cambian y features persistentes, el tiempo real es mayor.

3. Un colega presenta un clasificador entrenado sobre `panel5` que predice si una acción quedará por encima de la
   mediana dentro de 21 sesiones, con un acierto del 59,7 % en validación cruzada de 5 bloques barajados. Explica
   por qué no te lo crees y qué pedirías.

**Solución.**

El oráculo, que conoce el rendimiento esperado verdadero, acierta un 51 %: un 59,7 % está nueve puntos por encima del
techo, así que hay un error en la validación o una fuga. Aquí es la validación: las etiquetas de 21 sesiones se
solapan unas cuatro semanas y las features lentas casi no cambian, así que al barajar, el modelo ve en entrenamiento
la misma acción una semana antes y después, con casi la misma etiqueta, y la memoriza. El README del nivel lo mide:
59,7 % con k-fold barajado, 50,1 % con purged k-fold y con walk-forward. Pediría la validación purgada con embargo o
un walk-forward (lecciones 5.4.1 y 5.4.2), la auditoría de las fechas de cada feature y el resultado en métricas
económicas tras costes (5.1.3).

## Autoevaluación

1. Un modelo entrenado sobre `panel5` da un IC medio fuera de muestra de 0,15. El oráculo, que conoce el rendimiento esperado verdadero, da 0,029. ¿Qué se concluye?

   - A. Que el modelo ha encontrado una señal que el generador no conocía
   - B. Que el modelo es cinco veces mejor que el oráculo
   - C. Que hay una fuga o un error: ningún modelo puede superar fuera de muestra al rendimiento esperado verdadero
   - D. Que el oráculo está mal calculado

   *Respuesta C: Que hay una fuga o un error: ningún modelo puede superar fuera de muestra al rendimiento esperado verdadero.* El oráculo es la mejor predicción posible con la información del momento. Superarlo de forma sistemática solo es posible usando información posterior. En los datos, `revision_analistas` da un IC de 0,174 por eso: incluye los dos primeros días de la semana que predice.

2. El oráculo acierta si una acción queda por encima de la mediana en el 50,95 % de los casos. ¿Por qué ese acierto basta para una cartera con Sharpe bruto alto?

   - A. Porque se apuesta a la vez en 400 acciones cada semana, durante 729 semanas: una ventaja diminuta por apuesta se acumula con la amplitud
   - B. Porque el acierto está mal medido
   - C. Porque el oráculo acierta más en las acciones grandes
   - D. No basta: con un 51 % no se puede ganar dinero

   *Respuesta A: Porque se apuesta a la vez en 400 acciones cada semana, durante 729 semanas: una ventaja diminuta por apuesta se acumula con la amplitud.* La ley fundamental de la gestión activa (lección 5.1.3) relaciona el ratio de información con IC·√amplitud. Lo que no basta es para pagar los costes de rotarlo todo cada semana: el README del nivel da un Sharpe neto de 0,38 para el oráculo.

3. Un LightGBM con 512 hojas da un IC de 0,96 en entrenamiento (2012-2018) y de −0,002 en 2019-2025. ¿Cuál es el diagnóstico?

   - A. Fuga de información
   - B. Sobreajuste: el IC dentro de muestra supera con mucho el techo del oráculo (0,033 en ese periodo), así que el modelo ha memorizado ruido
   - C. Cambio de régimen en 2019
   - D. Falta de datos en 2019-2025

   *Respuesta B: Sobreajuste: el IC dentro de muestra supera con mucho el techo del oráculo (0,033 en ese periodo), así que el modelo ha memorizado ruido.* Dentro de muestra, cualquier IC por encima del techo es ruido aprendido. El LightGBM moderado también lo supera (0,066) y fuera de muestra queda en 0,014, por debajo de la ridge (0,019).

4. `vol_63d` tiene efecto verdadero y su IC da t = 0,6; `rsi_14` no tiene efecto propio y da t = −2,6. ¿Qué enseña?

   - A. Que el generador está mal
   - B. Que hay que usar el RSI
   - C. Que t = 2 es un umbral demasiado alto
   - D. Que con 14 años y esta relación señal/ruido, un contraste univariante confunde efectos verdaderos con features correlacionadas con otro efecto (el RSI hereda la reversión) y no detecta efectos tapados por otro (la beta tapa la baja volatilidad)

   *Respuesta D: Que con 14 años y esta relación señal/ruido, un contraste univariante confunde efectos verdaderos con features correlacionadas con otro efecto (el RSI hereda la reversión) y no detecta efectos tapados por otro (la beta tapa la baja volatilidad).* Las features se parecen entre sí y el ruido es enorme. El README da el detalle: en Fama-MacBeth, la baja volatilidad no se recupera con el rendimiento bruto porque las acciones volátiles tienen más beta.

5. Con la predicción del oráculo usada una semana tarde, el IC medio baja de 0,029 a 0,011. ¿Qué relación tiene con la competencia?

   - A. Ninguna
   - B. Indica que el oráculo tiene una fuga
   - C. Que hay que predecir a cuatro semanas
   - D. La señal tiene un horizonte corto: quien la explota antes se lleva la mayor parte. Con muchos participantes buscando lo mismo, el que llega tarde opera sobre la parte lenta que queda

   *Respuesta D: La señal tiene un horizonte corto: quien la explota antes se lleva la mayor parte. Con muchos participantes buscando lo mismo, el que llega tarde opera sobre la parte lenta que queda.* Es el decaimiento de la señal por retraso. A escala de años, McLean y Pontiff (2016) miden algo parecido con 97 anomalías publicadas: un 26 % menos de rendimiento fuera de muestra y un 58 % menos tras la publicación.

## Ideas clave

- En el panel del nivel, el modelo perfecto (el rendimiento esperado verdadero) tiene un IC semanal de 0,029, acierta el 50,95 % y explica un 0,09 % de la varianza. Ese es el techo, y es el de un mercado real.
- Incluso el oráculo tiene un IC negativo en el 35 % de las semanas: la señal solo se ve agregando muchas apuestas durante mucho tiempo.
- Muchas filas no son muchos datos: las features lentas cambian poco de una semana a otra y 14 años no bastan para separar efectos verdaderos de falsos en un contraste univariante.
- Las relaciones cambian: el coeficiente verdadero del momentum es positivo en calma y negativo en los rebotes; un modelo fijo aprende una media que no se repite.
- Regla de cordura: un IC dentro de muestra muy por encima del techo es sobreajuste; uno fuera de muestra por encima del techo es una fuga.
- La señal caduca: usar el oráculo una semana tarde deja un 38 % del IC; las anomalías publicadas pierden más de la mitad de su rendimiento.

## Recursos

### Research

- [Alpha Factors y Alpha Trading en Python](https://pythonparatrading.com/alpha-factors-y-alpha-trading-en-python/) · Research-0007 · 5 min
- [Introduccion al Quant Research](https://pythonparatrading.com/quant-research/) · Research-0023 · 8 min

### Libros

- [McLean, R. D. y Pontiff, J. (2016): Does Academic Research Destroy Stock Return Predictability? The Journal of Finance 71(1)](https://doi.org/10.1111/jofi.12365) · doi.org
- [Israel, R., Kelly, B. y Moskowitz, T. (2020): Can Machines «Learn» Finance? Journal of Investment Management 18(2), 23-36](https://www.aqr.com/Insights/Research/Journal-Article/Can-Machines-Learn-Finance) · aqr.com
- [Gu, S., Kelly, B. y Xiu, D. (2020): Empirical Asset Pricing via Machine Learning. The Review of Financial Studies 33(5), 2223-2273 (versión NBER w25398)](https://www.nber.org/papers/w25398) · nber.org
- [López de Prado, M. (2018): Advances in Financial Machine Learning. Wiley](https://www.wiley.com/en-us/Advances+in+Financial+Machine+Learning-p-9781119482086) · wiley.com

### Datos

- [Datos del nivel 5: features semanales del panel de 400 acciones (Parquet)](https://pythonparatrading.com/curso/datos/nivel-5/panel5_features.parquet) · Descarga · panel5_features.parquet

Lección siguiente: [5.1.2 · Aprendizaje supervisado, no supervisado y por refuerzo; pipelines de scikit-learn](https://pythonparatrading.com/curso-python-trading/nivel-5/aprendizaje-supervisado-y-pipelines-de-scikit-learn/)
