Lección 5.2.1 · 70 min

Features de precio, volumen y microestructura

Casi todo modelo de trading empieza por transformar precios y volúmenes en números comparables entre activos y entre fechas. Esta lección construye las features clásicas sobre un panel de 400 acciones con la verdad publicada, mide cuánto anticipa cada una y pasa a la microestructura: el desequilibrio de órdenes y el VPIN de un futuro en barras de un minuto, calculados con el lado agresor verdadero y con los clasificadores que hay que usar cuando no se tiene.

Fig. — IC de rangos medio del rendimiento pasado con el de la semana siguiente, por horizonte (400 acciones, 729 semanas): la reversión plantada a 5 sesiones es la más fuerte
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

Una feature es una función del pasado

En el machine learning financiero, una feature (variable explicativa, predictor) es un número que se calcula para cada activo y cada fecha con información disponible en esa fecha, y que se espera relacionado con lo que pasará después: el rendimiento, su signo, su magnitud. La definición tiene dos partes y las dos importan. «Con información disponible» excluye cualquier dato posterior, aunque sea de un minuto (lección 0.1.1). «Relacionado con lo que pasará» es una hipótesis que hay que medir, y casi siempre la respuesta es «un poco».

Esta lección trabaja con las familias que salen solo de precios y volúmenes. Los fundamentales y los datos alternativos son la lección 5.2.2; la normalización transversal sin fugas, la 5.2.4.

FamiliaEjemplosQué intenta capturarTrampa típica
Rentabilidades1, 5, 21, 63 sesiones; momentum 12-1Reversión a corto plazo, tendencia a medioHorizontes solapados que repiten la misma información
VolatilidadDesviación de 21 y 63 sesionesRiesgo, régimenConfundir anticipar la magnitud con anticipar la dirección
VolumenVolumen relativo, rotaciónAtención, liquidez, confirmaciónUnidades absolutas (acciones) no comparables
IndicadoresRSI, distancia al máximo, mediasLo mismo que las rentabilidades, en otra escalaUnidades de precio; redundancia entre ellos
MicroestructuraDesequilibrio de órdenes, VPINFlujo informado, toxicidadClasificar el lado del agresor sin tenerlo

El recorrido de los datos hasta el modelo es siempre el mismo, y las fugas se cuelan en las flechas de la izquierda:

Precios y volumenFeatures en tNormalizaciónModeloEtiqueta t a t+h
Fuente Mermaid
flowchart LR
  P[Precios y volumen] --> F[Features en t]
  F --> N[Normalización]
  N --> M[Modelo]
  E[Etiqueta t a t+h] --> M

El panel y las features recalculadas

Los datos son el panel sintético del nivel 5: 400 acciones ficticias, 3.772 sesiones de la NYSE de 2011 a 2025 y 22 features semanales publicadas con su definición (README de los datos). El generador conoce el rendimiento esperado verdadero de cada acción y semana, así que cada medida de esta lección se puede comparar con la respuesta. Son datos sintéticos: los efectos están plantados y ninguna cifra es evidencia de lo que pasa en un mercado real.

Antes de usar una feature publicada por otro (un proveedor, un compañero, el propio generador), conviene recalcularla desde los precios. Si sale distinta, o la definición no es la que se cree, o hay un problema de alineación:

IN [01]python
import numpy as np
import pandas as pd

px = pd.read_parquet("panel5_precios.parquet")
C = px.pivot(index="fecha", columns="ticker", values="cierre")
V = px.pivot(index="fecha", columns="ticker", values="volumen")
R = C.pct_change()

fe = pd.read_parquet("panel5_features.parquet")
fechas = pd.DatetimeIndex(fe["fecha"].unique())
print(C.shape, len(fechas), "semanas")

mias = {
    "ret_1s": C / C.shift(5) - 1,
    "mom_12_1": C.shift(21) / C.shift(252) - 1,
    "vol_21d": R.rolling(21).std() * np.sqrt(252),
    "volumen_rel": np.log(V.rolling(5).mean()
                          / V.rolling(63).mean()),
}
for nombre, X in mias.items():
    pub = fe.pivot(index="fecha", columns="ticker", values=nombre)
    dif = (X.loc[fechas] - pub).abs().max().max()
    print(f"{nombre:12s} diferencia máxima {dif:.4f}")
OUT [01]Precios del panel y cuatro features recalculadas
(3772, 400) 730 semanas
ret_1s       diferencia máxima 0.0001
mom_12_1     diferencia máxima 0.0001
vol_21d      diferencia máxima 0.0001
volumen_rel  diferencia máxima 0.0007

Las cuatro coinciden hasta la cuarta cifra decimal, que es la precisión con la que se publican; el volumen relativo difiere algo más porque el volumen del archivo está redondeado a tres cifras significativas. Dos detalles hacen que el cálculo sea point-in-time. Todas las ventanas (rolling, shift) terminan en la fila de la fecha, es decir, en el cierre de ese día: la feature se conoce al cierre y solo se puede operar con ella en la sesión siguiente. Y el momentum mom_12_1 mide de t−252t-252 a t−21t-21: salta el último mes a propósito, por la razón que se ve en la siguiente sección.

Rentabilidades a varios horizontes

La medida que se usa en todo el módulo es el IC de rangos (information coefficient): cada semana, la correlación de Spearman entre la feature y el rendimiento de la semana siguiente en las 400 acciones; después, su media en las 729 semanas y un estadístico tt que la divide por su error típico,

t=IC‾sIC/n.t = \frac{\overline{\mathrm{IC}}}{s_{\mathrm{IC}}/\sqrt{n}}.

Usar rangos protege de los valores extremos y mide lo que importa para ordenar acciones. Las etiquetas semanales no se solapan, así que tratar las semanas como observaciones distintas es razonable (con etiquetas de 21 sesiones no lo sería: lección 5.3.4). El oráculo del README, que conoce el rendimiento esperado verdadero, llega a 0,029: ese es el techo de cualquier feature o modelo en estos datos (lección 5.1.1).

IN [02]python
et = pd.read_parquet("panel5_etiquetas.parquet")
Y = et.pivot(index="fecha", columns="ticker",
             values="ret_sig_semana").loc[fechas]

