Lección 5.3.5 · 55 min
Muestreo por eventos: el filtro CUSUM simétrico
Un modelo que mira el mercado cada minuto pasa la mayor parte del tiempo observando ruido. El filtro CUSUM simétrico muestrea solo cuando el precio ha acumulado un movimiento suficiente desde la última observación. Esta lección lo construye sobre FNX, estudia el umbral fijo y el escalado por la volatilidad, mide cuántos eventos da cada umbral y en qué régimen caen, y lo compara con muestrear cada cierto tiempo y con una regla de bandas.
- Módulo
- 5.3 · Etiquetado y muestreo
- Duración
- 55 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
Cuándo observar
Las lecciones 5.3.1 y 5.3.2 etiquetaban observaciones; falta decidir qué observaciones. La opción por defecto es una por barra: cada minuto, cada día. Tiene dos problemas. El primero es que la mayoría de esas observaciones son ruido: un minuto de mediodía en el que el precio no se mueve no enseña nada sobre la señal que se busca. El segundo es que observaciones consecutivas son casi iguales: features que cambian poco y etiquetas que se solapan, el problema que medía la lección 5.3.4.
La lección 1.4.5 atacaba un problema parecido al construir las barras: barras de ticks, de volumen o de dólar, que se cierran cuando hay actividad y no cuando pasa el reloj. El muestreo por eventos (López de Prado, 2018, sección 2.5.2) va un paso más allá: dada una serie, sea de barras de tiempo o de actividad, solo se observa cuando ha pasado algo relevante. El filtro más usado es el CUSUM simétrico, una variante de las sumas acumuladas de Page (1954) que la lección 4.2.5 usaba para detectar que una estrategia había dejado de funcionar.
El filtro CUSUM simétrico
Con el log-rendimiento de cada barra, el filtro lleva dos sumas, una de subidas y otra de bajadas, recortadas en cero:
Cuando hay un evento alcista; cuando , uno bajista. Tras cada evento las dos sumas vuelven a cero. El recorte en cero hace que una bajada no se compense con una subida anterior: mide la subida acumulada desde el último mínimo relativo, y el evento llega cuando esa subida supera . La versión de López de Prado resta a su media esperada, que en rendimientos de un minuto es cero a todos los efectos.
Fuente Mermaid
stateDiagram-v2 [*] --> Acumular Acumular --> Acumular: sin pasar de h Acumular --> Evento: S+ > h o S− < −h Evento --> Acumular: reinicio a 0
Los datos son los de FNX (datos sintéticos del nivel 5): un año de barras de 1 minuto de un futuro ficticio con un
régimen latente de tendencia o de reversión, que hf5_verdad.parquet da minuto a minuto. Como en las lecciones
anteriores, el régimen solo se usa para medir.
import numpy as np
import pandas as pd
barras = pd.read_parquet("hf5_barras_1min.parquet")
verdad = pd.read_parquet("hf5_verdad.parquet")
n_ses = len(barras) // 390
C = barras["cierre"].to_numpy().reshape(n_ses, 390)
tend = (verdad["regimen"] == "tendencia").to_numpy()
tend = tend.reshape(n_ses, 390) # la verdad: solo para medir
lr = np.diff(np.log(C), axis=1) # 389 por sesión
sig = (pd.Series(lr.ravel()).ewm(span=100).std()
.to_numpy().reshape(n_ses, 389))
print(f"minutos en tendencia: {tend.mean():.1%}")minutos en tendencia: 35.3%El 35,3 % de los minutos del año está en tendencia. El umbral del modelo primario del nivel es , con la desviación EWMA (span de 100 minutos) de los rendimientos de 1 minuto calculada hasta el propio minuto: se conoce al cierre del minuto que dispara el evento, que es cuando se decide.
def cusum(lr, umbral):
"""Filtro CUSUM simétrico, sesión a sesión.
lr: log-rendimientos (sesiones x minutos); umbral: misma forma
(o un número). Acumula subidas en s_pos y bajadas en s_neg desde
el último evento; cuando una pasa del umbral, evento y las dos
vuelven a 0. Devuelve sesión, minuto (índice del cierre que
dispara), lado (+1 rotura al alza) y el umbral usado."""
h = np.broadcast_to(umbral, lr.shape)
filas = []
for d in range(lr.shape[0]):
s_pos = s_neg = 0.0
for m in range(lr.shape[1]):
if not np.isfinite(h[d, m]):
continue
s_pos = max(0.0, s_pos + lr[d, m])
s_neg = min(0.0, s_neg + lr[d, m])
if s_pos > h[d, m] or s_neg < -h[d, m]:
lado = 1 if s_pos > h[d, m] else -1
filas.append((d, m + 1, lado, h[d, m]))
s_pos = s_neg = 0.0
return pd.DataFrame(
filas, columns=["sesion", "minuto", "lado", "umbral"])
ev = cusum(lr, 4.0 * sig)
print(len(ev), "eventos,", round(len(ev) / n_ses, 1), "por sesión")
print("minutos entre eventos (mediana):",
ev.groupby("sesion").minuto.diff().median())
ev[ev.sesion == 133].head(6).assign(
umbral_pb=lambda x: (1e4 * x.umbral).round(1)).drop(
columns="umbral")6550 eventos, 26.5 por sesión
minutos entre eventos (mediana): 13.0
sesion minuto lado umbral_pb
3525 133 4 -1 14.2
3526 133 6 1 16.0
3527 133 11 1 16.6
3528 133 38 1 18.1
3529 133 45 1 18.8
3530 133 49 1 19.6Salen los 6.550 eventos del README, 26,5 por sesión, con una mediana de 13 minutos entre uno y el siguiente. La figura de la cabecera muestra la sesión del 15 de enero de 2025, la de los ejemplos de las lecciones 5.3.2 y 5.3.4. En la tendencia alcista de la mañana, casi todos los eventos son alcistas: el precio sube, el filtro acumula y dispara una y otra vez del mismo lado. En la reversión de la tarde, alcistas y bajistas alternan.
Los tres primeros eventos llegan en los primeros 11 minutos con umbrales de 14 a 17 pb. La EWMA arrastra la volatilidad de la tarde anterior, más baja que la de la apertura, así que el umbral es estrecho justo cuando el precio más se mueve. Es el mismo retraso que la lección 5.3.2 medía en las barreras.
Umbral fijo o escalado
La alternativa más simple es un umbral fijo en puntos básicos. Con el mismo nivel medio (4 veces la mediana de , 16,9 pb):
h_fijo = 4.0 * np.nanmedian(sig)
ev_fijo = cusum(lr, h_fijo)
print(f"umbral fijo: {1e4 * h_fijo:.1f} pb; {len(ev_fijo)} eventos")
vol_ses = lr.std(axis=1) # volatilidad de cada sesión
tercil = pd.qcut(vol_ses, 3, labels=["baja", "media", "alta"])
por_sesion = pd.DataFrame({
"tercil": tercil,
"escalado": np.bincount(ev.sesion, minlength=n_ses),
"fijo": np.bincount(ev_fijo.sesion, minlength=n_ses)})
por_sesion.groupby("tercil", observed=True).mean().round(1)umbral fijo: 16.9 pb; 6897 eventos
escalado fijo
tercil
baja 26.9 19.4
media 26.6 27.6
alta 26.0 36.9Con umbral fijo, las sesiones tranquilas (tercil bajo de volatilidad) dan 19,4 eventos y las agitadas 36,9: casi el doble. Un modelo entrenado con esos eventos aprende sobre todo de los días agitados, y un mismo movimiento de 17 pb significa cosas distintas según el día. Con el umbral escalado, cada tercil da entre 26,0 y 26,9 eventos: el filtro exige el mismo movimiento en unidades de volatilidad.
Para saber qué eventos son mejores hace falta etiquetarlos. Con la triple barrera de la lección 5.3.2 y el lado de la rotura (el modelo primario de momentum):
def acierto(ev, h=30):
"""Triple barrera ±σ·√h (5.3.2); 1 si el lado acierta."""
out = []
for d, m, lado in ev[["sesion", "minuto", "lado"]].itertuples(
index=False):
b = sig[d, m - 1] * np.sqrt(h)
ult = min(m + h, 389)
tramo = np.log(C[d, m + 1:ult + 1] / C[d, m])
golpes = np.flatnonzero(np.abs(tramo) >= b)
j = golpes[0] if len(golpes) else len(tramo) - 1
out.append(int(len(tramo) > 0 and lado * tramo[j] > 0))
return np.array(out)
def resumen(ev):
a = acierto(ev)
t = tend[ev.sesion, ev.minuto]
return {"por_sesion": len(ev) / n_ses, "en_tendencia": t.mean(),
"acierto": a.mean(), "acierto_tend": a[t].mean(),
"acierto_rev": a[~t].mean()}
pd.DataFrame({"escalado k = 4": resumen(ev),
"fijo": resumen(ev_fijo)}).T.round(3) por_sesion en_tendencia acierto acierto_tend acierto_rev
escalado k = 4 26.518 0.388 0.507 0.604 0.446
fijo 27.923 0.388 0.504 0.588 0.450Los dos filtros dan una fracción de eventos en tendencia del 38,8 % y un acierto parecido: 50,7 % el escalado y 50,4 % el fijo, una diferencia de medio error típico. El escalado acierta algo más en tendencia (60,4 % frente a 58,8 %). La razón principal para escalar no es el acierto medio sino la composición de la muestra.
Umbral y número de eventos
El umbral decide cuántos eventos hay. Barriéndolo:
barrido = pd.DataFrame({k: resumen(cusum(lr, k * sig))
for k in (2, 3, 4, 5, 6, 8)}).T
barrido.index.name = "k"
barrido.round(3) por_sesion en_tendencia acierto acierto_tend acierto_rev
k
2 68.575 0.364 0.502 0.565 0.465
3 40.308 0.373 0.503 0.575 0.460
4 26.518 0.388 0.507 0.604 0.446
5 18.785 0.396 0.506 0.612 0.436
6 13.976 0.413 0.507 0.615 0.431
8 8.636 0.441 0.507 0.637 0.404- Los eventos caen deprisa con el umbral: 68,6 por sesión con , 26,5 con y 8,6 con . Para un paseo aleatorio, el tiempo esperado hasta que una suma acumulada recorre crece con ; en FNX, de a los eventos se dividen por 2,6 y de 4 a 8 por 3,1, algo menos que 4 porque la sesión se corta al cierre y los episodios de tendencia aceleran el recorrido.
- Los eventos se concentran en tendencia: del 36,4 % con al 44,1 % con , frente al 35,3 % de los minutos. Un movimiento grande es más probable durante una tendencia, y el filtro lo encuentra.
- El acierto total no se mueve (del 50,2 % al 50,7 %), porque a la vez que el acierto en tendencia sube del 56,5 % al 63,7 %, el de reversión baja del 46,5 % al 40,4 %: en un régimen de vuelta a la media, cuanto mayor es la desviación, más fuerte es la vuelta, y el lado de momentum se equivoca más.
El umbral es, por tanto, un compromiso entre cantidad y composición: con hay muestras más «puras» pero un tercio de las de , y con ellas más varianza en todo lo que se estime. Igual que la anchura de las barreras (5.3.2), se fija por diseño y no mirando el acierto en la muestra de prueba.
Por eventos o por tiempo
¿Gana algo el CUSUM frente a observar cada cierto tiempo? Con el lado de momentum calculado sobre el intervalo anterior y la misma etiqueta:
def por_tiempo(n):
"""Una muestra cada n minutos; lado = signo de los n anteriores."""
filas = []
for d in range(n_ses):
for m in range(n, 390, n):
r = np.log(C[d, m] / C[d, m - n])
if r != 0:
filas.append((d, m, int(np.sign(r))))
return pd.DataFrame(filas, columns=["sesion", "minuto", "lado"])
tiempo = pd.DataFrame({f"cada {n} min": resumen(por_tiempo(n))
for n in (5, 15, 30)}).T
tiempo.round(3) por_sesion en_tendencia acierto acierto_tend acierto_rev
cada 5 min 75.332 0.353 0.503 0.556 0.474
cada 15 min 24.664 0.353 0.505 0.589 0.460
cada 30 min 11.879 0.354 0.511 0.592 0.466Cada 15 minutos da 24,7 observaciones por sesión, casi las mismas que el CUSUM con . Las diferencias:
- El muestreo por tiempo reparte las observaciones como los minutos: el 35,3 % en tendencia, ni más ni menos. El CUSUM las inclina hacia la tendencia (38,8 %).
- El acierto total es casi el mismo: 50,5 % cada 15 minutos y 50,7 % con el CUSUM. Con unos 6.000 eventos cada uno, el error típico de cada acierto ronda los 0,6 puntos: no hay diferencia medible. El CUSUM no hace bueno a un primario que no lo es; cambia qué momentos se observan.
Donde sí se ve la diferencia es en cuándo se observa:
def por_media_hora(ev):
return np.bincount(ev.minuto // 30, minlength=13) / n_ses
hora = pd.DataFrame({
"CUSUM escalado": por_media_hora(ev),
"CUSUM fijo": por_media_hora(ev_fijo),
"cada 15 min": por_media_hora(por_tiempo(15))},
index=[f"{9 + (30 + 30 * i) // 60}:{(30 + 30 * i) % 60:02d}"
for i in range(13)])
hora.round(1) CUSUM escalado CUSUM fijo cada 15 min
9:30 3.0 4.1 1.0
10:00 1.8 2.7 2.0
10:30 1.6 2.2 2.0
11:00 1.6 1.9 2.0
11:30 1.7 1.8 2.0
12:00 1.9 1.8 2.0
12:30 1.9 1.8 2.0
13:00 2.0 1.8 2.0
13:30 2.1 1.8 2.0
14:00 2.1 1.8 2.0
14:30 2.1 1.9 2.0
15:00 2.2 2.1 2.0
15:30 2.4 2.3 2.0El muestreo por tiempo es plano (la primera media hora tiene una sola observación porque el intervalo empieza a las 9:45). El CUSUM sigue a la actividad: con umbral fijo, 4,1 eventos por sesión en la primera media hora y 1,8 a mediodía, la misma U que dibujaban las barras de volumen de la lección 1.4.5. Escalado, la U se aplana pero no desaparece: 3,0 en la apertura por el retraso de la EWMA y una subida suave hacia el cierre. Si se quisiera un muestreo uniforme en unidades de volatilidad, el umbral tendría que usar el perfil intradía, como la barrera de la lección 5.3.2.
Bandas frente a CUSUM: por qué el reinicio importa
Una regla intuitiva para muestrear es la de bandas: un evento cada vez que el precio sale de una banda alrededor de su media móvil (al estilo de las bandas de Bollinger). Con una media exponencial de 30 minutos y bandas de :
lc = np.log(C)
ema = pd.DataFrame(lc.T).ewm(span=30).mean().to_numpy().T
z = np.full_like(lc, np.nan)
z[:, 1:] = (lc[:, 1:] - ema[:, 1:]) / (sig * np.sqrt(30))
fuera_arr, fuera_ab = z > 0.5, z < -0.5
cruce = np.zeros_like(lc, dtype=int)
cruce[:, 1:] = (np.where(fuera_arr[:, 1:] & ~fuera_arr[:, :-1], 1, 0)
- np.where(fuera_ab[:, 1:] & ~fuera_ab[:, :-1], 1, 0))
d_b, m_b = np.nonzero(cruce)
bandas = pd.DataFrame({"sesion": d_b, "minuto": m_b,
"lado": cruce[d_b, m_b]})
def racimos(ev):
gap = ev.groupby("sesion").minuto.diff().dropna()
return {"por_sesion": len(ev) / n_ses,
"a_3_min_o_menos": (gap <= 3).mean(),
"mediana_min": gap.median()}
pd.DataFrame({"bandas ±0,5": racimos(bandas),
"CUSUM k = 4": racimos(ev)}).T.round(3) por_sesion a_3_min_o_menos mediana_min
bandas ±0,5 22.514 0.149 11.0
CUSUM k = 4 26.518 0.031 13.0Las bandas dan 22,5 eventos por sesión, casi como el CUSUM, pero el 14,9 % llega a 3 minutos o menos del anterior, frente al 3,1 % del CUSUM. Cuando el precio oscila alrededor del borde de la banda, la regla dispara cada vez que lo cruza: eventos en racimo, casi idénticos, que solapan etiquetas y repiten features. El CUSUM, al reiniciarse, exige un movimiento completo de desde el último evento, y eso los separa.
El lado del evento lleva información
Un efecto lateral del muestreo por eventos es que la secuencia de lados ya dice algo del régimen:
misma = (ev.sesion == ev.sesion.shift()).to_numpy()
igual = (ev.lado == ev.lado.shift()).to_numpy()[misma]
t_ev = tend[ev.sesion, ev.minuto][misma]
print(f"mismo lado que el evento anterior: {igual.mean():.3f}")
print(f" en tendencia: {igual[t_ev].mean():.3f}")
print(f" en reversión: {igual[~t_ev].mean():.3f}")mismo lado que el evento anterior: 0.518
en tendencia: 0.624
en reversión: 0.451En conjunto, el 51,8 % de los eventos repite el lado del anterior de la misma sesión. En tendencia, el 62,4 %; en reversión, el 45,1 %. Es la firma visible en la figura de la cabecera y es información conocida en el momento de cada evento: una feature candidata para el meta-modelo (ejercicio 2 de la lección 5.3.3, donde resulta redundante con el imbalance del flujo).
El mismo filtro sobre la cinta de operaciones
Nada en el filtro exige barras de 1 minuto: funciona con cualquier serie de rendimientos. Sobre las operaciones regulares de NOVA del 2 de junio de 2025 (datos sintéticos del nivel 1, lección 1.1.1), operación a operación y con un umbral fijo de 10 pb:
op = pd.read_parquet("nova_operaciones.parquet")
op = op[op["condicion"] == "regular"].reset_index(drop=True)
r_op = np.diff(np.log(op["precio"].to_numpy()))[None, :]
ev_op = cusum(r_op, 0.001) # 10 pb, umbral fijo
hora_ny = op["ts"].iloc[ev_op.minuto].dt.tz_convert(
"America/New_York")
print(len(op), "operaciones,", len(ev_op), "eventos")
franja = hora_ny.dt.floor("30min").dt.strftime("%H:%M")
franja.value_counts().sort_index().to_frame("eventos").T50938 operaciones, 165 eventos
ts 09:30 10:00 10:30 11:00 11:30 12:00 12:30 13:00 13:30 14:00 14:30 15:00 15:30
eventos 23 18 10 10 6 13 6 8 16 9 9 13 24De 50.938 operaciones salen 165 eventos, con 23 en la primera media hora, 6 en la de las 11:30 y 24 en la última. Es la U de la actividad que las barras de volumen y de dólar dibujaban en la lección 1.4.5, conseguida por otro camino: allí la barra se cerraba al acumular volumen; aquí, al acumular movimiento. Con datos de operaciones hay un matiz: el rebote entre el bid y el ask suma y resta rendimientos sin que el precio se mueva de verdad. El CUSUM lo tolera mejor que una regla de cruces porque el rebote se cancela dentro de y , pero con umbrales del orden de la horquilla dispararía por ruido de microestructura.
Errores típicos
- Umbral calculado con el futuro. Una volatilidad de la sesión completa, o una EWMA centrada, hace que el filtro dispare «sabiendo» cuánto se va a mover el día.
- Elegir el umbral por el acierto en la muestra de prueba. Es un parámetro más de la búsqueda (2.5.4).
- Entrar al precio que dispara el evento. El evento se conoce al cierre del minuto; en real, la orden se ejecuta después (en FNX, la apertura del minuto siguiente coincide con ese cierre salvo el redondeo al tick, pero en un activo con horquilla o con barras más largas no).
- Olvidar el reinicio diario. Si las sumas pasan de una sesión a la siguiente, el salto de la noche dispara eventos en la apertura que no se pueden operar al precio del cierre.
Ejercicios
- Compara el acierto del modelo primario en los eventos de la primera media hora con el del resto de la sesión. ¿Qué relación tiene con el retraso de la EWMA?
Solución
a = acierto(ev)
apertura = (ev.minuto < 30).to_numpy()
t_a = tend[ev.sesion, ev.minuto]
for nombre, sel in (("apertura", apertura), ("resto", ~apertura)):
print(f"{nombre:>8}: {sel.sum()} eventos, acierto"
f" {a[sel].mean():.3f}, en tendencia {t_a[sel].mean():.3f}")apertura: 743 eventos, acierto 0.526, en tendencia 0.396
resto: 5807 eventos, acierto 0.505, en tendencia 0.387Los 743 eventos de la primera media hora aciertan el 52,6 % y los demás el 50,5 %, con una fracción en tendencia casi igual (39,6 % frente a 38,7 %). La diferencia es de poco más de un error típico (unos 1,8 puntos con 743 eventos): no se puede afirmar que la apertura sea mejor. Lo que sí es seguro es que hay muchos más eventos (743, el 11 % del total en el 8 % del tiempo) porque la EWMA va retrasada y el umbral es estrecho cuando la volatilidad es más alta. Si esos eventos no se quieren, la corrección es el perfil intradía (5.3.2), no subir para todo el día.
- Repite el CUSUM sobre la cinta de NOVA con umbrales de 5 y 20 pb. ¿Cuántos eventos salen y qué le pasa a la forma de U?
Solución
for h in (0.0005, 0.002):
e_h = cusum(r_op, h)
hny = op["ts"].iloc[e_h.minuto].dt.tz_convert("America/New_York")
franja = hny.dt.floor("30min").dt.strftime("%H:%M")
n = franja.value_counts().sort_index()
print(f"{1e4 * h:.0f} pb: {len(e_h)} eventos;"
f" 9:30 {n.get('09:30', 0)}, 11:30 {n.get('11:30', 0)},"
f" 15:30 {n.get('15:30', 0)}")5 pb: 417 eventos; 9:30 59, 11:30 19, 15:30 57
20 pb: 70 eventos; 9:30 9, 11:30 2, 15:30 10Con 5 pb salen 417 eventos (59 en la primera media hora, 19 en la de las 11:30 y 57 en la última); con 20 pb, solo 70 (9, 2 y 10). La forma de U se mantiene con los tres umbrales, porque la sigue la actividad del mercado y no el umbral. Lo que cambia es la resolución: con 20 pb hay franjas con dos eventos en toda la sesión, demasiado pocos para medir nada, y con 5 pb el umbral se acerca a la horquilla de NOVA (un céntimo en una acción de 53 USD son unos 2 pb) y el rebote entre bid y ask empieza a pesar.
- La lección 4.2.5 usaba un CUSUM para apagar una estrategia cuando sus rendimientos empeoraban. ¿Qué cambia entre ese uso y el de esta lección, y por qué en aquel se resta una media y en este no?
Solución
La recursión es la misma: sumas acumuladas de desviaciones, recortadas en cero, comparadas con un umbral. Cambia el objetivo. En 4.2.5 el CUSUM es un contraste secuencial de un cambio de media: a cada rendimiento tipificado se le compara con un valor de referencia , a medio camino entre la media del backtest y cero, de modo que la suma solo crece de forma sostenida cuando los rendimientos quedan por debajo de lo esperado; cuando pasa del umbral, se da la alarma y se para. El umbral se calibra con el compromiso entre falsas alarmas y retraso. Aquí el filtro no busca un cambio permanente sino movimientos acumulados de cierto tamaño en cualquier dirección, y tras cada evento se reinicia y sigue: es una regla de muestreo. Solo se resta la media esperada de los rendimientos, que en un minuto es prácticamente cero, porque lo que interesa es el movimiento, no la desviación respecto a un rendimiento objetivo. El umbral se elige por el número y el tipo de eventos que se quieren, no por una tasa de falsas alarmas.
Respuesta plegada en cada pregunta
Autoevaluación
¿Qué hace el filtro CUSUM simétrico después de disparar un evento?
- ASigue acumulando desde el valor que tenía
- BPone a cero las dos sumas, la de subidas y la de bajadas: el siguiente evento exige otro movimiento completo desde ese punto
- CCambia el umbral
- DEspera un número fijo de minutos
Ver respuesta
Respuesta B. Pone a cero las dos sumas, la de subidas y la de bajadas: el siguiente evento exige otro movimiento completo desde ese punto
El reinicio es lo que evita los racimos: una regla de bandas dispara cada vez que el precio cruza la banda, y si el precio oscila alrededor de ella dispara muchas veces seguidas. En FNX, el 14,9 % de los eventos de bandas llega a 3 minutos o menos del anterior, frente al 3,1 % del CUSUM.
Con un umbral fijo de 16,9 pb, las sesiones del tercil de volatilidad más bajo dan 19,4 eventos y las del más alto 36,9. Con el umbral de 4σ (EWMA), 26,9 y 26,0. ¿Qué consecuencia tiene para un modelo?
- ANinguna
- BEl umbral fijo es mejor porque da más eventos
- CEl umbral escalado mira al futuro
- DCon umbral fijo, los días agitados dominan la muestra de entrenamiento; con umbral escalado, cada sesión aporta un número parecido de eventos y el movimiento que se exige es comparable
Ver respuesta
Respuesta D. Con umbral fijo, los días agitados dominan la muestra de entrenamiento; con umbral escalado, cada sesión aporta un número parecido de eventos y el movimiento que se exige es comparable
Un umbral fijo mezcla dos cosas: la señal que se busca y el nivel de volatilidad. Escalar con una volatilidad conocida en el momento hace el muestreo comparable entre días tranquilos y agitados.
Al subir el umbral de k = 2 a k = 8, la fracción de eventos en tendencia sube del 36,4 % al 44,1 %, pero el acierto total del primario se queda en el 50,2-50,7 %. ¿Por qué?
- APorque a la vez que el acierto en tendencia sube (del 56,5 % al 63,7 %), el de reversión baja (del 46,5 % al 40,4 %): un movimiento grande en reversión es más probable que se deshaga
- BPorque el umbral no influye en el acierto
- CPorque hay pocos eventos
- DPor un error en la triple barrera
Ver respuesta
Respuesta A. Porque a la vez que el acierto en tendencia sube (del 56,5 % al 63,7 %), el de reversión baja (del 46,5 % al 40,4 %): un movimiento grande en reversión es más probable que se deshaga
El CUSUM selecciona movimientos grandes. En tendencia, un movimiento grande suele continuar; en reversión a la media, cuanto mayor es la desviación, más fuerte es la vuelta. Los dos efectos se compensan en el agregado.
Muestrear cada 15 minutos da 24,7 observaciones por sesión, casi como el CUSUM (26,5). ¿Qué diferencia se mide en FNX?
- AEl CUSUM duplica el acierto
- BEl muestreo por tiempo no tiene sesgo de anticipación y el CUSUM sí
- CEl muestreo por tiempo reparte las observaciones como los minutos (35,3 % en tendencia); el CUSUM las inclina hacia la tendencia (38,8 %). El acierto total es casi el mismo (50,5 % frente a 50,7 %)
- DNinguna
Ver respuesta
Respuesta C. El muestreo por tiempo reparte las observaciones como los minutos (35,3 % en tendencia); el CUSUM las inclina hacia la tendencia (38,8 %). El acierto total es casi el mismo (50,5 % frente a 50,7 %)
El CUSUM no hace bueno al primario: cambia qué momentos se observan. Su ventaja es muestrear cuando el mercado se mueve (más en la apertura, menos a mediodía) y no repetir observaciones casi iguales.
¿En qué se diferencia el CUSUM de esta lección del de Page de la lección 4.2.5?
- AEs la misma recursión de sumas acumuladas recortadas en cero, pero aquí se usa para muestrear (se reinicia tras cada evento y sigue) y allí para detectar un cambio de media y parar una estrategia
- BSon algoritmos sin relación
- CEl de Page no tiene umbral
- DEl de esta lección solo funciona con precios
Ver respuesta
Respuesta A. Es la misma recursión de sumas acumuladas recortadas en cero, pero aquí se usa para muestrear (se reinicia tras cada evento y sigue) y allí para detectar un cambio de media y parar una estrategia
Page (1954) propuso el CUSUM para control de calidad: alarma cuando la suma de desviaciones respecto a la media esperada pasa de un umbral. López de Prado lo usa sobre rendimientos con media esperada cero para decidir cuándo muestrear.
Ideas clave
- Muestrear por eventos es decidir cuándo observar: solo cuando el precio ha acumulado un movimiento de tamaño h desde el último evento.
- El filtro CUSUM simétrico acumula subidas y bajadas por separado, recortadas en cero, y reinicia las dos tras cada evento: no dispara en racimo cuando el precio oscila alrededor de un nivel (3,1 % de eventos a 3 minutos o menos, frente al 14,9 % de una regla de bandas).
- Con umbral fijo, el número de eventos sigue a la volatilidad de cada día (19,4 frente a 36,9 por sesión); escalado con una EWMA, se mantiene en unos 26,5.
- Subir el umbral reduce los eventos (de 68,6 a 8,6 por sesión) y los concentra en tendencia (del 36 % al 44 %), sin cambiar el acierto total: la reversión empeora a la vez.
- Frente a muestrear cada 15 minutos, el CUSUM sigue la actividad (3,0 eventos en la primera media hora, 1,6 a media mañana) y se inclina hacia la tendencia; el acierto total es casi el mismo.
- El filtro sirve con cualquier serie: sobre la cinta de NOVA dibuja la misma U intradía que las barras de actividad de la lección 1.4.5.