---
title: "5.2.1 Features de precio, volumen y microestructura"
description: "Rentabilidades a varios horizontes, volatilidad, indicadores normalizados, desequilibrio de órdenes y VPIN, medidos contra la respuesta conocida."
url: "https://pythonparatrading.com/curso-python-trading/nivel-5/features-de-precio-volumen-y-microestructura/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-5/features-de-precio-volumen-y-microestructura.png"
---

# 5.2.1 · 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.

- Curso: [Nivel 5: Machine learning y deep learning (Keras/PyTorch)](https://pythonparatrading.com/curso-python-trading/nivel-5/)
- Módulo: 5.2 · Ingeniería de características
- Duración: 70 min
- Requisitos: [1.4.5 · Construcción de barras: tiempo, ticks, volumen, dólar y desequilibrio](https://pythonparatrading.com/curso-python-trading/nivel-1/barras-de-tiempo-ticks-volumen-y-dolar/), [1.7.1 · Indicadores técnicos en Python](https://pythonparatrading.com/curso-python-trading/nivel-1/indicadores-tecnicos-en-python/), [2.3.1 · Seguimiento de tendencia y momentum](https://pythonparatrading.com/curso-python-trading/nivel-2/seguimiento-de-tendencia-y-momentum/), [4.3.2 · Order flow imbalance, señales de microestructura y latencia](https://pythonparatrading.com/curso-python-trading/nivel-4/order-flow-imbalance-y-latencia/), [5.1.1 · Por qué el machine learning financiero es distinto](https://pythonparatrading.com/curso-python-trading/nivel-5/por-que-el-ml-financiero-es-distinto/)

## Al terminar esta lección

- Construir rentabilidades a varios horizontes, volatilidades, volumen relativo e indicadores normalizados con datos conocidos al cierre de cada fecha, y comprobarlas contra las features publicadas.
- Medir el IC de rangos de cada feature con su error y separar el efecto propio del heredado de una feature vecina.
- Distinguir una feature de riesgo (anticipa la magnitud del movimiento) de una de dirección, y construir una interacción con el volumen.
- Calcular el desequilibrio de órdenes y el VPIN con el volumen comprador verdadero, con la regla del tick y con la clasificación por volumen (BVC), y medir cuánta información se pierde al clasificar.
- Evaluar una feature de microestructura sin solapamiento y fuera de muestra, frente a la baseline del rendimiento pasado.

## 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.

| Familia | Ejemplos | Qué intenta capturar | Trampa típica |
|---|---|---|---|
| Rentabilidades | 1, 5, 21, 63 sesiones; momentum 12-1 | Reversión a corto plazo, tendencia a medio | Horizontes solapados que repiten la misma información |
| Volatilidad | Desviación de 21 y 63 sesiones | Riesgo, régimen | Confundir anticipar la magnitud con anticipar la dirección |
| Volumen | Volumen relativo, rotación | Atención, liquidez, confirmación | Unidades absolutas (acciones) no comparables |
| Indicadores | RSI, distancia al máximo, medias | Lo mismo que las rentabilidades, en otra escala | Unidades de precio; redundancia entre ellos |
| Microestructura | Desequilibrio de órdenes, VPIN | Flujo informado, toxicidad | Clasificar 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:

```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:

```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}")
```
```output
(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-252$ a $t-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 $t$ que la divide por su error típico,

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

```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})
```
```output
                  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)[^1], que separaron el momentum
  de la reversión a un mes documentada por Jegadeesh (1990)[^2].
- **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 $t$ 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:

```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})")
```
```output
  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 $t$ 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:

```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})")
```
```output
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 $t$ 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, $\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:

```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})")
```
```output
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 $t$ = −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.* Tres barras horizontales negativas. Con volumen relativo bajo, destacada, IC −0,024 (t −5,1); con volumen medio, −0,016 (t −3,3); con volumen alto, −0,008 (t −1,9). Las rayas de ±1,96 errores típicos de los terciles bajo y alto apenas se tocan.

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:

```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}")
```
```output
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

$$
\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)[^3] 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'Hara[^4] reparte el volumen de cada
barra según el cambio de precio tipificado: $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.

```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)
```
```output
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:

```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}")
```
```output
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.* Dos paneles de barras horizontales. Izquierda, AUC para distinguir el régimen de tendencia: rendimiento de 30 minutos 0,603, desequilibrio con el lado verdadero 0,682, regla del tick 0,556, BVC 0,564. Derecha, correlación fuera de muestra con el rendimiento de los 30 minutos siguientes: rendimiento de 30 minutos 0,111, lado verdadero 0,182 (destacado), regla del tick 0,049, BVC 0,061.

## ¿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:

```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}")
```
```output
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.

<aside class="callout" data-kind="aviso">