def ic(X, y=Y):
    """IC de rangos semanal: media, t y número de semanas."""
    X = X.reindex(index=fechas, columns=y.columns)
    v = X.rank(axis=1).corrwith(y.rank(axis=1), axis=1)
    v = v.dropna()
    t = v.mean() / v.std() * np.sqrt(len(v))
    return {"ic": round(v.mean(), 4), "t": round(t, 1),
            "semanas": len(v)}

tabla = {f"{h} sesiones": ic(C / C.shift(h) - 1)
         for h in [1, 2, 5, 10, 21, 63, 126, 252]}
tabla["mom_12_1"] = ic(C.shift(21) / C.shift(252) - 1)
pd.DataFrame(tabla).T.astype({"semanas": int})
OUT [02]IC de rangos por horizonte
                  ic    t  semanas
1 sesiones   -0.0040 -1.0      729
2 sesiones   -0.0095 -2.4      729
5 sesiones   -0.0159 -4.3      729
10 sesiones  -0.0109 -2.9      729
21 sesiones  -0.0077 -2.1      729
63 sesiones  -0.0050 -1.6      729
126 sesiones -0.0000 -0.0      729
252 sesiones  0.0053  1.7      729
mom_12_1      0.0077  2.4      729

La figura de la cabecera dibuja la tabla. El generador planta una reversión a 5 sesiones (ret_1s) y un momentum de 12 meses que salta el último (mom_12_1); los demás horizontes no tienen efecto propio. Aun así:

  • Los vecinos heredan. El rendimiento de 2 sesiones es una parte del de 5, y el de 10 lo contiene entero: los dos salen negativos y significativos (t = −2,4 y −2,9) sin efecto propio. El de 1 sesión, con menos información de la semana, se queda en t = −1,0.
  • Los efectos se tapan entre sí. A 126 sesiones, la reversión reciente y el momentum antiguo se compensan y el IC es cero. A 252, el momentum asoma (0,0053, t = 1,7), pero contaminado por el último mes; quitándolo, mom_12_1 sube a 0,0077 (t = 2,4). Esa es la razón del «12-1» clásico de Jegadeesh y Titman (1993)1Nota 1 · N. Jegadeesh y S. Titman, «Returns to Buying Winners and Selling Losers: Implications for Stock Market Efficiency», Journal of Finance 48(1), 1993, pp. 65-91, doi:10.1111/j.1540-6261.1993.tb04702.x., que separaron el momentum de la reversión a un mes documentada por Jegadeesh (1990)2Nota 2 · N. Jegadeesh, «Evidence of Predictable Behavior of Security Returns», Journal of Finance 45(3), 1990, pp. 881-898, doi:10.1111/j.1540-6261.1990.tb05110.x..
  • Un IC significativo no es una señal operable. El mejor, −0,016, es algo más de la mitad del techo del oráculo, y la cartera larga-corta que se construye con señales así no cubre los costes de su rotación semanal (README de los datos; lección 5.1.3).

Con 22 features y 8 horizontes probados, además, un tt de 2 no impresiona: es una búsqueda múltiple (lección 2.1.8). Aquí se puede contrastar con la verdad; en datos reales, no.

La segunda variante habitual divide el rendimiento por la volatilidad de la acción, para que una subida del 5 % en una acción tranquila pese más que en una volátil:

IN [03]python
vol63 = R.rolling(63).std()
for h in [5, 21, 252]:
    bruto = C / C.shift(h) - 1
    ajustado = bruto / (vol63 * np.sqrt(h))
    print(f"{h:3d} sesiones: bruto {ic(bruto)['ic']:+.4f}, "
          f"/vol {ic(ajustado)['ic']:+.4f} "
          f"(t = {ic(ajustado)['t']:+.1f})")
OUT [03]Rendimiento dividido por volatilidad
  5 sesiones: bruto -0.0159, /vol -0.0154 (t = -4.7)
 21 sesiones: bruto -0.0077, /vol -0.0064 (t = -2.0)
252 sesiones: bruto +0.0053, /vol +0.0066 (t = +2.2)

Dividir por la volatilidad casi no cambia el IC a 5 sesiones (−0,0159 frente a −0,0154, con un tt algo mayor) y mejora algo el de 252 (de 0,0053 a 0,0066). Con rangos dentro de cada semana, el efecto de la normalización es pequeño porque la escala ya da igual; lo que cambia es el orden entre acciones de distinta volatilidad. En un modelo que use los valores (una regresión, una red), la normalización importa mucho más: sin ella, las acciones volátiles dominan la pérdida. La lección 5.2.4 trata cómo normalizar sin usar información futura.

Volatilidad: anticipa la magnitud, no la dirección

La volatilidad realizada es la feature más fácil de predecir y la que más se confunde. Es muy persistente (agrupamiento de la volatilidad, lección 1.6.2), así que la de las últimas semanas anticipa bien la de la siguiente. Lo que no anticipa es el signo:

IN [04]python
for w in [5, 21, 63]:
    v = R.rolling(w).std()
    con_abs = ic(v, Y.abs())
    con_signo = ic(v)
    print(f"vol {w:2d} sesiones: IC con |r| {con_abs['ic']:.3f} "
          f"(t = {con_abs['t']:.0f}); con r {con_signo['ic']:+.4f} "
          f"(t = {con_signo['t']:+.1f})")
OUT [04]Volatilidad frente a rendimiento y rendimiento absoluto
vol  5 sesiones: IC con |r| 0.171 (t = 56); con r +0.0024 (t = +0.5)
vol 21 sesiones: IC con |r| 0.220 (t = 69); con r +0.0020 (t = +0.4)
vol 63 sesiones: IC con |r| 0.227 (t = 76); con r +0.0032 (t = +0.6)

Las tres ventanas ordenan muy bien a las acciones por la magnitud del movimiento de la semana siguiente: un IC de 0,22 con 63 sesiones, unas 30 veces el techo del oráculo para la dirección, con tt de 76. Con el rendimiento con signo, el IC es cero. Una feature de riesgo así sirve para normalizar otras (la sección anterior), para condicionarlas (una interacción) y para dimensionar posiciones (lección 4.2.2), no como predictor de dirección.

