---
title: "5.3.1 Etiquetado de horizonte fijo y sus problemas"
description: "La etiqueta más usada en ML financiero, el signo del rendimiento a h barras, medida contra la verdad: ruido, umbrales fijos, horizonte y solapamiento."
url: "https://pythonparatrading.com/curso-python-trading/nivel-5/etiquetado-de-horizonte-fijo/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-5/etiquetado-de-horizonte-fijo.png"
---

# 5.3.1 · Etiquetado de horizonte fijo y sus problemas

> Antes de entrenar un modelo hay que decidir qué tiene que predecir. La respuesta por defecto, el signo del rendimiento a un horizonte fijo, parece neutra y no lo es: el horizonte decide qué mide la etiqueta, un umbral fijo la llena de volatilidad y el solapamiento multiplica la evidencia aparente. Esta lección lo mide en el panel de acciones y en el futuro intradía del nivel, donde se conoce la verdad.

- Curso: [Nivel 5: Machine learning y deep learning (Keras/PyTorch)](https://pythonparatrading.com/curso-python-trading/nivel-5/)
- Módulo: 5.3 · Etiquetado y muestreo
- Duración: 60 min
- Requisitos: [2.1.1 · Contrastes de hipótesis, p-valores, bootstrap y regresión con errores robustos](https://pythonparatrading.com/curso-python-trading/nivel-2/contrastes-bootstrap-y-errores-robustos/), [5.2.1 · Features de precio, volumen y microestructura](https://pythonparatrading.com/curso-python-trading/nivel-5/features-de-precio-volumen-y-microestructura/), [5.2.4 · Normalización sin leakage y rank transversal](https://pythonparatrading.com/curso-python-trading/nivel-5/normalizacion-sin-leakage-y-rank-transversal/)

## Al terminar esta lección

- Definir la etiqueta de horizonte fijo (signo, umbral y rendimiento relativo a la sección) y guardar con cada etiqueta su fecha de fin.
- Medir cuánto de una etiqueta es señal comparándola con el alfa verdadero del panel, y qué pasa al descartar los rendimientos pequeños.
- Comprobar que un umbral fijo reparte las clases según la volatilidad (año, hora del día) y corregirlo con un umbral proporcional a una volatilidad prevista sin futuro.
- Elegir el horizonte según lo que se quiere medir: el acierto de la etiqueta frente al estado latente de FNX sube hasta la duración típica del régimen y después baja.
- Cuantificar cómo el solapamiento de etiquetas infla el estadístico t y corregirlo con Newey-West o con submuestras sin solape.

## Qué tiene que predecir el modelo

El aprendizaje supervisado necesita una respuesta para cada fila. En los tutoriales de ML aplicado a mercados la
respuesta casi siempre es la misma: el signo del rendimiento desde el cierre de la barra $t$ hasta el de la barra
$t + h$. Con precios $P$ y rendimiento logarítmico $r_{t,t+h} = \ln(P_{t+h}/P_t)$, hay tres variantes:

$$y_t = \operatorname{signo}(r_{t,t+h}), \qquad
y_t = \begin{cases} +1 & r_{t,t+h} > \tau_t \\ 0 & |r_{t,t+h}| \le \tau_t \\ -1 & r_{t,t+h} < -\tau_t \end{cases},
\qquad y_t = r_{t,t+h} - \operatorname{mediana}_j\, r_{j,t,t+h}.$$

La primera es una clasificación binaria; la segunda añade una clase «sin movimiento» con un umbral $\tau_t$; la tercera
es la de un panel de acciones, donde lo que importa para una cartera larga-corta es el rendimiento **relativo** a las
demás acciones de la misma fecha. A esta familia se la llama de **horizonte fijo** porque $h$ es el mismo para todas
las filas, pase lo que pase entre $t$ y $t + h$.

```mermaid
flowchart LR
  T[Barra t: features] --> T1[Fin t1 = t + h]
  T1 --> R[r de t a t1]
  R --> Y[Etiqueta y]
```

Es la etiqueta más sencilla y la que usan las lecciones de validación y de modelos de este nivel con el panel. Tiene
cinco problemas, y esta lección mide cuatro con datos donde se conoce la verdad (los datos son sintéticos: los efectos
están plantados y el modelo generador está publicado en `public/curso/datos/nivel-5/README.md`). El quinto, que ignora
la trayectoria entre $t$ y $t + h$, es el punto de partida de la triple barrera (lección 5.3.2).

| Problema | Qué pasa | Dónde se mide |
|---|---|---|
| Ruido | La etiqueta casi no se parece a la parte predecible | Panel: signo frente al alfa verdadero |
| Umbral fijo | Las clases se reparten según la volatilidad, no según la señal | Panel por años y FNX por horas |
| Horizonte | Decide qué fenómeno mide la etiqueta | FNX: signo frente a la tendencia latente |
| Solapamiento | Si $h$ es mayor que el paso de muestreo, las etiquetas comparten rendimientos | Panel: etiqueta a 21 sesiones |
| Trayectoria | Un stop tocado por el camino no cuenta | Lección 5.3.2 |

## Las etiquetas del panel

El panel de 400 acciones ficticias trae dos etiquetas ya calculadas, cada una con su final: `ret_sig_semana` va del
cierre de `fecha` (el último día de la semana) al cierre de la semana siguiente (`fin_sig_semana`), y `ret_sig_21d`, a
21 sesiones (`fin_sig_21d`). La etiqueta de clasificación se construye restando la mediana de la semana:

```python
import numpy as np
import pandas as pd

f = pd.read_parquet("panel5_features.parquet")
e = pd.read_parquet("panel5_etiquetas.parquet")
v = pd.read_parquet("panel5_verdad.parquet")
d = (f.merge(e, on=["fecha", "ticker"])
      .merge(v[["fecha", "ticker", "alfa_verdadero"]],
             on=["fecha", "ticker"])
      .dropna(subset=["ret_sig_21d"]).reset_index(drop=True))
sem = d.groupby("fecha")
d["y"] = d["ret_sig_semana"] - sem["ret_sig_semana"].transform("median")
d["alfa"] = (d["alfa_verdadero"]
             - sem["alfa_verdadero"].transform("median"))
d["etiqueta"] = np.sign(d["y"]).astype(int)
cols = ["fecha", "fin_sig_semana", "ret_sig_semana", "etiqueta",
        "fin_sig_21d", "ret_sig_21d"]
d.loc[d["ticker"] == "BAKU", cols].head(3).round(
    {"ret_sig_semana": 4, "ret_sig_21d": 4})
```
```output
         fecha fin_sig_semana  ret_sig_semana  etiqueta fin_sig_21d  ret_sig_21d
2   2012-01-13     2012-01-20         -0.0723        -1  2012-02-14       0.0902
402 2012-01-20     2012-01-27          0.0176         1  2012-02-21       0.2986
802 2012-01-27     2012-02-03          0.0688         1  2012-02-28       0.3393
```

Las fechas de fin no son `fecha` más siete días ni más 21 días naturales: se cuentan en sesiones de la NYSE, así que
los festivos las desplazan. Por eso la función de etiquetado tiene que devolver el final y no dejar que el resto del
código lo recalcule.

## Cuánto de la etiqueta es señal

El panel publica `alfa_verdadero`: el rendimiento extra de la semana siguiente que el generador planta en cada acción.
Es la parte predecible de la etiqueta; el resto es ruido. Si un modelo conociera el alfa exacto (el **oráculo**), su
acierto al predecir el signo de la etiqueta sería la proporción de filas en que el signo del alfa y el de la etiqueta
coinciden:

```python
acierta = np.sign(d["y"]) == np.sign(d["alfa"])
print(f"todas: {acierta.mean():.2%}")
pct = sem["y"].transform(lambda s: s.abs().rank(pct=True))
cortes = [0.25, 0.5, 0.75, 0.9, 0.97]
pd.Series({f"|y| por encima del percentil {int(q * 100)}":
           acierta[pct > q].mean() for q in cortes}
          ).map("{:.2%}".format)
```
```output
todas: 50.93%
|y| por encima del percentil 25    51.20%
|y| por encima del percentil 50    51.41%
|y| por encima del percentil 75    51.62%
|y| por encima del percentil 90    51.63%
|y| por encima del percentil 97    51.39%
dtype: str
```

El 50,93 %. La desviación del alfa entre acciones es de unos 12 pb a la semana y la de la etiqueta, de unos 360 pb:
la señal es una treintava parte del ruido, una relación realista para acciones (lección 5.1.1). Este es el techo de
cualquier clasificador entrenado con esta etiqueta: un acierto fuera de muestra del 55 % no es un buen modelo, es una
fuga.

La segunda parte de la celda responde a una idea muy extendida: quitar los rendimientos pequeños, «que son ruido», para
que el modelo aprenda solo de los movimientos claros. El acuerdo con el alfa sube un poco al descartar el cuartil más
pequeño (51,20 %) y la mitad (51,41 %), se estanca en el 51,6 % y vuelve a bajar con el 3 % de rendimientos más
extremos (51,39 %). Los rendimientos grandes de una acción no son más informativos: con colas gruesas, los más extremos
son sobre todo saltos (resultados, sustos sectoriales) que el alfa no explica. Y cada filtro deja al modelo con menos
filas. Un umbral moderado puede ayudar algo; no convierte el ruido en señal.

## Umbral fijo: las clases siguen a la volatilidad

La segunda variante, con clase 0, necesita un umbral. El más común es un número fijo («más del 3 % en una semana»). El
problema es que la volatilidad cambia, y con ella la proporción de rendimientos que pasan el umbral:

```python
r = d["ret_sig_semana"]
sigma_sem = d["vol_63d"] / np.sqrt(52)
anio = d["fecha"].dt.year
pd.DataFrame({
    "|r| > 3 %": (r.abs() > 0.03).groupby(anio).mean(),
    "|r| > 0,5·σ": (r.abs() > 0.5 * sigma_sem).groupby(anio).mean(),
}).loc[[2013, 2017, 2019, 2020, 2022, 2023]].round(3)
```
```output
       |r| > 3 %  |r| > 0,5·σ
fecha
2013       0.362        0.574
2017       0.356        0.586
2019       0.392        0.578
2020       0.503        0.591
2022       0.525        0.599
2023       0.349        0.552
```

Con un 3 % fijo, la clase ±1 es el 35 % de las filas en 2017 y 2023 y más de la mitad en 2020 y 2022. Un clasificador
entrenado así aprende, antes que nada, en qué régimen de volatilidad está, y las clases cambian de proporción entre
el entrenamiento y la prueba. Con un umbral proporcional a la volatilidad de la acción, $\tau_{i,t} = 0{,}5\cdot
\sigma_{i,t}/\sqrt{52}$ (la `vol_63d` del panel, conocida en `fecha`, llevada a una semana), la proporción queda entre
el 55 % y el 60 % todos los años.

En datos intradía el efecto es más fuerte, porque la volatilidad tiene una curva en U dentro de cada sesión. FNX, el
futuro ficticio del nivel, tiene un año de barras de un minuto (247 sesiones de 390 minutos). Para escalar sin futuro
hace falta una volatilidad **prevista** para los 30 minutos siguientes: aquí, la varianza de cada minuto del día
estimada con las 20 sesiones anteriores, suavizada entre minutos vecinos y sumada sobre el horizonte.

```python
b = pd.read_parquet("hf5_barras_1min.parquet")
ver = pd.read_parquet("hf5_verdad.parquet")
C = np.log(b["cierre"].to_numpy()).reshape(-1, 390)
D = ver["direccion"].to_numpy().reshape(-1, 390)
r2 = np.diff(C, axis=1) ** 2            # 389 rendimientos por sesión

# varianza de cada minuto: media de las 20 sesiones ANTERIORES
s2 = pd.DataFrame(r2).rolling(20).mean().shift(1)
s2 = s2.T.rolling(9, center=True, min_periods=1).mean().T.to_numpy()
acum = np.c_[np.zeros(len(s2)), np.cumsum(s2, axis=1)]
print(C.shape, "sesiones x minutos")
```
```output
(247, 390) sesiones x minutos
```

```python
h = 30
fut = C[:, h:] - C[:, :-h]              # r de t a t+h, t = 0…359
sd = np.sqrt(acum[:, h:390] - acum[:, :390 - h])
ok = ~np.isnan(sd)
media_hora = np.tile(np.arange(390 - h) // 30, (len(C), 1))
fijo = np.abs(fut) > 0.0020
escalado = np.abs(fut) > sd
tabla = pd.DataFrame({
    "σ prevista (pb)": [np.nanmean(sd[ok & (media_hora == k)]) * 1e4
                        for k in range(12)],
    "|r| > 20 pb": [fijo[ok & (media_hora == k)].mean()
                    for k in range(12)],
    "|r| > σ prevista": [escalado[ok & (media_hora == k)].mean()
                         for k in range(12)],
}, index=[f"{9 + (30 + 30 * k) // 60:02d}:{(30 + 30 * k) % 60:02d}"
          for k in range(12)])
tabla.round(3)
```
```output
       σ prevista (pb)  |r| > 20 pb  |r| > σ prevista
09:30           34.713        0.519             0.285
10:00           25.928        0.405             0.299
10:30           22.832        0.341             0.281
11:00           21.717        0.312             0.280
11:30           21.258        0.306             0.284
12:00           21.072        0.287             0.264
12:30           21.109        0.340             0.321
13:00           21.044        0.301             0.287
13:30           21.283        0.317             0.292
14:00           21.638        0.324             0.290
14:30           22.556        0.373             0.315
15:00           23.508        0.374             0.315
```

*Fig. — FNX: proporción de etiquetas a 30 minutos que superan el umbral, por media hora de inicio, con un umbral fijo de 20 pb y con uno igual a la volatilidad prevista con las 20 sesiones anteriores.* Dos líneas por franja de 9:30 a 15:00. Con el umbral fijo, destacada, la proporción de etiquetas ±1 es del 52 % a las 9:30, baja al 29 % a las 12:00 y sube al 37 % a las 15:00, siguiendo la curva en U de la volatilidad. Con el umbral escalado queda entre el 26 % y el 32 % todo el día.

Con un umbral fijo de 20 pb, el 52 % de las etiquetas de la primera media hora son ±1 y a mediodía, el 29 %: la mitad
del trabajo del modelo sería aprender la hora. Con el umbral igual a la volatilidad prevista de los 30 minutos (de
35 pb en la apertura a 21 pb a mediodía), la proporción de ±1 queda entre el 26 % y el 32 % en todas las franjas. Esta
es la idea que la triple barrera lleva más lejos, con barreras de $\pm k\cdot\sigma_t\sqrt{h}$ (lección 5.3.2).

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

El umbral escalado tiene que usar una volatilidad **conocida en t**. Dividir el rendimiento futuro por la volatilidad
realizada entre $t$ y $t + h$ es una fuga sutil: esa volatilidad depende del mismo movimiento que se está etiquetando.
La de la celda usa solo las sesiones anteriores; la `vol_63d` del panel, solo las 63 sesiones hasta `fecha`.

</aside>

## El horizonte decide qué mide la etiqueta

FNX alterna dos regímenes latentes: **tendencia** (flujo informado con una dirección $D = \pm 1$, episodios de 60
minutos de media) y **reversión** a una media exponencial de 30 minutos. La verdad minuto a minuto está en
`hf5_verdad.parquet`. Eso permite preguntar, para cada horizonte $h$, con qué frecuencia el signo de la etiqueta
coincide con la dirección verdadera de la tendencia en el minuto $t$, que es lo que un modelo debería aprender a
reconocer:

```python
def acierto_tendencia(h):
    fut = C[:, h:] - C[:, :-h]
    dd = D[:, :-h]
    m = dd != 0
    return (np.sign(fut[m]) == dd[m]).mean()

horizontes = [1, 5, 10, 15, 30, 45, 60, 90, 120, 180]
pd.Series({h: acierto_tendencia(h) for h in horizontes},
          name="signo = dirección").rename_axis("h (min)").round(3)
```
```output
h (min)
1      0.540
5      0.621
10     0.662
15     0.687
30     0.714
45     0.735
60     0.732
90     0.719
120    0.711
180    0.676
Name: signo = dirección, dtype: float64
```

La figura de la cabecera dibuja la serie. Con $h = 1$ minuto, la etiqueta coincide con la dirección verdadera el 54 %
de las veces: el ruido de un minuto tapa la deriva. Al alargar el horizonte la deriva se acumula más deprisa que el
ruido (la deriva crece con $h$; el ruido, con $\sqrt{h}$) y el acuerdo sube hasta el 73,5 % con 45 minutos. Después
baja: el régimen dura unos 60 minutos de media, y una etiqueta a 180 minutos mezcla el episodio de tendencia con lo
que venga detrás, que no depende de la dirección de ahora.

No hay un horizonte correcto en general. Hay uno que corresponde al fenómeno que se quiere capturar y a cómo se va a
operar: una estrategia que mantiene la posición media hora necesita una etiqueta del orden de media hora, no de un
minuto (que premia predecir ruido) ni de un día. Y si el fenómeno dura un tiempo variable, ningún $h$ fijo encaja con
todos los eventos: la triple barrera pone el final de cada etiqueta donde el precio decide.

## Solapamiento: la misma evidencia contada varias veces

En el panel, las features son semanales y la etiqueta `ret_sig_21d` cubre 21 sesiones, unas cuatro semanas y
media. La etiqueta de una semana comparte con la de la semana siguiente cuatro de sus cinco semanas de rendimientos.
La lección 5.4.1 mide lo que eso hace a una validación cruzada barajada; aquí interesa lo que hace a la inferencia.
El IC semanal de una feature con esa etiqueta forma una serie autocorrelacionada, y su estadístico $t$ ingenuo,
$\overline{IC}/s_{IC}\cdot\sqrt{n}$, trata cada semana como una observación independiente. Hay dos correcciones
habituales: errores de **Newey-West** (lección 2.1.1), que estiman la varianza de la media con las autocovarianzas
hasta un retardo, y **submuestras sin solape** (una de cada cinco semanas, promediando las cinco posibles):

```python
def ic_por_semana(col, lab):
    x = d[["fecha", col, lab]].dropna()
    rk = x.groupby("fecha")[[col, lab]].rank()
    return rk[col].groupby(x["fecha"]).corr(rk[lab]).to_numpy()

def t_ingenuo(s):
    return s.mean() / s.std() * np.sqrt(len(s))

def t_newey_west(s, retardos=6):
    x = s - s.mean()
    var = x @ x / len(x)
    for k in range(1, retardos + 1):
        var += 2 * (1 - k / (retardos + 1)) * (x[k:] @ x[:-k]) / len(x)
    return s.mean() / np.sqrt(var / len(x))

def t_sin_solape(s, paso=5):
    return np.mean([t_ingenuo(s[i::paso]) for i in range(paso)])

filas = {}
for c in ["alfa_verdadero", "valor_bp", "mom_12_1", "ey",
          "ruido_4", "ruido_3", "ruido_1"]:
    s21 = ic_por_semana(c, "ret_sig_21d")
    s5 = ic_por_semana(c, "ret_sig_semana")
    filas[c] = {"IC 21 s": s21.mean(), "t ingenuo": t_ingenuo(s21),
                "t Newey-West": t_newey_west(s21),
                "t sin solape": t_sin_solape(s21),
                "IC 1 sem.": s5.mean(), "t 1 sem.": t_ingenuo(s5)}
pd.DataFrame(filas).T.round(3)
```
```output
                IC 21 s  t ingenuo  t Newey-West  t sin solape  IC 1 sem.  t 1 sem.
alfa_verdadero    0.030      9.229         5.713         4.135      0.029     8.390
valor_bp          0.020      7.760         3.911         3.477      0.009     3.920
mom_12_1          0.017      5.542         2.951         2.477      0.008     2.445
ey                0.018      7.874         4.061         3.536      0.009     4.059
ruido_4           0.010      4.366         2.722         1.962      0.005     1.934
ruido_3          -0.001     -0.356        -0.200        -0.151     -0.001    -0.641
ruido_1          -0.001     -0.632        -0.653        -0.284     -0.002    -1.322
```

*Fig. — Estadístico t del IC semanal con la etiqueta de 21 sesiones, que se solapa con las cuatro semanas siguientes: ingenuo, con Newey-West (6 retardos) y con submuestras sin solape, frente al t con la etiqueta semanal.* Una fila por feature con cuatro marcas. Los t ingenuos van destacados. Alfa verdadero: t ingenuo 9,2, Newey-West 5,7, sin solape 4,1 y con la etiqueta semanal 8,4. Valor_bp: 7,8, 3,9, 3,5 y 3,9. Ey: 7,9, 4,1, 3,5 y 4,1. Mom_12_1: 5,5, 3,0, 2,5 y 2,4. Ruido_4, sin efecto propio: 4,4, 2,7, 2,0 y 1,9. Ruido_3 y ruido_1, sin efecto, cerca de cero en todas. Línea discontinua en t = 2.

El alfa verdadero tiene con la etiqueta de 21 sesiones un $t$ ingenuo de 9,2; corregido, 5,7 (Newey-West) o 4,1 (sin
solape). La etiqueta semanal, sin solape, da con el mismo alfa $t = 8{,}4$: la etiqueta larga no aporta el doble de
evidencia, aporta menos y la cuenta varias veces. El caso peligroso es `ruido_4`, una feature sin efecto propio que
hereda el del momentum: con el $t$ ingenuo (4,4) parece un descubrimiento; corregido, se queda en 2,0-2,7, el mismo
nivel que con la etiqueta semanal (1,9). En un barrido de cientos de features (lección 2.1.8), el $t$ ingenuo de
etiquetas solapadas multiplica los falsos positivos.

```mermaid
flowchart TB
  E[Solapamiento] --> I[t inflado]
  E --> V[Fuga en la CV]
  E --> W[Filas repetidas]
  I --> I2[Newey-West]
  V --> V2[Purga 5.4.2]
  W --> W2[Unicidad 5.3.4]
```

El solapamiento no es un error en sí: una etiqueta de 21 sesiones puede ser la adecuada para una estrategia que
mantiene las posiciones un mes. El error es tratar las filas como independientes en las tres partes del proceso del
diagrama.

## Una función de etiquetado que guarda el final

Todo lo anterior se resume en una función que devuelve, para cada barra, el final de su etiqueta, el rendimiento y la
clase, con un umbral opcional que debe estar calculado con información hasta $t$:

```python
def horizonte_fijo(precio, h, umbral=None):
    """Etiqueta de cada barra con su rendimiento log a h barras.

    precio: Series con índice temporal (una sola sesión o serie
    continua). umbral: Series de umbrales en log (o None: signo).
    """
    lp = np.log(precio)
    r = lp.shift(-h) - lp
    t1 = pd.Series(precio.index, index=precio.index).shift(-h)
    if umbral is None:
        lab = np.sign(r)
    else:
        lab = np.sign(r).where(r.abs() > umbral, 0.0)
    out = pd.DataFrame({"t1": t1, "ret": r, "etiqueta": lab})
    return out.dropna()

una = b.set_index("ts")["cierre"].iloc[20 * 390:21 * 390]  # sesión 21
umbral = pd.Series(sd[20], index=una.index[:390 - h])
et = horizonte_fijo(una, h, umbral.reindex(una.index))
print(et["etiqueta"].value_counts().sort_index().to_dict())
et.iloc[[0, 1, 359]].round({"ret": 5})
```
```output
{-1.0: 45, 0.0: 188, 1.0: 127}
                                                 t1      ret  etiqueta
ts
2024-07-31 13:30:00+00:00 2024-07-31 14:00:00+00:00  0.01345       1.0
2024-07-31 13:31:00+00:00 2024-07-31 14:01:00+00:00  0.01202       1.0
2024-07-31 19:29:00+00:00 2024-07-31 19:59:00+00:00  0.00605       1.0
```

El índice de la salida es $t$ y la columna `t1`, el final: es la pareja que la purga de la validación (5.4.2) y la
unicidad de las muestras (5.3.4) necesitan. En la sesión del ejemplo, con el umbral igual a la volatilidad prevista,
188 de los 360 minutos etiquetables quedan en la clase 0, 127 en +1 y 45 en −1: una sesión alcista. Las últimas 30
barras de la sesión no tienen etiqueta, porque su horizonte cruzaría el cierre; en datos intradía conviene no
prolongar la etiqueta a la sesión siguiente, donde el salto de la apertura no tiene nada que ver con las features de la
víspera.

## Errores típicos

- **Etiquetar con $h$ en filas en lugar de en tiempo.** `shift(-h)` sobre un DataFrame con huecos (festivos, barras sin
  operaciones) da horizontes distintos para filas distintas. Hay que saber qué es $h$ y guardar `t1`.
- **Cruzar el cierre de la sesión** sin decidirlo: el rendimiento nocturno es otro fenómeno.
- **Umbral calculado con toda la muestra** (por ejemplo, la mediana de $|r|$ de todo el histórico): la misma fuga de
  estadísticos de la lección 5.2.4.
- **Comparar aciertos entre etiquetas distintas.** Un 60 % con la clase 0 incluida no es comparable con un 51 % en una
  binaria: hay que comparar contra la proporción de la clase mayoritaria y, mejor, en métricas económicas (5.1.3).
- **Confiar en el $t$ de etiquetas solapadas** sin corregir.

## Ejercicios

1. En los minutos de **reversión** de FNX, la deriva empuja el precio hacia su media exponencial de 30 minutos. Mide,
   para varios horizontes, con qué frecuencia el signo de la etiqueta es el contrario del de la desviación
   $\ln P_t - \text{EMA}_{30}$. ¿Qué horizonte refleja mejor ese régimen?

**Solución.**

```python
ema = pd.DataFrame(C.T).ewm(span=30).mean().T.to_numpy()
desv = C - ema
rev = (ver["regimen"].to_numpy().reshape(-1, 390) == "reversion")

def acierto_reversion(h, minimo=0.001):
    fut = C[:, h:] - C[:, :-h]
    dv = desv[:, :-h]
    m = rev[:, :-h] & (np.abs(dv) > minimo)
    return (np.sign(fut[m]) == -np.sign(dv[m])).mean(), m.sum()

pd.DataFrame({h: acierto_reversion(h) for h in [1, 5, 15, 30, 60]},
             index=["acierto", "minutos"]).T.round(3)
```
```output
    acierto  minutos
1     0.517  17406.0
5     0.550  17176.0
15    0.567  16655.0
30    0.555  15830.0
60    0.528  14330.0
```

En reversión la etiqueta refleja el régimen mucho peor que en tendencia: el acierto sube del 51,7 % con 1 minuto a un
máximo del 56,7 % con 15 minutos y baja al 52,8 % con 60. La vuelta a la media es lenta (un 2 % de la desviación por
minuto) y se mezcla pronto con el cambio de régimen, así que el horizonte útil es más corto que en tendencia. Un mismo
$h$ no sirve para los dos fenómenos: otro argumento para etiquetas cuyo final dependa del evento (lección 5.3.2) o
para separar los dos problemas con un modelo primario y un meta-modelo (lección 5.3.3). (Se descartan las
desviaciones menores de 10 pb, donde el signo de la desviación es casi aleatorio.)

2. Con el umbral igual a $k$ veces la volatilidad prevista de 30 minutos, calcula la proporción de cada clase para
   $k$ = 0,5, 1 y 1,5. ¿Qué $k$ deja las tres clases más equilibradas?

**Solución.**

```python
reparto = {}
for k in [0.5, 1.0, 1.5]:
    lab = np.where(fut > k * sd, 1, np.where(fut < -k * sd, -1, 0))
    reparto[k] = pd.Series(lab[ok]).value_counts(normalize=True)
pd.DataFrame(reparto).T.round(3)
```
```output
         0     -1      1
0.5  0.412  0.298  0.289
1.0  0.707  0.149  0.144
1.5  0.867  0.068  0.065
```

Con $k = 1$, el 70,7 % de las etiquetas quedan en la clase 0; con $k = 1{,}5$, el 86,7 %. El reparto más
equilibrado es el de $k = 0{,}5$: 41 % de ceros y alrededor del 29-30 % de cada signo. No hay un $k$ correcto: un
umbral alto deja un problema muy desequilibrado (un clasificador que diga siempre 0 acierta el 87 %), y uno bajo
vuelve a la etiqueta binaria con su ruido. Elegirlo mirando el resultado de la validación es otra forma de sobreajuste
(lección 5.5.4); conviene fijarlo por el problema (el coste de operar, el movimiento mínimo que paga la horquilla).

3. Una estrategia larga-corta del panel rebalancea cada viernes y mantiene la cartera una semana. Un compañero propone
   entrenar con `ret_sig_21d` «porque tiene más señal» (su IC con el alfa es 0,030 frente a 0,029). ¿Qué le contestas?

**Solución.**

Que el IC medio parecido no significa más información. La etiqueta de 21 sesiones, muestreada cada semana, se solapa
con las cuatro siguientes: su $t$ corregido (4,1-5,7) es menor que el de la etiqueta semanal (8,4), así que aporta
menos evidencia independiente, no más. Además mide otra cosa que lo que la estrategia cobra: la cartera se rehace cada
semana, y lo que gana o pierde es el rendimiento de esa semana. Si se quiere usar la de 21 sesiones (por ejemplo, para
reducir rotación con una señal más lenta), hay que purgar la validación con su `t1` (5.4.2), ponderar por unicidad
(5.3.4) y corregir cualquier estadístico con Newey-West o con submuestras sin solape.

## Autoevaluación

1. En el panel, el signo de la etiqueta semanal (rendimiento menos la mediana de la semana) coincide con el signo del alfa verdadero en el 50,9 % de las filas. ¿Qué significa?

   - A. Que la etiqueta está mal calculada
   - B. Que el alfa verdadero no sirve para predecir
   - C. Que hay que usar un horizonte más corto
   - D. Que casi toda la etiqueta es ruido: el alfa plantado es pequeño frente a la dispersión de los rendimientos, y un modelo perfecto solo acertaría ese 50,9 %

   *Respuesta D: Que casi toda la etiqueta es ruido: el alfa plantado es pequeño frente a la dispersión de los rendimientos, y un modelo perfecto solo acertaría ese 50,9 %.* El oráculo es la mejor previsión posible. Si ni siquiera él coincide con el signo de la etiqueta más que el 50,9 % de las veces, ningún modelo puede pasar de ahí; un acierto mayor fuera de muestra indica una fuga.

2. Con un umbral fijo de 20 pb para etiquetar el rendimiento a 30 minutos de FNX, el 52 % de las etiquetas de las 9:30 son ±1 y solo el 29 % de las de las 12:00. ¿Qué está midiendo sobre todo la etiqueta?

   - A. Que por la mañana hay más señal
   - B. Que el modelo primario funciona mejor a primera hora
   - C. La volatilidad de cada franja: con una curva en U, el mismo umbral es pequeño en la apertura y grande a mediodía
   - D. El volumen de cada franja

   *Respuesta C: La volatilidad de cada franja: con una curva en U, el mismo umbral es pequeño en la apertura y grande a mediodía.* Con el umbral escalado por la volatilidad prevista de cada franja (calculada con las 20 sesiones anteriores), la fracción de etiquetas ±1 queda entre el 26 % y el 32 % todo el día.

3. En los minutos de tendencia de FNX, el signo del rendimiento a h minutos coincide con la dirección verdadera el 54,0 % de las veces con h = 1, el 73,5 % con h = 45 y el 67,6 % con h = 180. ¿Por qué baja al alargar el horizonte?

   - A. Porque el régimen dura de media unos 60 minutos: más allá, la etiqueta mezcla el episodio de tendencia con lo que venga después, que no depende de la dirección de ahora
   - B. Por el redondeo del precio
   - C. Porque hay menos observaciones
   - D. Porque la volatilidad baja por la tarde

   *Respuesta A: Porque el régimen dura de media unos 60 minutos: más allá, la etiqueta mezcla el episodio de tendencia con lo que venga después, que no depende de la dirección de ahora.* Con horizontes cortos manda el ruido de cada minuto; con horizontes largos, el cambio de régimen. El horizonte que mejor refleja el estado en t está cerca de la duración típica del fenómeno que se quiere capturar.

4. El IC del alfa verdadero con la etiqueta de 21 sesiones, muestreada cada semana, tiene un t ingenuo de 9,2; con Newey-West es 5,7 y con submuestras sin solape, 4,1. ¿Cuál es la lectura correcta?

   - A. Que la etiqueta de 21 sesiones tiene el doble de información que la semanal
   - B. Que cada rendimiento entra en unas cinco etiquetas seguidas: el t ingenuo cuenta varias veces la misma evidencia y hay que corregirlo
   - C. Que Newey-West siempre reduce el t a la mitad
   - D. Que el alfa verdadero no es significativo

   *Respuesta B: Que cada rendimiento entra en unas cinco etiquetas seguidas: el t ingenuo cuenta varias veces la misma evidencia y hay que corregirlo.* Las etiquetas de 21 sesiones de semanas consecutivas comparten cuatro de sus cinco semanas, así que la serie de IC está autocorrelacionada. La etiqueta semanal, sin solape, da t = 8,4 con el mismo alfa.

5. ¿Por qué la función de etiquetado debe devolver, además de la etiqueta, el momento en que termina (`t1`)?

   - A. Para dibujarla
   - B. Porque scikit-learn lo exige
   - C. Para calcular el umbral
   - D. Porque la purga de la validación (5.4.2) y los pesos por unicidad (5.3.4) necesitan saber qué intervalo de precios usa cada etiqueta

   *Respuesta D: Porque la purga de la validación (5.4.2) y los pesos por unicidad (5.3.4) necesitan saber qué intervalo de precios usa cada etiqueta.* Dos etiquetas se solapan si sus intervalos de t a t1 se cruzan. Sin t1 no se puede purgar el entrenamiento ni medir la concurrencia; con horizontes en sesiones, festivos y cierres de sesión, t1 no es simplemente t + h.

## Ideas clave

- La etiqueta de horizonte fijo es el signo (o el rendimiento) de t a t + h, a veces con un umbral que crea una clase 0. Hay que guardar con ella su final, t1.
- Casi toda la etiqueta es ruido: en el panel, el signo de la etiqueta semanal coincide con el del alfa verdadero en el 50,9 % de las filas. Quitar los rendimientos pequeños solo lo sube al 51,6 % y quitar demasiados lo vuelve a bajar.
- Un umbral fijo reparte las clases según la volatilidad: del 35 % al 53 % de etiquetas ±1 según el año en el panel y del 29 % al 52 % según la hora en FNX. Un umbral proporcional a la volatilidad prevista lo estabiliza.
- El horizonte decide qué mide la etiqueta. En FNX, el signo a h minutos refleja la tendencia latente el 54 % de las veces con 1 minuto, el 73,5 % con 45 y el 67,6 % con 180.
- Etiquetas solapadas inflan la evidencia: con 21 sesiones muestreadas cada semana, el t ingenuo del alfa verdadero es 9,2 y el corregido, entre 4,1 y 5,7.
- El método de la triple barrera (5.3.2) añade la trayectoria y el horizonte en función del evento; los pesos por unicidad (5.3.4) y la purga (5.4.2) tratan el solapamiento.

## Recursos

### Documentación

- [statsmodels: cov_hac (errores de Newey-West)](https://www.statsmodels.org/stable/generated/statsmodels.stats.sandwich_covariance.cov_hac.html) · statsmodels.org

### Libros

- [López de Prado (2018): Advances in Financial Machine Learning, Wiley (cap. 3, etiquetado)](https://www.wiley.com/en-us/Advances+in+Financial+Machine+Learning-p-9781119482086) · wiley.com

### Datos

- [Datos del nivel 5: etiquetas del panel (Parquet)](https://pythonparatrading.com/curso/datos/nivel-5/panel5_etiquetas.parquet) · Descarga · panel5_etiquetas.parquet
- [Datos del nivel 5: barras de 1 minuto de FNX (Parquet)](https://pythonparatrading.com/curso/datos/nivel-5/hf5_barras_1min.parquet) · Descarga · hf5_barras_1min.parquet
- [Datos del nivel 5: verdad de FNX, régimen y dirección (Parquet)](https://pythonparatrading.com/curso/datos/nivel-5/hf5_verdad.parquet) · Descarga · hf5_verdad.parquet

Lección anterior: [5.2.5 · Importancia de features: MDI, MDA, SFI y SHAP](https://pythonparatrading.com/curso-python-trading/nivel-5/importancia-de-features-mdi-mda-sfi-y-shap/)

Lección siguiente: [5.3.2 · Método de la triple barrera: etiquetas que imitan una operación](https://pythonparatrading.com/curso-python-trading/nivel-5/metodo-de-la-triple-barrera/)