Una correlación de 0,18 con el rendimiento de los 30 minutos siguientes sería, en un mercado real, una señal
extraordinaria. Aquí es la respuesta plantada por el generador (el término 0,03·D) y sirve para enseñar qué se pierde
al clasificar. Antes de creer algo parecido en datos reales hay que descartar la alineación (¿el volumen comprador del
minuto incluye operaciones posteriores al cierre de la barra?), los costes (una operación cada 30 minutos paga la
horquilla dos veces) y la búsqueda de parámetros.

</aside>

## 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 $V$ y promedia el
desequilibrio absoluto de los últimos $n$ cubos,

$$
\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.

```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:

```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}")
```
```output
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.* Dos paneles con el minuto de la sesión en el eje horizontal. Arriba, el precio de FNX cae de unos 5.480 a unos 5.400 durante el primer episodio de tendencia (minutos 1 a 109) y sube durante el segundo (124 a 161). Abajo, el VPIN, destacado, sube por encima de 0,12 durante el primer episodio y vuelve a niveles de 0,05 a 0,08 en la reversión de la tarde; una línea discontinua marca la media del año, 0,084.

Las conclusiones del artículo original sobre el VPIN se discutieron mucho. Andersen y Bondarenko[^5] 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.**

```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})")
```
```output
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.

2. 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.**

```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}")
```
```output
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.

3. 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.

[^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](https://doi.org/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](https://doi.org/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](https://doi.org/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](https://doi.org/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](https://doi.org/10.1016/j.jfineco.2016.01.018).
[^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](https://doi.org/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](https://doi.org/10.1016/j.finmar.2013.08.002) (resumen consultado el
    23/10/2026).

## 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?

   - A. Porque el rendimiento de 10 sesiones contiene el de las 5 últimas: hereda su efecto aunque no tenga uno propio
   - B. Porque el generador planta también una reversión a 10 sesiones
   - C. Porque el IC de rangos está sesgado hacia valores negativos
   - D. Porque las etiquetas se solapan

   *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?

   - A. Ninguno: si no predice la dirección hay que quitarla
   - B. Es la mejor feature de dirección del panel
   - 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
   - D. Sustituir a la etiqueta

   *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?

   - A. Porque la regla del tick tiene un error de programación
   - 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
   - C. Porque el AUC no se puede comparar entre features
   - D. Porque el volumen comprador verdadero tiene fuga

   *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?

   - A. Que el mercado es más tóxico de lo que parece
   - B. Que BVC es más preciso
   - C. Que el VPIN no depende del tamaño de los cubos
   - 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

   *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?

   - 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
   - B. Para que el cálculo sea más rápido
   - C. Porque las barras de un minuto no tienen volumen comprador
   - D. Porque la correlación no admite más de 3.000 puntos

   *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

### Research

- [Alpha Factors y Alpha Trading en Python](https://pythonparatrading.com/alpha-factors-y-alpha-trading-en-python/) · Research-0007 · 5 min
- [Liquidez Order Book - Trading Liquidez Agregada](https://pythonparatrading.com/limit-order-book-y-liquidez-de-mercado/) · Research-0008 · 11 min

### Documentación

- [Easley, López de Prado y O'Hara (2012): Flow Toxicity and Liquidity in a High-frequency World, Review of Financial Studies 25(5)](https://doi.org/10.1093/rfs/hhs053) · doi.org
- [Andersen y Bondarenko (2014): VPIN and the flash crash, Journal of Financial Markets 17](https://doi.org/10.1016/j.finmar.2013.05.005) · doi.org
- [Easley, López de Prado y O'Hara (2016): Discerning information from trade data, Journal of Financial Economics 120(2)](https://doi.org/10.1016/j.jfineco.2016.01.018) · doi.org
- [Lee y Ready (1991): Inferring Trade Direction from Intraday Data, Journal of Finance 46(2)](https://doi.org/10.1111/j.1540-6261.1991.tb02683.x) · doi.org

### Datos

- [Datos del nivel 5: precios diarios del panel de 400 acciones (Parquet)](https://pythonparatrading.com/curso/datos/nivel-5/panel5_precios.parquet) · Descarga · panel5_precios.parquet
- [Datos del nivel 5: barras de 1 minuto de FNX con volumen comprador (Parquet)](https://pythonparatrading.com/curso/datos/nivel-5/hf5_barras_1min.parquet) · Descarga · hf5_barras_1min.parquet

Lección anterior: [5.1.3 · Métricas económicas para modelos de trading: más allá del acierto](https://pythonparatrading.com/curso-python-trading/nivel-5/metricas-economicas-para-modelos-de-trading/)

Lección siguiente: [5.2.2 · Fundamentales point-in-time, macro y datos alternativos](https://pythonparatrading.com/curso-python-trading/nivel-5/fundamentales-point-in-time-macro-y-datos-alternativos/)