El generador planta un efecto de baja volatilidad pequeño y negativo en vol_63d, y no se ve: las acciones volátiles tienen más beta y, como el mercado sube de media, la beta compensa la prima (el README lo recupera quitando la beta verdadera). Es un buen recordatorio de que el IC univariante mezcla el efecto de una feature con el de todo lo que se le parece.

Volumen e interacciones

El volumen en acciones no es comparable entre empresas (una de 2 USD negocia muchas más acciones que una de 800). La forma habitual es el volumen relativo: el de los últimos días frente al de un periodo más largo de la misma acción, en logaritmos. En el panel, volumen_rel = log(volumen medio de 5 sesiones / de 63 sesiones) no tiene efecto propio, pero el generador lo usa en una interacción: la reversión de la semana es más fuerte cuando el movimiento se hizo con poco volumen, α∝g(ret_1s) (1−0,5 g(volumen_rel))\alpha \propto g(\text{ret\_1s})\,(1 - 0{,}5\,g(\text{volumen\_rel})). Es la idea económica de que un movimiento sin volumen suele ser ruido de liquidez, y uno con volumen, información. Se ve partiendo la muestra por terciles:

IN [05]python
r1s = C / C.shift(5) - 1
vrel = np.log(V.rolling(5).mean() / V.rolling(63).mean())
q = vrel.loc[fechas].rank(axis=1, pct=True)
tramos = {"bajo": (0, 1 / 3), "medio": (1 / 3, 2 / 3),
          "alto": (2 / 3, 1)}
serie = {}
for nombre, (lo, hi) in tramos.items():
    X = r1s.loc[fechas].where((q > lo) & (q <= hi))
    serie[nombre] = X.rank(axis=1).corrwith(Y.rank(axis=1), axis=1)
    r = ic(X)
    print(f"volumen relativo {nombre:5s}: IC {r['ic']:+.4f} "
          f"(t = {r['t']:+.1f})")
dif = (serie["bajo"] - serie["alto"]).dropna()
t_dif = dif.mean() / dif.std() * np.sqrt(len(dif))
print(f"bajo - alto: {dif.mean():+.4f} (t = {t_dif:+.1f})")

centrado = lambda X: X.rank(axis=1, pct=True) - 0.5
inter = centrado(r1s.loc[fechas]) * (1 - centrado(vrel.loc[fechas]))
r = ic(inter)
print(f"interacción: IC {r['ic']:+.4f} (t = {r['t']:+.1f})")
OUT [05]Reversión según el volumen relativo
volumen relativo bajo : IC -0.0240 (t = -5.1)
volumen relativo medio: IC -0.0162 (t = -3.3)
volumen relativo alto : IC -0.0080 (t = -1.9)
bajo - alto: -0.0160 (t = -3.4)
interacción: IC -0.0170 (t = -4.6)

La reversión con volumen bajo es tres veces la de volumen alto (−0,024 frente a −0,008), y la diferencia, medida semana a semana sobre las mismas fechas, es de −0,016 con tt = −3,4: no es ruido. La feature de interacción construida a mano, con los rangos centrados, da un IC de −0,017, algo mejor que el rendimiento de 5 sesiones solo.

Fig. — IC del rendimiento de 5 sesiones por tercil de volumen relativo de la semana: la reversión plantada es más fuerte cuando el movimiento se hizo con poco volumen

Un árbol de decisión o un boosting (lecciones 5.5.2 y 5.5.3) encuentran interacciones así sin construirlas; un modelo lineal no, salvo que se le den. Construirlas a mano tiene la ventaja de que se pueden razonar y contrastar una a una, y el inconveniente de que cada interacción probada es otra prueba más en la búsqueda múltiple.

Indicadores normalizados

Los indicadores técnicos (lección 1.7.1) se construyen casi siempre en unidades de precio: la distancia a una media móvil en dólares, un MACD, una banda de Bollinger. En un panel, esas unidades hacen que el indicador mida sobre todo cuánto vale la acción, no lo que se quería medir. Y muchos indicadores son, en el fondo, el mismo número con otra escala:

IN [06]python
sma50 = C.rolling(50).mean()
en_precio = (C - sma50).loc[fechas]
en_tanto = (C / sma50 - 1).loc[fechas]
en_vol = (en_tanto / (vol63.loc[fechas] * np.sqrt(50)))
nivel = C.loc[fechas].rank(axis=1)

for nombre, X in [("C - SMA50 (USD)", en_precio),
                  ("C / SMA50 - 1", en_tanto),
                  ("(C / SMA50 - 1) / vol", en_vol)]:
    rho = X.abs().rank(axis=1).corrwith(nivel, axis=1).mean()
    print(f"{nombre:22s} corr. de |X| con el precio: {rho:+.2f}")

rsi = fe.pivot(index="fecha", columns="ticker", values="rsi_14")
r14 = (C / C.shift(14) - 1).loc[fechas]
rho = rsi.rank(axis=1).corrwith(r14.rank(axis=1), axis=1).mean()
print(f"RSI 14 frente a rendimiento de 14 sesiones: {rho:.2f}")
OUT [06]Unidades de precio frente a unidades comparables
C - SMA50 (USD)        corr. de |X| con el precio: +0.72
C / SMA50 - 1          corr. de |X| con el precio: +0.01
(C / SMA50 - 1) / vol  corr. de |X| con el precio: +0.00
RSI 14 frente a rendimiento de 14 sesiones: 0.95

La distancia a la media de 50 sesiones en dólares tiene una correlación de rangos de 0,72 entre su valor absoluto y el precio: en una acción de 800 USD, una distancia «grande» es simplemente la de una acción cara. Dividida por la media, la relación desaparece (0,01); dividida además por la volatilidad esperada en 50 sesiones, se convierte en un número de desviaciones típicas, comparable entre acciones y entre fechas. Las tres reglas: dividir por el precio, por la volatilidad y, para el volumen, por el propio historial de la acción.

