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.
- 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, 2.1.8 · El problema de las pruebas múltiples: Bonferroni, FDR y el «factor zoo», 2.5.1 · Sesgos de backtest: anticipación, supervivencia, selección y narrativa, 2.5.2 · In-sample y out-of-sample: walk-forward anclado y móvil
Lecciones del nivel 5
5.1 · Fundamentos de ML aplicados a finanzas
5.2 · Ingeniería de características
5.3 · Etiquetado y muestreo
- 5.3.1 Etiquetado de horizonte fijo y sus problemas
- 5.3.2 Método de la triple barrera: etiquetas que imitan una operación
- 5.3.3 Meta-labeling: un modelo para el lado y otro para el tamaño
- 5.3.4 Unicidad de las etiquetas, pesos por solapamiento y bootstrap secuencial
- 5.3.5 Muestreo por eventos: el filtro CUSUM simétrico
5.4 · Validación para series financieras
5.5 · Modelos clásicos
- 5.5.1 Regresiones regularizadas: Ridge, Lasso, Elastic Net y logística
- 5.5.2 Árboles de decisión, Random Forest y bagging
- 5.5.3 Gradient boosting: XGBoost, LightGBM y CatBoost
- 5.5.4 Optuna y el riesgo de sobreajustar la validación
- 5.5.5 Aprendizaje no supervisado: PCA, clustering y autoencoders
- 5.5.6 Learning to rank para la selección de acciones
5.6 · Deep learning con Keras 3
5.7 · Deep learning con PyTorch
- 5.7.1 PyTorch: tensores, autograd y el bucle de entrenamiento
- 5.7.2 Arquitecturas de series: TCN, N-BEATS, PatchTST e iTransformer
- 5.7.3 Modelos probabilísticos: pérdida cuantílica, DeepAR e intervalos
- 5.7.4 Redes de grafos para relaciones entre activos
- 5.7.5 Optimización de carteras end-to-end: capas diferenciables y pérdida de Sharpe
5.8 · Modelos fundacionales de series temporales
- 5.8.1 Modelos fundacionales de series: preentrenar una vez, prever sin entrenar
- 5.8.2 Las familias de modelos fundacionales: arquitectura, datos y licencia
- 5.8.3 Uso práctico: contexto, cuantiles, covariables y ajuste fino
- 5.8.4 Modelos fundacionales en finanzas: qué dice la evidencia
- 5.8.5 Laboratorio: modelos fundacionales frente a GARCH, HAR y LightGBM
5.9 · Aprendizaje por refuerzo
5.10 · Interpretabilidad, robustez y riesgo de modelo
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.
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)(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.072296La 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:
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)ic_medio 0.0292
desv 0.0929
t 8.4714
semanas_pos 0.6461
dtype: float64El 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:
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")Acierto frente a la mediana: 50.95%
Varianza explicada (IC²): 0.085%
Dispersión semanal: 3.62%; desv. del alfa: 12.3 pbUn 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:
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) 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.1La 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:
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)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: float64El 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:
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) 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 FUGATres lecturas de la tabla:
- Efectos verdaderos que no se ven.
vol_63dtiene 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).sentimientoysue, con efecto, quedan por debajo deey. - Features sin efecto propio que parecen buenas.
ey(t = 4,0) yroe(t = 2,7) heredan el efecto del value y del SUE, con los que están correlacionadas;rsi_14(t = −2,6) yret_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_analistasda 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 con desviación semanal , el estadístico t tras semanas es , y para llegar a t = 2 hacen falta semanas. Para una estrategia con Sharpe anual , los años necesarios son :
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) 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: float64El 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:
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) 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.0107En 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.
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) 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.0022El 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:
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:
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)) 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:
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_analistasno 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
- Calcula el IC medio del oráculo por régimen (
calma,estres,rebote) con la columnaregimendepanel5_coef_verdad.csv. ¿En qué régimen es más difícil predecir aunque se conozca el rendimiento esperado?
Solución
reg = coef["regimen"].reindex(ic_or.index)
ic_or.groupby(reg).agg(["mean", "std", "count"]).round(4) mean std count
regimen
calma 0.0286 0.0889 575
estres 0.0301 0.0948 89
rebote 0.0330 0.1223 65El 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.
- Con la fórmula , 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
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)")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.
- Un colega presenta un clasificador entrenado sobre
panel5que 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
Un modelo entrenado sobre
panel5da 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_analistasda un IC de 0,174 por eso: incluye los dos primeros días de la semana que predice.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.
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).
vol_63dtiene efecto verdadero y su IC da t = 0,6;rsi_14no 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.
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
Libros
- McLean, R. D. y Pontiff, J. (2016): Does Academic Research Destroy Stock Return Predictability? The Journal of Finance 71(1)
- Israel, R., Kelly, B. y Moskowitz, T. (2020): Can Machines «Learn» Finance? Journal of Investment Management 18(2), 23-36
- 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)
- López de Prado, M. (2018): Advances in Financial Machine Learning. Wiley