Lección 5.4.3 · 65 min
Walk-forward con reentrenamiento: ventanas y frecuencia
El walk-forward es la validación que imita la producción: entrenar con lo que se sabía, predecir la semana siguiente y volver a empezar. Quedan dos decisiones que la lección 2.5.2 no tuvo que tomar con un modelo: con cuánta historia se entrena y cada cuánto se reentrena. En un panel con efectos que cambian de signo según el régimen, la intuición dice «ventana corta y reentreno frecuente»; los datos dicen otra cosa.
- Módulo
- 5.4 · Validación para series financieras
- Duración
- 65 min
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
Dos decisiones que la validación no toma por ti
La lección 2.5.2 hizo un walk-forward para elegir un parámetro de una estrategia y la 5.4.2, una validación purgada para comparar modelos. Un walk-forward con un modelo de machine learning añade dos decisiones de diseño:
- La ventana: con toda la historia disponible (expansiva), con los últimos N años (móvil) o con toda pero dando más peso a lo reciente (pesos exponenciales, con una semivida).
- La frecuencia: reentrenar cada año, cada trimestre, cada mes o cada semana.
Fuente Mermaid
flowchart TB D[Fecha de decisión] --> E[Etiquetas cerradas] E --> V[Elegir ventana] V --> M[Reentrenar si toca] M --> P[Predecir la semana] P --> A[Avanzar una semana] A --> D
La figura compara qué ve el modelo antes de cada año de prueba con una ventana expansiva y con una móvil de tres años, las dos reentrenadas cada enero:
La intuición empuja hacia ventanas cortas y reentrenos frecuentes: los mercados cambian. El panel del nivel 5 es un
buen sitio para ponerla a prueba, porque los efectos cambian de verdad y se sabe cuánto. Según el README, cada
coeficiente verdadero es base × régimen × m, con m un AR(1) semanal de persistencia 0,9 (los efectos se debilitan y
a veces se invierten) y el momentum multiplicado por −2,5 en los rebotes tras una crisis (el crash del momentum).
panel5_coef_verdad.csv trae los coeficientes de cada semana y su régimen.
El panel en arrays
Para reentrenar cientos de veces conviene tener el panel como arrays de semanas × acciones × features: 730 × 400 × 21. La transformación es la del README (y la de la lección 5.2.4): el rango normal transversal con las acciones que tienen dato, y 0 donde falta.
import numpy as np
import pandas as pd
from scipy.stats import norm, rankdata
from sklearn.linear_model import Ridge
feat = pd.read_parquet("panel5_features.parquet")
etiq = pd.read_parquet("panel5_etiquetas.parquet")
feat = feat.sort_values(["fecha", "ticker"])
etiq = etiq.sort_values(["fecha", "ticker"])
FEATS = [c for c in feat.columns if c not in
("fecha", "ticker", "sector", "revision_analistas")]
fechas = pd.DatetimeIndex(feat["fecha"].unique())
nW, nA, nF = len(fechas), feat["ticker"].nunique(), len(FEATS)
F = feat[FEATS].to_numpy(dtype=float).reshape(nW, nA, nF)
with np.errstate(invalid="ignore", divide="ignore"):
rango = rankdata(F, axis=1, nan_policy="omit")
n = np.isfinite(F).sum(axis=1, keepdims=True)
G = np.nan_to_num(norm.ppf((rango - 0.5) / n))
R = etiq["ret_sig_semana"].to_numpy().reshape(nW, nA)
fin = etiq["fin_sig_semana"].to_numpy().reshape(nW, nA)[:, 0]
con_feat = np.isfinite(F[:, :, 0]).all(axis=1)
con_etiq = np.isfinite(R).all(axis=1)
valida = con_feat & con_etiq # semanas para entrenar
Yc = np.zeros_like(R)
Yc[valida] = R[valida] - R[valida].mean(axis=1, keepdims=True)
coef = pd.read_csv("panel5_coef_verdad.csv", parse_dates=["fecha"])
coef = coef.set_index("fecha").reindex(fechas)
regimen = coef["regimen"].to_numpy()
print(f"{nW} semanas x {nA} acciones x {nF} features; "
f"semanas válidas {valida.sum()}")
print(pd.Series(regimen[valida]).value_counts().to_string())730 semanas x 400 acciones x 21 features; semanas válidas 729
calma 575
estres 89
rebote 65La regla del tiempo: entrenar con etiquetas cerradas
En la fecha de decisión w solo se puede entrenar con filas cuya etiqueta ya ha terminado. La regla sale de los datos,
no de una suposición: para cada fecha, la última semana utilizable es la última cuyo fin_sig_semana es
anterior a ella.
siguiente = (fin[:-1] == fechas[1:].to_numpy()).mean()
print(f"la etiqueta de la semana w termina en la fecha w+1: "
f"{siguiente:.1%} de las semanas")
def ultima_conocida(w):
"""Índice de la última semana cuya etiqueta terminó antes de
la fecha w (fin < fecha: un margen de una semana)."""
return int(np.searchsorted(fin, fechas[w], side="left")) - 1
w = fechas.get_loc(pd.Timestamp("2020-03-20"))
u = ultima_conocida(w)
print(f"decisión el {fechas[w].date()}: entrenar hasta la semana del "
f"{fechas[u].date()} (su etiqueta terminó el "
f"{pd.Timestamp(fin[u]).date()})")la etiqueta de la semana w termina en la fecha w+1: 100.0% de las semanas
decisión el 2020-03-20: entrenar hasta la semana del 2020-03-06 (su etiqueta terminó el 2020-03-13)La etiqueta de la semana w termina exactamente en la fecha w+1. Así que, al cierre de la fecha w, la etiqueta de w−1
acaba de cerrarse y la de w está abierta. La regla fin < fecha deja fuera también la de w−1 (que termina justo ese
día): es el margen que usa el README, prudente si las órdenes se envían antes del cierre. Incluir la semana w sería
una fuga: el ejercicio 1 mide cuánta.
Reentrenar mil veces en segundos
Un ridge con intercepto sobre 21 features tiene solución cerrada a partir de cinco cantidades: el número de filas, las sumas de x e y, y las matrices y . Todas se suman entre semanas. Si se guardan por semana, el ridge de cualquier ventana (o con pesos por semana) es una suma y un sistema de 21 × 21:
donde el subíndice c indica datos centrados (scikit-learn centra para estimar el intercepto, que no se penaliza).
X1 = np.concatenate([np.ones((nW, nA, 1)), G], axis=2)
peso_valida = valida.astype(float)
S_xx = np.einsum("wni,wnj->wij", X1, X1) * peso_valida[:, None, None]
S_xy = np.einsum("wni,wn->wi", X1, Yc) * peso_valida[:, None]
def ridge(pesos, alfa=10.0):
"""Ridge con intercepto de las semanas con `pesos` > 0."""
A = np.tensordot(pesos, S_xx, 1)
b = np.tensordot(pesos, S_xy, 1)
n_, mx, my = A[0, 0], A[0, 1:] / A[0, 0], b[0] / A[0, 0]
C = A[1:, 1:] - n_ * np.outer(mx, mx)
c = b[1:] - n_ * mx * my
return np.linalg.solve(C + alfa * np.eye(nF), c)
prueba = np.flatnonzero(valida & (fechas.year < 2015))
sk = Ridge(alpha=10.0).fit(G[prueba].reshape(-1, nF),
Yc[prueba].reshape(-1))
pesos = np.zeros(nW)
pesos[prueba] = 1.0
print(f"máx. |diferencia| con scikit-learn: "
f"{np.abs(ridge(pesos) - sk.coef_).max():.1e}")máx. |diferencia| con scikit-learn: 8.1e-18La diferencia está en el último decimal del float64. A partir de aquí, cada reentrenamiento es una suma ponderada de
730 matrices pequeñas: el walk-forward semanal entero tarda menos de un segundo.
El walk-forward y su métrica
La función recorre las semanas de prueba (2015-2025), reentrena cuando toca y predice la puntuación de cada acción. La métrica es el IC: la correlación de rangos, cada semana, entre la puntuación y el rendimiento de la semana siguiente, y su media. El error típico sale de la serie semanal de ICs (las etiquetas de una semana no se solapan, así que esa serie es casi independiente).
anyo = fechas.year.to_numpy()
semanas_prueba = np.flatnonzero(con_feat & (anyo >= 2015))
def walk_forward(ventana=None, cada=None, anual=False,
semivida=None):
"""Puntuaciones (semanas x acciones) y coeficientes por semana.
ventana: semanas de historia (None = expansiva); cada: reentrenar
cada n semanas; anual: cada enero; semivida: pesos exponenciales."""
pred = np.full((nW, nA), np.nan)
betas = np.full((nW, nF), np.nan)
beta = None
for i, w in enumerate(semanas_prueba):
toca = (beta is None
or (anual and anyo[w] != anyo[semanas_prueba[i - 1]])
or (cada is not None and i % cada == 0))
if toca:
u = ultima_conocida(w)
p = peso_valida.copy()
p[u + 1:] = 0.0
if ventana is not None:
p[:max(0, u + 1 - ventana)] = 0.0
if semivida is not None:
p *= 0.5 ** ((u - np.arange(nW)) / semivida)
beta = ridge(p)
pred[w] = G[w] @ beta
betas[w] = beta
return pred, betas
evaluables = semanas_prueba[con_etiq[semanas_prueba]]
def ic_semanal(pred):
out = []
for w in evaluables:
out.append(np.corrcoef(rankdata(pred[w]),
rankdata(R[w]))[0, 1])
return np.array(out)
def resumen(ic):
et = ic.std(ddof=1) / np.sqrt(len(ic))
return {"IC": ic.mean(), "error típico": et, "t": ic.mean() / et}
base, betas_base = walk_forward(anual=True)
ic_base = ic_semanal(base)
print({k: round(float(v), 4) for k, v in resumen(ic_base).items()},
f"semanas: {len(ic_base)}"){'IC': 0.0185, 'error típico': 0.0038, 't': 4.8789} semanas: 574Expansiva, reentrenada cada enero: IC de 0,0185 con t = 4,9 en 574 semanas, el mismo resultado que publica el README para este modelo. Es la referencia del resto de la lección.
Frecuencia de reentrenamiento
Con la ventana expansiva, cuatro frecuencias: cada enero, cada 13 semanas (trimestral), cada 4 (mensual) y cada semana.
esquemas = {"expansiva, anual": ic_base}
for nombre, kw in [("expansiva, cada 13 semanas", {"cada": 13}),
("expansiva, cada 4 semanas", {"cada": 4}),
("expansiva, semanal", {"cada": 1})]:
esquemas[nombre] = ic_semanal(walk_forward(**kw)[0])
pd.DataFrame({k: resumen(v) for k, v in esquemas.items()}).T.round(4) IC error típico t
expansiva, anual 0.0185 0.0038 4.8789
expansiva, cada 13 semanas 0.0176 0.0038 4.6397
expansiva, cada 4 semanas 0.0172 0.0038 4.5324
expansiva, semanal 0.0176 0.0038 4.6463Reentrenar más a menudo no mejora nada: de 0,0185 a 0,0172-0,0176, diferencias de unas milésimas con un error típico de casi cuatro. Tiene sentido: con ventana expansiva, un año nuevo sobre diez de historia apenas mueve los coeficientes, y una semana nueva, menos.
Ventana expansiva, móvil y con pesos
Ahora la ventana. Móviles de cinco, tres, dos y un año reentrenadas cada enero, y dos versiones semanales que deberían ser las más adaptativas: una ventana móvil de un año y pesos exponenciales con semivida de dos años y de uno.
for nombre, kw in [
("móvil 5 años, anual", {"ventana": 260, "anual": True}),
("móvil 3 años, anual", {"ventana": 156, "anual": True}),
("móvil 2 años, anual", {"ventana": 104, "anual": True}),
("móvil 1 año, anual", {"ventana": 52, "anual": True}),
("móvil 1 año, semanal", {"ventana": 52, "cada": 1}),
("semivida 2 años, semanal", {"semivida": 104, "cada": 1}),
("semivida 1 año, semanal", {"semivida": 52, "cada": 1})]:
p, b = walk_forward(**kw)
esquemas[nombre] = ic_semanal(p)
if nombre == "móvil 1 año, semanal":
betas_movil = b
tabla = pd.DataFrame({k: resumen(v) for k, v in esquemas.items()}).T
print(tabla.round(4).to_string()) IC error típico t
expansiva, anual 0.0185 0.0038 4.8789
expansiva, cada 13 semanas 0.0176 0.0038 4.6397
expansiva, cada 4 semanas 0.0172 0.0038 4.5324
expansiva, semanal 0.0176 0.0038 4.6463
móvil 5 años, anual 0.0143 0.0040 3.6273
móvil 3 años, anual 0.0102 0.0037 2.7680
móvil 2 años, anual 0.0085 0.0038 2.2282
móvil 1 año, anual 0.0084 0.0039 2.1652
móvil 1 año, semanal 0.0039 0.0038 1.0086
semivida 2 años, semanal 0.0120 0.0038 3.1412
semivida 1 año, semanal 0.0081 0.0038 2.1550La figura de la cabecera ordena los once esquemas con su intervalo de ±2 errores típicos. El patrón es nítido: cuanto más corta la ventana, peor. Cinco años pierden un cuarto del IC (0,0143); uno, más de la mitad (0,0084), y con reentrenamiento semanal, casi todo (0,0039, t = 1,0, indistinguible de cero). Los pesos exponenciales quedan entre medias, peor cuanto más corta la semivida.
Las diferencias entre esquemas no se leen con los intervalos de cada uno, que se solapan: se leen con la diferencia pareada semana a semana, que elimina el ruido común (las semanas buenas y malas son las mismas para todos los modelos).
def pareada(a, b):
dif = esquemas[a] - esquemas[b]
et = dif.std(ddof=1) / np.sqrt(len(dif))
return {"diferencia": dif.mean(), "t": dif.mean() / et}
pd.DataFrame({
"expansiva semanal - móvil 1 año semanal":
pareada("expansiva, semanal", "móvil 1 año, semanal"),
"expansiva anual - expansiva semanal":
pareada("expansiva, anual", "expansiva, semanal"),
"expansiva anual - móvil 5 años anual":
pareada("expansiva, anual", "móvil 5 años, anual"),
}).T.round(4) diferencia t
expansiva semanal - móvil 1 año semanal 0.0138 3.8797
expansiva anual - expansiva semanal 0.0008 1.2332
expansiva anual - móvil 5 años anual 0.0041 2.6339Expansiva frente a móvil de un año, las dos con reentreno semanal: 0,014 de diferencia con t = 3,9. Es la única conclusión firme de la tabla. Frecuencia anual frente a semanal: t = 1,2, nada. Expansiva frente a móvil de cinco años, las dos anuales: 0,004 con t = 2,6, en la misma dirección.
Por régimen: el crash del momentum
La esperanza de la ventana móvil era adaptarse a los cambios. El panel tiene cambios de régimen conocidos: en estres
la reversión se refuerza (×1,5) y en rebote el momentum se invierte (×−2,5). El IC por régimen dice si alguna ventana
lo aprovecha:
reg_eval = regimen[evaluables]
por_regimen = {}
for nombre in ("expansiva, anual", "móvil 3 años, anual",
"móvil 1 año, semanal", "semivida 1 año, semanal"):
ic = esquemas[nombre]
por_regimen[nombre] = {r: ic[reg_eval == r].mean()
for r in ("calma", "estres", "rebote")}
t = pd.DataFrame(por_regimen).T.round(4)
t.columns = [f"{r} ({(reg_eval == r).sum()} sem.)" for r in t.columns]
t calma (420 sem.) estres (89 sem.) rebote (65 sem.)
expansiva, anual 0.0230 0.0026 0.0110
móvil 3 años, anual 0.0156 -0.0132 0.0075
móvil 1 año, semanal 0.0073 -0.0094 -0.0003
semivida 1 año, semanal 0.0128 -0.0077 -0.0003Ninguna ventana corta gana en los rebotes, que es donde se esperaba la ventaja de adaptarse: allí la expansiva sigue siendo la mejor (0,011, la mitad que en calma) y las adaptativas se quedan en cero. Los rebotes del panel duran de tres a seis meses (marzo-junio de 2020, abril-julio de 2025…): cuando una ventana de un año empieza a reflejar el cambio de signo del momentum, el rebote ha terminado y el modelo se queda con un coeficiente distorsionado para la calma siguiente. En las semanas de estrés, además, las ventanas cortas tienen un IC negativo. La adaptación que funcionaría no es de ventana sino de modelo: una feature de régimen observable a tiempo (y bien fechada, lección 5.2.2) con la que el modelo aprenda la interacción.
Sesgo frente a varianza: la trayectoria de un coeficiente
La explicación está en los coeficientes. panel5_coef_verdad.csv trae el coeficiente verdadero del momentum cada
semana (c_mom, en rendimiento semanal por unidad de g). El ridge estima el mismo objeto con la feature mom_12_1
(con las otras 20 al lado, entre ellas ruido_4, que hereda una parte del momentum, así que el nivel no tiene por qué
coincidir). Lo que importa es cómo se mueve cada estimación frente a la verdad:
j = FEATS.index("mom_12_1")
verdad_mom = coef["c_mom"].to_numpy()[semanas_prueba] * 1e4
_, betas_exp = walk_forward(cada=1)
filas = {}
for nombre, b in (("expansiva semanal", betas_exp),
("móvil 1 año semanal", betas_movil)):
est = b[semanas_prueba, j] * 1e4
filas[nombre] = {
"media (pb)": est.mean(), "desv. (pb)": est.std(),
"corr. con la verdad": np.corrcoef(est, verdad_mom)[0, 1],
"error cuadrático medio (pb)":
np.sqrt(np.mean((est - verdad_mom) ** 2))}
filas["verdad"] = {"media (pb)": verdad_mom.mean(),
"desv. (pb)": verdad_mom.std()}
pd.DataFrame(filas).T.round(2) media (pb) desv. (pb) corr. con la verdad error cuadrático medio (pb)
expansiva semanal 4.88 0.93 -0.01 6.04
móvil 1 año semanal 4.37 6.37 -0.10 8.99
verdad 2.40 5.42 NaN NaNEl coeficiente verdadero oscila mucho de una semana a otra (desviación de 5,4 pb sobre una media de 2,4) y cambia de signo en los rebotes. La estimación de un año también oscila, con una desviación de 6,4 pb, pero no sigue a la verdad: su correlación con ella es de −0,10. Persigue el ruido de la muestra de los últimos doce meses, no el efecto. La expansiva es casi una línea (desviación de 0,9 pb, correlación nula): está sesgada, porque no ve los cambios, pero su error cuadrático frente a la verdad es de 6,0 pb y el de la móvil, de 9,0. Seguir un efecto que cambia cada pocas semanas con un año de datos ruidosos añade más error del que quita. Es el resultado clásico de Pesaran y Timmermann (2007): con rupturas, la ventana óptima depende del tamaño de la ruptura frente al ruido, y cuando la señal es pequeña, usar todos los datos suele ganar.
| Situación | Ventana que suele convenir |
|---|---|
| Señal débil (IC de centésimas), efectos que oscilan sin tendencia | expansiva, o pesos con semivida larga |
| Ruptura grande y permanente (un efecto que desaparece) | móvil o con pesos, tras detectarla (lección 5.10.2) |
| Señal fuerte y cambiante (alta frecuencia, datos abundantes) | móvil corta y reentreno frecuente |
| Cualquiera | la que diga un walk-forward fijado de antemano, con su diferencia pareada |
Ejercicios
- Repite el walk-forward expansivo semanal y el móvil de 26 semanas olvidando el margen: entrena hasta la propia semana w, cuya etiqueta es la que se predice. ¿Cuánto sube el IC en cada uno y por qué más en la ventana corta?
Solución
def ultima_con_fuga(w):
return w # ¡incluye la etiqueta de la propia semana!
original = ultima_conocida
for nombre, kw in (("expansiva semanal", {"cada": 1}),
("móvil 26 semanas", {"ventana": 26, "cada": 1})):
fila = []
for fn in (original, ultima_con_fuga):
ultima_conocida = fn
fila.append(resumen(ic_semanal(walk_forward(**kw)[0]))["IC"])
print(f"{nombre:<18} sin fuga {fila[0]:.4f} con fuga "
f"{fila[1]:.4f}")
ultima_conocida = originalexpansiva semanal sin fuga 0.0176 con fuga 0.0272
móvil 26 semanas sin fuga 0.0051 con fuga 0.0603Con la ventana expansiva, la fila con fuga es una semana de las más de 500 que hay en el entrenamiento, y aun así el IC sube. Con 26 semanas, es un 4 % del entrenamiento: el modelo aprende sobre todo de la semana que tiene que predecir y el IC se multiplica. Las ventanas cortas son más sensibles a cualquier fuga en el borde, y por eso un walk-forward con ventana móvil que da un resultado excelente merece una revisión especial de la regla del tiempo.
- Prueba los pesos exponenciales con semividas de 13, 26, 52, 104, 208 y 416 semanas (reentreno semanal). ¿Cuál gana? ¿Puedes presentar su IC como resultado fuera de muestra?
Solución
barrido = {h: resumen(ic_semanal(walk_forward(semivida=h,
cada=1)[0]))["IC"]
for h in (13, 26, 52, 104, 208, 416)}
pd.Series(barrido, name="IC").rename_axis("semivida").round(4)semivida
13 0.0048
26 0.0057
52 0.0081
104 0.0120
208 0.0150
416 0.0165
Name: IC, dtype: float64Gana la semivida más larga, que es la que más se parece a la ventana expansiva: el barrido confirma lo de la lección. Pero el IC de la mejor no es un resultado fuera de muestra: se eligió mirando los seis walk-forward. Hay que registrarlo como el mejor de seis intentos y descontar la selección (lección 5.4.4), o elegir la semivida dentro del walk-forward con datos pasados (un walk-forward anidado). Aquí el barrido dice algo útil, que más historia es mejor; la cifra concreta de la ganadora no.
- Los datos
deriva5_*del nivel (lección 5.10.2) tienen un cambio de signo permanente del efecto de una feature en julio de 2019. Sin hacer cálculos, ¿esperarías que allí ganase la ventana expansiva o la móvil? ¿Qué necesitarías para elegir sin mirar el resultado?
Solución
Allí debería ganar una ventana móvil o con pesos: el cambio es grande (el efecto pasa de +8 a −8 puntos básicos por unidad) y permanente, así que después de unos meses la historia anterior a 2019 enseña al modelo exactamente lo contrario de lo que ocurre. La expansiva tardaría años en diluirlo. Para elegir sin mirar el resultado hace falta una regla fijada de antemano: un detector de deriva (la media móvil del IC del modelo, como en el README del nivel, o pruebas de cambio estructural de la lección 2.1.7) que dispare el paso a una ventana corta, evaluado todo junto en un walk-forward. La lección general: la ventana se elige por la naturaleza de los cambios (oscilaciones frente a rupturas) y por la cantidad de señal, y se valida como un procedimiento completo.
Respuesta plegada en cada pregunta
Autoevaluación
En el panel, la etiqueta
ret_sig_semanade la fecha w termina en la fecha w+1. ¿Con qué semanas puede entrenarse un modelo que decide al cierre de la fecha w?- ACon todas hasta la w incluida
- BSolo con las de años anteriores
- CCon las semanas cuya etiqueta ya terminó: hasta la w−1 (que termina justo en w) o, dejando un margen como el README, hasta la w−2
- DCon las de la w+1 en adelante
Ver respuesta
Respuesta C. Con las semanas cuya etiqueta ya terminó: hasta la w−1 (que termina justo en w) o, dejando un margen como el README, hasta la w−2
La etiqueta de la propia semana w es lo que se quiere predecir. Incluirla es una fuga: en el ejercicio 1 sube el IC de 0,018 a 0,027 con ventana expansiva y de 0,005 a 0,060 con una ventana de 26 semanas.
Ventana expansiva reentrenada cada año: IC 0,0185 (t = 4,9). Ventana móvil de un año reentrenada cada semana: IC 0,0039 (t = 1,0). ¿Por qué pierde la móvil, si los coeficientes verdaderos cambian semana a semana?
- APorque la ventana móvil tiene un error de programación
- BPorque con un IC verdadero de unas centésimas, un año de datos estima los coeficientes con tanto ruido que el error de estimación supera con mucho lo que se gana siguiendo los cambios
- CPorque reentrenar cada semana provoca fugas
- DPorque las ventanas móviles solo sirven en datos diarios
Ver respuesta
Respuesta B. Porque con un IC verdadero de unas centésimas, un año de datos estima los coeficientes con tanto ruido que el error de estimación supera con mucho lo que se gana siguiendo los cambios
Es el equilibrio sesgo-varianza. La trayectoria del coeficiente de momentum con ventana de un año oscila tanto como la verdadera, pero no se correlaciona con ella (−0,10); la expansiva es casi constante, sesgada pero con menos error (6,0 pb frente a 9,0).
Reentrenar cada semana en lugar de cada enero (ventana expansiva) cambia el IC de 0,0185 a 0,0176, con una diferencia pareada de t = 1,2. ¿Qué se concluye?
- AQue reentrenar cada semana empeora el modelo
- BQue hay que reentrenar cada día
- CQue la ventana expansiva no aprende
- DQue con ventana expansiva la frecuencia apenas importa: un año más de datos sobre diez mueve poco los coeficientes, y la diferencia no se distingue del ruido
Ver respuesta
Respuesta D. Que con ventana expansiva la frecuencia apenas importa: un año más de datos sobre diez mueve poco los coeficientes, y la diferencia no se distingue del ruido
Una diferencia de t = 1,2 no es evidencia de nada. Con ventana expansiva, cada reentrenamiento añade una fracción pequeña de datos; reentrenar a menudo cuesta cómputo y no compra precisión.
En las 65 semanas de régimen
rebote(el momentum verdadero se invierte, ×−2,5), ¿qué esquema protege del crash del momentum?- ANinguno: los rebotes duran unos meses y ningún reentrenamiento detecta a tiempo el cambio de signo; la protección tendría que venir del modelo (el régimen como feature) o del riesgo, no de la ventana
- BLa ventana móvil de un año, porque se adapta
- CLa expansiva con reentrenamiento semanal
- DLos pesos exponenciales con semivida de 13 semanas
Ver respuesta
Respuesta A. Ninguno: los rebotes duran unos meses y ningún reentrenamiento detecta a tiempo el cambio de signo; la protección tendría que venir del modelo (el régimen como feature) o del riesgo, no de la ventana
Para cuando un año de datos refleja un rebote, el rebote ha terminado. La adaptación por ventana llega tarde y paga el ruido en todas las demás semanas.
Se prueban seis semividas para los pesos exponenciales y se informa del IC de la mejor. ¿Qué hay que hacer con ese número?
- ATratarlo como el mejor de seis intentos: registrarlos todos y descontar la selección (Deflated Sharpe, lección 5.4.4), o elegir la semivida dentro del propio walk-forward con datos pasados
- BNada: es fuera de muestra porque viene de un walk-forward
- CMultiplicarlo por seis
- DUsar la peor de las seis
Ver respuesta
Respuesta A. Tratarlo como el mejor de seis intentos: registrarlos todos y descontar la selección (Deflated Sharpe, lección 5.4.4), o elegir la semivida dentro del propio walk-forward con datos pasados
Un walk-forward es fuera de muestra para un procedimiento fijado de antemano. Si el procedimiento se elige mirando el resultado del walk-forward, el resultado ya no lo es.
Ideas clave
- Regla del walk-forward en un panel: en la fecha de decisión solo se entrena con filas cuya etiqueta ya terminó (
fin_sig_semanaanterior a la fecha). Olvidarla con una ventana móvil de 26 semanas multiplica el IC por doce. - Con estadísticos suficientes por semana (X′X, X′y), un ridge se reentrena miles de veces en segundos; coincide con scikit-learn.
- Ventana expansiva reentrenada cada enero: IC 0,0185 (t = 4,9), el dato del README. Reentrenar más a menudo no mejora (0,0172-0,0176).
- Las ventanas móviles pierden, y más cuanto más cortas: 0,0143 con cinco años, 0,0084 con uno. Expansiva frente a móvil de un año, ambas semanales: diferencia pareada de 0,014 con t = 3,9.
- Ninguna ventana corta se adapta a los rebotes (crash del momentum): el cambio dura menos de lo que tarda en verse en los datos, y la expansiva sigue siendo la mejor también allí.
- Con poca señal manda la varianza: la estimación de un año sigue mal a un coeficiente que cambia; la expansiva, sesgada y estable, predice mejor.