La última línea es la redundancia. El RSI de 14 sesiones y el rendimiento de 14 sesiones tienen una correlación de rangos media de 0,95: son casi la misma feature. Por eso rsi_14 sale con IC negativo (−0,008, t = −2,6 en el README) sin tener efecto propio: hereda la reversión. Meter en un modelo veinte indicadores que son transformaciones del mismo rendimiento no añade información y sí ruido y colinealidad (la lección 5.2.5 mide la importancia por grupos de features correladas).

Microestructura: el desequilibrio de órdenes

Las features de precio y volumen diarias resumen lo que ya pasó en el precio. La microestructura mira quién empujó: si el volumen de cada intervalo lo iniciaron compradores o vendedores agresivos (los que cruzan la horquilla, lección 1.1.1). El desequilibrio de órdenes de un intervalo es

DI=Vcompra−VventaV=2 VcompraV−1∈[−1,1].\mathrm{DI} = \frac{V_{\mathrm{compra}} - V_{\mathrm{venta}}}{V} = \frac{2\,V_{\mathrm{compra}}}{V} - 1 \in [-1, 1].

Es la versión con operaciones del order flow imbalance del libro de la lección 4.3.2. Su problema práctico es que el lado agresor no siempre está en los datos: la cinta consolidada de acciones de EE. UU. no lo trae, y por eso existen los clasificadores. La regla del tick asigna cada operación al lado del último cambio de precio, y el algoritmo de Lee y Ready (1991)3Nota 3 · C. M. C. Lee y M. J. Ready, «Inferring Trade Direction from Intraday Data», Journal of Finance 46(2), 1991, pp. 733-746, doi:10.1111/j.1540-6261.1991.tb02683.x. la combina con la cotización vigente. Con barras, sin operaciones individuales, la clasificación por volumen (BVC, bulk volume classification) de Easley, López de Prado y O'Hara4Nota 4 · D. Easley, M. López de Prado y M. O'Hara, «Flow Toxicity and Liquidity in a High-frequency World», Review of Financial Studies 25(5), 2012, pp. 1457-1493, doi:10.1093/rfs/hhs053 (VPIN y clasificación por volumen); de los mismos autores, «Discerning information from trade data», Journal of Financial Economics 120(2), 2016, pp. 269-285, doi:10.1016/j.jfineco.2016.01.018. reparte el volumen de cada barra según el cambio de precio tipificado: Vcompra=V Φ(Δp/σΔp)V_{\mathrm{compra}} = V\,\Phi(\Delta p/\sigma_{\Delta p}).

Los datos son un año de barras de un minuto de FNX, un futuro ficticio sobre un índice (247 sesiones de 390 minutos), con el volumen comprador verdadero de cada minuto. El generador alterna dos regímenes ocultos: tendencia, con flujo informado que empuja en una dirección durante una hora de media, y reversión a la media de 30 minutos. La fracción compradora sigue al precio en los dos regímenes y, solo en tendencia, lleva además un sesgo persistente hacia la dirección del episodio (0,5 + 0,12·tanh(r/σ) + 0,03·D + ruido). Ese sesgo es la huella del flujo informado, y no está en el precio.

IN [07]python
from scipy.stats import norm

b = pd.read_parquet("hf5_barras_1min.parquet")
b["sesion"] = b["ts"].dt.date
g = b.groupby("sesion")
b["r"] = np.log(b["cierre"]).groupby(b["sesion"]).diff()

# lado verdadero (el agresor de cada operación)
b["vc"] = b["volumen_comprador"]
# regla del tick: todo el minuto al lado del cambio de precio
signo = np.sign(b["cierre"].groupby(b["sesion"]).diff())
b["vc_tick"] = b["volumen"] * (signo.fillna(0) + 1) / 2
# BVC: fracción compradora = Φ(r / σ), σ de los minutos previos
sigma = g["r"].transform(
    lambda s: s.ewm(span=100, min_periods=10).std().shift(1))
frac = pd.Series(norm.cdf(b["r"] / sigma), index=b.index)
b["vc_bvc"] = b["volumen"] * frac.fillna(0.5)

d = {m: 2 * b[c] / b["volumen"] - 1
     for m, c in [("verdad", "vc"), ("tick", "vc_tick"),
                  ("bvc", "vc_bvc")]}
d = pd.DataFrame(d)[b["r"].notna() & sigma.notna()]
print(len(b), "minutos;", b["sesion"].nunique(), "sesiones")
d.corr().round(3)
OUT [07]Barras de FNX: lado verdadero, regla del tick y BVC
96330 minutos; 247 sesiones
        verdad   tick    bvc
verdad   1.000  0.591  0.658
tick     0.591  1.000  0.861
bvc      0.658  0.861  1.000

Minuto a minuto, los dos clasificadores se parecen mucho entre sí (0,86) y bastante menos a la verdad (0,59 la regla del tick y 0,66 BVC). Es lo esperable: los dos deducen el lado del movimiento del precio, así que reconstruyen la parte del flujo que sigue al precio y pierden la otra. La pregunta para un modelo es si lo que pierden importa. El régimen de tendencia es lo que interesa detectar, y el desequilibrio de 30 minutos es la feature natural:

IN [08]python
from sklearn.metrics import roc_auc_score

verdad = pd.read_parquet("hf5_verdad.parquet")
b["regimen"] = verdad["regimen"].to_numpy()  # misma fila, mismo ts

def roll30(col):
    return g[col].transform(lambda s: s.rolling(30).sum())

vol30 = roll30("volumen")
for m, c in [("verdad", "vc"), ("tick", "vc_tick"),
             ("bvc", "vc_bvc")]:
    b[f"di_{m}"] = 2 * roll30(c) / vol30 - 1
b["r30"] = g["r"].transform(lambda s: s.rolling(30).sum())

ok = b[["di_verdad", "r30"]].notna().all(axis=1)
tend = b.loc[ok, "regimen"] == "tendencia"
print(f"minutos en tendencia: {tend.mean():.1%}")
for c in ["r30", "di_verdad", "di_tick", "di_bvc"]:
    auc = roc_auc_score(tend, b.loc[ok, c].abs())
    print(f"AUC de |{c}| para el régimen: {auc:.3f}")
OUT [08]Desequilibrio de 30 minutos y régimen
minutos en tendencia: 35.1%
AUC de |r30| para el régimen: 0.603
AUC de |di_verdad| para el régimen: 0.682
AUC de |di_tick| para el régimen: 0.556
AUC de |di_bvc| para el régimen: 0.564

