Lección 5.1.1 · 60 min

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.

Fig. — IC semanal del oráculo (el rendimiento esperado verdadero) en las 729 semanas del panel, 2012-2025: media 0,029 y negativo en el 35 % de las semanas
Lecciones del nivel 5
  1. 5.1 · Fundamentos de ML aplicados a finanzas

    1. 5.1.1 Por qué el machine learning financiero es distinto
    2. 5.1.2 Aprendizaje supervisado, no supervisado y por refuerzo; pipelines de scikit-learn
    3. 5.1.3 Métricas económicas para modelos de trading: más allá del acierto
  2. 5.2 · Ingeniería de características

    1. 5.2.1 Features de precio, volumen y microestructura
    2. 5.2.2 Fundamentales point-in-time, macro y datos alternativos
    3. 5.2.3 Diferenciación fraccional para features
    4. 5.2.4 Normalización sin leakage y rank transversal
    5. 5.2.5 Importancia de features: MDI, MDA, SFI y SHAP
  3. 5.3 · Etiquetado y muestreo

    1. 5.3.1 Etiquetado de horizonte fijo y sus problemas
    2. 5.3.2 Método de la triple barrera: etiquetas que imitan una operación
    3. 5.3.3 Meta-labeling: un modelo para el lado y otro para el tamaño
    4. 5.3.4 Unicidad de las etiquetas, pesos por solapamiento y bootstrap secuencial
    5. 5.3.5 Muestreo por eventos: el filtro CUSUM simétrico
  4. 5.4 · Validación para series financieras

    1. 5.4.1 Por qué falla el k-fold aleatorio en datos financieros
    2. 5.4.2 Purged k-fold con embargo y CPCV para modelos de ML
    3. 5.4.3 Walk-forward con reentrenamiento: ventanas y frecuencia
    4. 5.4.4 Backtest de señales de ML con costes y Deflated Sharpe
  5. 5.5 · Modelos clásicos

    1. 5.5.1 Regresiones regularizadas: Ridge, Lasso, Elastic Net y logística
    2. 5.5.2 Árboles de decisión, Random Forest y bagging
    3. 5.5.3 Gradient boosting: XGBoost, LightGBM y CatBoost
    4. 5.5.4 Optuna y el riesgo de sobreajustar la validación
    5. 5.5.5 Aprendizaje no supervisado: PCA, clustering y autoencoders
    6. 5.5.6 Learning to rank para la selección de acciones
  6. 5.6 · Deep learning con Keras 3

    1. 5.6.1 Keras 3: un modelo, varios motores de cálculo
    2. 5.6.2 Un MLP sobre un panel de acciones: regularizar y comparar
    3. 5.6.3 CNN 1D, LSTM y GRU: ventanas, escalado y fugas
    4. 5.6.4 Callbacks, una pérdida de Sharpe y el modelo en producción
  7. 5.7 · Deep learning con PyTorch

    1. 5.7.1 PyTorch: tensores, autograd y el bucle de entrenamiento
    2. 5.7.2 Arquitecturas de series: TCN, N-BEATS, PatchTST e iTransformer
    3. 5.7.3 Modelos probabilísticos: pérdida cuantílica, DeepAR e intervalos
    4. 5.7.4 Redes de grafos para relaciones entre activos
    5. 5.7.5 Optimización de carteras end-to-end: capas diferenciables y pérdida de Sharpe
  8. 5.8 · Modelos fundacionales de series temporales

    1. 5.8.1 Modelos fundacionales de series: preentrenar una vez, prever sin entrenar
    2. 5.8.2 Las familias de modelos fundacionales: arquitectura, datos y licencia
    3. 5.8.3 Uso práctico: contexto, cuantiles, covariables y ajuste fino
    4. 5.8.4 Modelos fundacionales en finanzas: qué dice la evidencia
    5. 5.8.5 Laboratorio: modelos fundacionales frente a GARCH, HAR y LightGBM
  9. 5.9 · Aprendizaje por refuerzo

    1. 5.9.1 MDP, Q-learning, PPO y entornos Gymnasium
    2. 5.9.2 RL para ejecución, market making y cobertura dinámica
    3. 5.9.3 Por qué el RL sobreajusta en trading direccional
  10. 5.10 · Interpretabilidad, robustez y riesgo de modelo

    1. 5.10.1 SHAP, dependencia parcial y estabilidad de las importancias
    2. 5.10.2 Deriva de datos y de concepto: detectarla y qué hacer
    3. 5.10.3 Ensembles, combinación de señales y datos sintéticos

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.