El AUC mide cuánto separa el valor absoluto de cada feature los minutos de tendencia de los de reversión (0,5 es azar, 1 es perfecto). El rendimiento de los últimos 30 minutos ya separa algo (0,603), porque en tendencia el precio se mueve más en una dirección. El desequilibrio con el lado verdadero separa más (0,682): añade el sesgo persistente que el precio no tiene. Los dos clasificados quedan por debajo del propio rendimiento (0,556 y 0,564): son una versión ruidosa del cambio de precio, y además el ruido de clasificar se acumula en 30 minutos.

Fig. — FNX, barras de 1 minuto: qué distingue el régimen de tendencia (AUC del valor absoluto) y qué anticipa el rendimiento de los 30 minutos siguientes fuera de muestra, según cómo se mida el desequilibrio de 30 minutos

¿Anticipa los 30 minutos siguientes?

Distinguir el régimen no es aún anticipar el precio. La prueba honesta es la de las lecciones 5.4: una observación cada 30 minutos (las ventanas de 30 minutos no se solapan), un modelo ajustado en la primera mitad de las sesiones y evaluado en la segunda, y la comparación con la baseline más simple, el rendimiento de los 30 minutos anteriores (momentum intradía). El objetivo es el rendimiento de los 30 minutos siguientes dividido por la volatilidad esperada:

IN [09]python
import statsmodels.api as sm

b["minuto"] = g.cumcount()
b["sigma"] = g["r"].transform(
    lambda s: s.ewm(span=100, min_periods=10).std())
b["fut30"] = g["r"].transform(
    lambda s: s[::-1].rolling(30).sum()[::-1].shift(-1))
b["y"] = b["fut30"] / (b["sigma"] * np.sqrt(30))
b["r30z"] = b["r30"] / (b["sigma"] * np.sqrt(30))

cols = ["y", "r30z", "di_verdad", "di_tick", "di_bvc"]
m = b[(b["minuto"] % 30 == 0)
      & b[cols].notna().all(axis=1)].copy()