RasgoVisión, lenguajeRendimientos financieros
Relación señal/ruidoAlta: la etiqueta casi está en los datosBajísima: el modelo perfecto explica décimas de punto de la varianza
EstacionariedadEl fenómeno no cambiaLas relaciones cambian con el régimen y desaparecen al explotarse
DatosMillones de ejemplos independientes; se pueden generar másUna sola historia; observaciones correlacionadas en el tiempo y entre activos
AdversarioNinguno: el gato no reacciona al clasificadorOtros participantes compiten por la misma señal y la gastan
Coste del errorSimétrico, medido con acierto o pérdidaAsimé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.

IN [01]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)
OUT [01]El panel y el oráculo
(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:

IN [02]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)
OUT [02]IC semanal y su resumen
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:

IN [03]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")
OUT [03]Acierto y varianza explicada del oráculo
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.

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

IN [04]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)
OUT [04]Rendimiento de la semana siguiente por decil del oráculo
                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

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:

IN [05]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)
OUT [05]Persistencia de las features de una semana a la siguiente
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:

IN [06]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)
OUT [06]IC de cada feature frente a su efecto verdadero
                    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 IC‾\overline{IC} con desviación semanal ss, el estadístico t tras nn semanas es IC‾n/s\overline{IC}\sqrt{n}/s, y para llegar a t = 2 hacen falta n=(2s/IC‾)2n = (2s/\overline{IC})^2 semanas. Para una estrategia con Sharpe anual SR\mathrm{SR}, los años necesarios son (2/SR)2(2/\mathrm{SR})^2:

IN [07]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)
OUT [07]Cuánto tiempo hace falta para distinguir una señal
                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:

IN [08]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)
OUT [08]El coeficiente verdadero del momentum por régimen y por año
          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

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.

IN [09]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)
OUT [09]Tres modelos: IC dentro y fuera de muestra
               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

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:

sínosínonosíResultado¿IC dentro muy por encimadel techo?Sobreajuste¿IC fuera por encima deltecho?Fuga o error¿Gana tras costes?Señal sin valorValidar más: 5.4
Fuente 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:

IN [10]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))
OUT [10]El oráculo usado con retraso
               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.

Qué cambia en el método

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

Datos point-in-timeEtiquetas y pesosValidación purgadaModelo y baselineMétricas económicasBacktest y DSRVigilar la deriva
Fuente 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
IN [11]python
reg = coef["regimen"].reindex(ic_or.index)
ic_or.groupby(reg).agg(["mean", "std", "count"]).round(4)
OUT [11]IC del oráculo por régimen
           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.

  1. Con la fórmula n=(2s/IC‾)2n = (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
IN [12]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)")
OUT [12]Semanas necesarias para t = 2 y t = 3
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.

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

Respuesta plegada en cada pregunta

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?

    • AQue el modelo ha encontrado una señal que el generador no conocía
    • BQue el modelo es cinco veces mejor que el oráculo
    • CQue hay una fuga o un error: ningún modelo puede superar fuera de muestra al rendimiento esperado verdadero
    • DQue el oráculo está mal calculado
    Ver respuesta

    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?

    • APorque se apuesta a la vez en 400 acciones cada semana, durante 729 semanas: una ventaja diminuta por apuesta se acumula con la amplitud
    • BPorque el acierto está mal medido
    • CPorque el oráculo acierta más en las acciones grandes
    • DNo basta: con un 51 % no se puede ganar dinero
    Ver respuesta

    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?

    • AFuga de información
    • BSobreajuste: 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
    • CCambio de régimen en 2019
    • DFalta de datos en 2019-2025
    Ver respuesta

    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?

    • AQue el generador está mal
    • BQue hay que usar el RSI
    • CQue t = 2 es un umbral demasiado alto
    • DQue 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)
    Ver respuesta

    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?

    • ANinguna
    • BIndica que el oráculo tiene una fuga
    • CQue hay que predecir a cuatro semanas
    • DLa 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
    Ver respuesta

    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