sesiones = np.sort(m["sesion"].unique())
ent = m["sesion"] < sesiones[len(sesiones) // 2]
print(f"{ent.sum()} observaciones de entrenamiento, "
      f"{(~ent).sum()} de prueba; error típico ≈ "
      f"{1 / np.sqrt((~ent).sum()):.3f}")

for x in [["r30z"], ["di_tick"], ["di_bvc"], ["di_verdad"],
          ["r30z", "di_verdad"]]:
    X = sm.add_constant(m[x])
    fit = sm.OLS(m.loc[ent, "y"], X[ent]).fit()
    pred = fit.predict(X[~ent])
    c = np.corrcoef(pred, m.loc[~ent, "y"])[0, 1]
    print(f"{' + '.join(x):18s} corr. fuera de muestra {c:+.3f}")
OUT [09]Evaluación sin solapamiento y fuera de muestra
1353 observaciones de entrenamiento, 1364 de prueba; error típico ≈ 0.027
r30z               corr. fuera de muestra +0.111
di_tick            corr. fuera de muestra +0.049
di_bvc             corr. fuera de muestra +0.061
di_verdad          corr. fuera de muestra +0.182
r30z + di_verdad   corr. fuera de muestra +0.182

Con 1.364 observaciones de prueba, el error típico de cada correlación es de unos 0,027. El rendimiento pasado da 0,111: hay momentum intradía, porque una parte del tiempo es tendencia. Los dos desequilibrios clasificados quedan por debajo (0,049 y 0,061): no añaden nada al precio del que salen. El desequilibrio verdadero llega a 0,182, y combinado con el rendimiento pasado no mejora: lo contiene. La figura de la sección anterior resume las dos medidas.

VPIN: el desequilibrio en tiempo de volumen

El VPIN (volume-synchronized probability of informed trading) de Easley, López de Prado y O'Hara (2012)4 mide el mismo desequilibrio en tiempo de volumen: parte la negociación en cubos de un volumen fijo VV y promedia el desequilibrio absoluto de los últimos nn cubos,

VPIN=1n∑τ=1n∣Vτcompra−Vτventa∣V.\mathrm{VPIN} = \frac{1}{n}\sum_{\tau=1}^{n} \frac{\lvert V^{\mathrm{compra}}_\tau - V^{\mathrm{venta}}_\tau \rvert}{V}.

Medir en cubos de volumen hace que el indicador avance deprisa cuando hay mucha actividad y despacio cuando hay poca, como las barras de volumen de la lección 1.4.5. Sus autores lo presentaron como una medida de la toxicidad del flujo para los creadores de mercado y como indicador de volatilidad a corto plazo.

Barras de 1 minCubos de volumenDesequilibrioMedia de 10 cubos
Fuente Mermaid
flowchart LR
  B[Barras de 1 min] --> C[Cubos de volumen]
  C --> D[Desequilibrio]
  D --> M[Media de 10 cubos]

El tamaño del cubo se fija con datos anteriores (aquí, 1/50 del volumen diario medio de las 20 primeras sesiones) y no se recalcula con la muestra entera, que sería mirar el futuro. Un minuto que cruza el borde de un cubo se reparte entre los dos en proporción a su volumen:

IN [10]python
def vpin(vol, vc, cubo, ventana=10):
    """VPIN por cubo: media móvil de |2·V_compra − V| / V."""
    cum_v = np.cumsum(vol)
    bordes = np.arange(cubo, cum_v[-1], cubo)
    # volumen comprador acumulado en cada borde (reparto lineal
    # del minuto que cruza el borde entre los dos cubos)
    cum_c = np.interp(bordes, cum_v, np.cumsum(vc))
    comp = np.diff(np.r_[0.0, cum_c])
    desequilibrio = np.abs(2 * comp - cubo) / cubo
    vp = pd.Series(desequilibrio).rolling(ventana).mean()
    cierre = np.searchsorted(cum_v, bordes)  # minuto de cierre
    return vp.to_numpy(), cierre

vol = b["volumen"].to_numpy(float)
# cubo fijado con las 20 primeras sesiones: 1/50 del volumen diario
cubo = b.groupby("sesion")["volumen"].sum().iloc[:20].mean() / 50
print(f"cubo de {cubo:,.0f} contratos")

res = {}
for m_, c in [("verdad", "vc"), ("tick", "vc_tick"),
              ("bvc", "vc_bvc")]:
    vp, cierre = vpin(vol, b[c].to_numpy(float), cubo)
    res[m_] = vp
res = pd.DataFrame(res)
res["regimen"] = b["regimen"].to_numpy()[cierre]
res = res.dropna()
print(len(res), "cubos; VPIN medio con el lado verdadero",
      round(res["verdad"].mean(), 3))
print(res.groupby("regimen")[["verdad", "tick", "bvc"]]
      .mean().round(3))
tend = res["regimen"] == "tendencia"
for m_ in ["verdad", "tick", "bvc"]:
    print(f"AUC del VPIN ({m_}): "
          f"{roc_auc_score(tend, res[m_]):.3f}")
OUT [10]VPIN con cubos de volumen
cubo de 23,644 contratos
12687 cubos; VPIN medio con el lado verdadero 0.084
           verdad   tick    bvc
regimen
reversion   0.079  0.311  0.197
tendencia   0.091  0.337  0.210
AUC del VPIN (verdad): 0.639
AUC del VPIN (tick): 0.575
AUC del VPIN (bvc): 0.565

Con el lado verdadero, el VPIN medio es de 0,084: 0,079 en reversión y 0,091 en tendencia, con un AUC de 0,639 para separar los regímenes, algo menos que el desequilibrio de 30 minutos (el README de los datos, con cubos de 1/50 del volumen diario nominal del generador, 22.000 contratos, da 0,094 y 0,080: el nivel depende del tamaño del cubo). Con los clasificadores pasan dos cosas. El nivel se dispara: 0,32 con la regla del tick y 0,20 con BVC, porque cada error de clasificación pasa volumen de un lado a otro y, en valor absoluto, casi nunca se compensa. Y la separación cae a 0,575 y 0,565. Un umbral de VPIN fijado con un método de clasificación no vale con otro, y un nivel publicado no se puede comparar con uno calculado con otros cubos.

La figura muestra una sesión elegida como ilustración (la diferencia media entre regímenes es mucho menor que la de este día). En el primer episodio de tendencia, una caída de casi 80 puntos en una hora y media, el VPIN sube hasta 0,15; en la reversión de la tarde se queda entre 0,04 y 0,07.

Fig. — FNX, sesión del 3 de septiembre de 2024 (elegida como ilustración): precio arriba y VPIN con el lado verdadero abajo; en gris, los episodios de tendencia del régimen verdadero

Las conclusiones del artículo original sobre el VPIN se discutieron mucho. Andersen y Bondarenko5Nota 5 · T. G. Andersen y O. Bondarenko, «VPIN and the flash crash», Journal of Financial Markets 17, 2014, pp. 1-46, doi:10.1016/j.finmar.2013.05.005, y «Reflecting on the VPIN dispute», Journal of Financial Markets 17, 2014, pp. 53-64, doi:10.1016/j.finmar.2013.08.002 (resumen consultado el 23/10/2026). mostraron con datos de futuros sobre el S&P 500 que el VPIN está correlado por construcción con el volumen y con la volatilidad, que al controlar por ellos no encuentran poder predictivo adicional para la volatilidad futura y que sus propiedades dependen mucho de la clasificación de las operaciones. Para un modelo, la lección práctica es la de toda la sección: una feature de microestructura vale lo que vale el dato del lado agresor que la alimenta, y se compara siempre con lo que ya dicen el precio, el volumen y la volatilidad.

Errores típicos

  • Calcular la feature con el cierre del día y operar a ese mismo cierre. La feature se conoce al cierre; la orden va a la sesión siguiente (o a la subasta de cierre, si se calcula unos minutos antes con precios de ese momento).
  • Horizontes que se repiten. Rendimientos de 1, 2, 5 y 10 sesiones comparten casi toda la información; con ventanas anidadas, el modelo reparte un mismo efecto entre varias columnas y la importancia de cada una engaña.
  • Unidades de precio en un panel. Una distancia en dólares, un MACD sin normalizar o un volumen en acciones ordenan por tamaño o por precio de la acción, no por lo que se quería medir.
  • Confundir riesgo con dirección. La volatilidad anticipa la magnitud del movimiento; si el modelo predice rendimientos con signo, su IC alto con el valor absoluto no dice nada.
  • Clasificar el lado agresor y creer que se tiene el flujo. La regla del tick y BVC reconstruyen lo que ya está en el precio. Si la señal depende del flujo, hace falta el dato del lado agresor.
  • Ventanas que cruzan sesiones. Un desequilibrio de 30 minutos a las 9:35 que incluya el final de la sesión anterior mezcla dos días; en FNX los rolling van agrupados por sesión (el VPIN, en cambio, sí sigue de un día al siguiente, como en su definición original).
  • Evaluar con ventanas solapadas. Una observación por minuto con un objetivo de 30 minutos infla el número de observaciones unas 30 veces y hace que todo parezca significativo.

Ejercicios

  1. dist_max_52s y rsi_14 no tienen efecto propio en el panel, pero su IC univariante no es cero. Quita, semana a semana, la parte de cada una que explica el rendimiento de 5 sesiones (regresión de rangos) y calcula el IC de lo que queda. ¿Qué features son redundantes?
Solución
IN [11]python
def residuo_rangos(X, Z):
    # rango de X menos su proyección lineal sobre el rango de Z
    rx, rz = X.rank(axis=1), Z.rank(axis=1)
    rx = rx.sub(rx.mean(axis=1), axis=0)
    rz = rz.sub(rz.mean(axis=1), axis=0)
    beta = (rx * rz).sum(axis=1) / (rz * rz).sum(axis=1)
    return rx - rz.mul(beta, axis=0)

r1 = fe.pivot(index="fecha", columns="ticker", values="ret_1s")
for nombre in ["rsi_14", "dist_max_52s"]:
    X = fe.pivot(index="fecha", columns="ticker", values=nombre)
    antes, despues = ic(X), ic(residuo_rangos(X, r1))
    print(f"{nombre:13s} IC {antes['ic']:+.4f} (t = {antes['t']:+.1f})"
          f" -> sin ret_1s {despues['ic']:+.4f} "
          f"(t = {despues['t']:+.1f})")
OUT [11]IC después de quitar la reversión
rsi_14        IC -0.0083 (t = -2.6) -> sin ret_1s -0.0005 (t = -0.2)
dist_max_52s  IC -0.0031 (t = -1.0) -> sin ret_1s +0.0019 (t = +0.7)

El RSI pierde todo su IC al quitarle la reversión de 5 sesiones (de −0,0083 a −0,0005): era la misma feature con otra escala. La distancia al máximo de 52 semanas no era significativa antes (t = −1,0) y tampoco lo es después (t = 0,7): el pequeño cambio de signo es ruido. Las dos son redundantes con las rentabilidades. En datos reales no se tiene la verdad para confirmarlo, pero el procedimiento es el mismo: medir el IC incremental sobre las features que ya están en el modelo, no el univariante.

  1. Repite el VPIN con el lado verdadero con cubos el doble de grandes (1/25 del volumen diario) y con una ventana de 50 cubos. ¿Cómo cambian el nivel medio y el AUC del régimen? ¿Qué parámetros elegirías y cómo, sin mirar la muestra de prueba?
Solución
IN [12]python
vc = b["vc"].to_numpy(float)
for factor, ventana in [(1, 10), (2, 10), (1, 50), (2, 50)]:
    vp, cierre = vpin(vol, vc, cubo * factor, ventana)
    t = pd.DataFrame({"vp": vp,
                      "reg": b["regimen"].to_numpy()[cierre]})
    t = t.dropna()
    auc = roc_auc_score(t["reg"] == "tendencia", t["vp"])
    print(f"cubo x{factor}, ventana {ventana:2d}: media "
          f"{t['vp'].mean():.3f}, AUC {auc:.3f}")
OUT [12]VPIN con otros cubos y otra ventana
cubo x1, ventana 10: media 0.084, AUC 0.639
cubo x2, ventana 10: media 0.064, AUC 0.613
cubo x1, ventana 50: media 0.084, AUC 0.556
cubo x2, ventana 50: media 0.064, AUC 0.543

Con cubos el doble de grandes, cada cubo agrega más volumen y el desequilibrio absoluto medio baja (de 0,084 a 0,064): el nivel del VPIN depende del cubo, no solo del mercado. Con una ventana de 50 cubos (unas sesiones enteras) el indicador se suaviza tanto que deja de seguir episodios de una hora y el AUC cae a 0,556 y 0,543. Los parámetros se eligen con el horizonte del fenómeno que se quiere detectar (aquí, episodios de una hora: ventanas de unas horas como mucho) y con validación en la primera mitad de los datos, nunca con el AUC de la segunda. Cada combinación probada cuenta en la búsqueda múltiple.

  1. Tu proveedor de datos de un futuro te da barras de un minuto con OHLCV, sin el lado agresor. Un colega propone construir el desequilibrio con BVC y entrenar un modelo con él. ¿Qué le dirías, y qué dato pedirías?
Solución

Que BVC solo reparte el volumen según el cambio de precio de la barra: en FNX, el desequilibrio así construido separa el régimen peor que el propio rendimiento de 30 minutos (AUC 0,564 frente a 0,603) y anticipa menos el precio (0,061 frente a 0,111). Antes de añadirlo hay que compararlo con las features de precio de las que sale; si no las mejora fuera de muestra, sobra. Si la hipótesis es que el flujo contiene información que el precio aún no tiene, hace falta el dato de cada operación con su lado agresor (muchos mercados de futuros lo difunden en su feed de operaciones) o, como mínimo, las operaciones con la cotización vigente para aplicar Lee y Ready. Y, en los dos casos, comprobar la marca de tiempo de cada operación frente al cierre de la barra.

Footnotes

  1. N. Jegadeesh y S. Titman, «Returns to Buying Winners and Selling Losers: Implications for Stock Market Efficiency», Journal of Finance 48(1), 1993, pp. 65-91, doi:10.1111/j.1540-6261.1993.tb04702.x. ↩

  2. N. Jegadeesh, «Evidence of Predictable Behavior of Security Returns», Journal of Finance 45(3), 1990, pp. 881-898, doi:10.1111/j.1540-6261.1990.tb05110.x. ↩

  3. C. M. C. Lee y M. J. Ready, «Inferring Trade Direction from Intraday Data», Journal of Finance 46(2), 1991, pp. 733-746, doi:10.1111/j.1540-6261.1991.tb02683.x. ↩

  4. D. Easley, M. López de Prado y M. O'Hara, «Flow Toxicity and Liquidity in a High-frequency World», Review of Financial Studies 25(5), 2012, pp. 1457-1493, doi:10.1093/rfs/hhs053 (VPIN y clasificación por volumen); de los mismos autores, «Discerning information from trade data», Journal of Financial Economics 120(2), 2016, pp. 269-285, doi:10.1016/j.jfineco.2016.01.018. ↩ ↩2

  5. T. G. Andersen y O. Bondarenko, «VPIN and the flash crash», Journal of Financial Markets 17, 2014, pp. 1-46, doi:10.1016/j.finmar.2013.05.005, y «Reflecting on the VPIN dispute», Journal of Financial Markets 17, 2014, pp. 53-64, doi:10.1016/j.finmar.2013.08.002 (resumen consultado el 23/10/2026). ↩

Respuesta plegada en cada pregunta

Autoevaluación

  1. En el panel, el rendimiento de 5 sesiones tiene un IC de −0,016 y el de 10 sesiones, de −0,011, aunque solo el de 5 tiene efecto plantado. ¿Por qué el de 10 también sale negativo y significativo?

    • APorque el rendimiento de 10 sesiones contiene el de las 5 últimas: hereda su efecto aunque no tenga uno propio
    • BPorque el generador planta también una reversión a 10 sesiones
    • CPorque el IC de rangos está sesgado hacia valores negativos
    • DPorque las etiquetas se solapan
    Ver respuesta

    Respuesta A. Porque el rendimiento de 10 sesiones contiene el de las 5 últimas: hereda su efecto aunque no tenga uno propio

    Un rendimiento de 10 sesiones es el de las 5 últimas más el de las 5 anteriores. La parte reciente arrastra la reversión plantada; la antigua solo añade ruido, y por eso el IC baja pero no desaparece. Un IC univariante no distingue el efecto propio del heredado: hace falta un modelo con las dos features a la vez o conocer la verdad, como aquí.

  2. La volatilidad de 63 sesiones tiene un IC de 0,23 con el valor absoluto del rendimiento de la semana siguiente y de 0,003 con el rendimiento con signo. ¿Qué uso tiene en un modelo de dirección?

    • ANinguno: si no predice la dirección hay que quitarla
    • BEs la mejor feature de dirección del panel
    • CEscalar o condicionar otras features (una reversión pesa distinto en una acción tranquila que en una volátil) y dimensionar posiciones; por sí sola no ordena por rendimiento esperado
    • DSustituir a la etiqueta
    Ver respuesta

    Respuesta C. Escalar o condicionar otras features (una reversión pesa distinto en una acción tranquila que en una volátil) y dimensionar posiciones; por sí sola no ordena por rendimiento esperado

    La volatilidad es muy persistente y anticipa la magnitud de los movimientos, no su signo. Sirve para normalizar (rendimiento dividido por volatilidad), para interacciones y para el tamaño de las posiciones. En el panel, además, tiene un efecto plantado negativo pequeño que la beta tapa con el rendimiento bruto (README de los datos).

  3. Con las barras de un minuto de FNX, el desequilibrio de 30 minutos calculado con el volumen comprador verdadero distingue el régimen de tendencia con un AUC de 0,68; reconstruido con la regla del tick, con 0,56, menos que el propio rendimiento de 30 minutos (0,60). ¿Por qué tanta diferencia?

    • APorque la regla del tick tiene un error de programación
    • BPorque la regla del tick deduce el lado del movimiento del precio, así que solo recupera la parte del flujo que ya está en el precio; la parte que no se ve en él (el sesgo persistente del flujo informado) se pierde
    • CPorque el AUC no se puede comparar entre features
    • DPorque el volumen comprador verdadero tiene fuga
    Ver respuesta

    Respuesta B. Porque la regla del tick deduce el lado del movimiento del precio, así que solo recupera la parte del flujo que ya está en el precio; la parte que no se ve en él (el sesgo persistente del flujo informado) se pierde

    En los datos, la fracción compradora es 0,5 + 0,12·tanh(r/σ) + 0,03·D en tendencia + ruido. Un clasificador que mira el precio solo puede reconstruir el primer término. El término 0,03·D, la huella del flujo informado, solo está en el lado agresor de cada operación.

  4. El VPIN de FNX calculado con BVC sale en torno a 0,20 y con el lado verdadero en torno a 0,08. ¿Qué se concluye?

    • AQue el mercado es más tóxico de lo que parece
    • BQue BVC es más preciso
    • CQue el VPIN no depende del tamaño de los cubos
    • DQue el nivel del VPIN depende del método de clasificación: el ruido de clasificar se suma como desequilibrio aparente, y un umbral fijado con un método no vale con otro
    Ver respuesta

    Respuesta D. Que el nivel del VPIN depende del método de clasificación: el ruido de clasificar se suma como desequilibrio aparente, y un umbral fijado con un método no vale con otro

    Cada cubo de volumen mide |V_compra − V_venta|/V. Un error de clasificación desplaza volumen de un lado a otro y casi nunca se compensa en valor absoluto: el VPIN medido sube. Por eso los niveles publicados de VPIN no se pueden comparar si cambia la clasificación o el tamaño del cubo.

  5. Para evaluar si el desequilibrio de 30 minutos anticipa el rendimiento de los 30 minutos siguientes, ¿por qué se toma una observación cada 30 minutos y no una por minuto?

    • APorque con una por minuto las ventanas se solapan: 30 observaciones seguidas comparten casi todo el futuro y el error típico calculado como si fueran independientes sería unas cinco veces demasiado pequeño
    • BPara que el cálculo sea más rápido
    • CPorque las barras de un minuto no tienen volumen comprador
    • DPorque la correlación no admite más de 3.000 puntos
    Ver respuesta

    Respuesta A. Porque con una por minuto las ventanas se solapan: 30 observaciones seguidas comparten casi todo el futuro y el error típico calculado como si fueran independientes sería unas cinco veces demasiado pequeño

    Con ventanas solapadas, la correlación media se puede estimar, pero el número efectivo de observaciones es unas 30 veces menor. Muestrear sin solapamiento da un error típico honesto de 1/√n (lecciones 5.3.4 y 5.4.1).

Ideas clave

  • Una feature es una función del pasado conocida al cierre de su fecha. Recalcularla desde los precios y compararla con la publicada es la forma más barata de encontrar errores de definición y de alineación.
  • En el panel, la reversión plantada a 5 sesiones da el IC más fuerte entre los rendimientos (−0,016, t = −4,3); los horizontes vecinos heredan parte de su efecto. El momentum de 12 meses salta el último mes para no mezclarse con la reversión.
  • La volatilidad anticipa la magnitud (IC de 0,23 con el rendimiento absoluto) y no la dirección: sirve para normalizar, condicionar y dimensionar.
  • Las interacciones importan: la reversión es tres veces más fuerte con volumen relativo bajo (IC −0,024) que con volumen alto (−0,008).
  • Los indicadores en unidades de precio no son comparables entre acciones: se normalizan por el precio y por la volatilidad. Muchos indicadores clásicos son casi la misma feature (el RSI de 14 sesiones y el rendimiento de 14 sesiones tienen una correlación de rangos de 0,95).
  • El desequilibrio de órdenes con el lado agresor verdadero lleva información que el precio no tiene: en FNX distingue el régimen (AUC 0,68) y anticipa los 30 minutos siguientes (correlación de 0,18 fuera de muestra, frente a 0,11 del rendimiento pasado). Clasificado desde las barras, pierde casi todo eso; el VPIN, además, cambia de nivel con el método.

Recursos