---
title: "2.3.2 Reversión a la media: RSI, Bollinger y gaps"
description: "Cierre de huecos de apertura en barras de 5 minutos, RSI y Bollinger en acciones con controles, y por qué los costes matan la reversión de corto plazo."
url: "https://pythonparatrading.com/curso-python-trading/nivel-2/reversion-a-la-media-y-gaps/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-2/reversion-a-la-media-y-gaps.png"
---

# 2.3.2 · Reversión a la media: RSI y Bollinger, reversión intradía y huecos

> Comprar lo que acaba de caer y vender lo que acaba de subir es la estrategia más intuitiva y la que peor sobrevive a los costes. Esta lección mide un efecto de cierre de hueco plantado en barras de 5 minutos, lo compara con el resto de la sesión, que no tiene ninguno, y prueba las reglas de RSI y Bollinger en 120 acciones donde no hay reversión que explotar.

- Curso: [Nivel 2: Estrategias sistemáticas y backtesting riguroso](https://pythonparatrading.com/curso-python-trading/nivel-2/)
- Módulo: 2.3 · Familias de estrategias
- Duración: 65 min
- Requisitos: [1.7.1 · Indicadores técnicos en Python](https://pythonparatrading.com/curso-python-trading/nivel-1/indicadores-tecnicos-en-python/), [1.7.2 · Implementación propia frente a TA-Lib: validar resultados](https://pythonparatrading.com/curso-python-trading/nivel-1/validar-indicadores-frente-a-ta-lib/), [1.1.5 · Costes, liquidación, apalancamiento y margen](https://pythonparatrading.com/curso-python-trading/nivel-1/costes-liquidacion-apalancamiento-y-margen/)

## Al terminar esta lección

- Medir el cierre parcial del hueco de apertura con una regresión con errores robustos y comparar la estimación con el efecto plantado.
- Convertir ese efecto en una regla con ejecución en la barra siguiente y calcular su coste de equilibrio por lado.
- Usar el resto de la sesión como control y reconocer un falso positivo cercano al umbral.
- Programar reglas de RSI y bandas de Bollinger con TA-Lib, evaluarlas contra el universo con errores HAC y separar lo que es reversión de lo que es continuación heredada.
- Explicar por qué la rotación de las reglas de pocos días hace que los costes dominen el resultado.

## Revertir a la media: de dónde podría salir la ventaja

Una estrategia de reversión a la media apuesta a que un movimiento reciente se deshará en parte: compra lo que acaba de
caer y vende lo que acaba de subir. Es lo contrario del seguimiento de tendencia de la lección 2.3.1, y su lógica
económica es distinta. Cuando un vendedor con prisa empuja el precio por debajo de su valor, alguien tiene que
comprarle: el que lo hace presta **liquidez** y cobra por ello la vuelta del precio. Si el movimiento fue una
reacción exagerada a una noticia, quien apuesta contra ella cobra la corrección.

```mermaid
flowchart TB
  M[Movimiento brusco] --> L[Prisa por vender]
  M --> N[Noticia nueva]
  L --> R[Revierte: gana]
  N --> C[Continúa: pierde]
  R --> K[Menos costes]
  C --> K
```

La dificultad está en la última caja. Una ventaja de reversión suele ser pequeña por operación y se opera mucho, así que
los costes son la mitad de la historia. En datos reales hay evidencia de reversión de corto plazo en acciones:
Jegadeesh documentó en 1990 una autocorrelación negativa muy significativa de las rentabilidades mensuales de acciones
individuales de EE. UU.[^1], y Lehmann, ese mismo año, que las ganadoras de una semana tendían a perder la siguiente y
las perdedoras a recuperarse, también tras corregir por horquillas y con costes plausibles[^2]. En los datos de esta
lección, en cambio, se sabe exactamente dónde hay reversión y dónde no.

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

Datos **sintéticos con un efecto plantado**. En las barras de 5 minutos del ETF ficticio KAPA, el rendimiento esperado
de las seis primeras barras (09:30-10:00) es `−0,10 × hueco`: el hueco de apertura tiende a cerrarse un 10 %. Fuera de
esa media hora, los rendimientos no tienen autocorrelación. En las 120 acciones del universo **no hay ninguna
reversión plantada**: las reglas de RSI y Bollinger se prueban donde no debería haber nada.

</aside>

## El hueco de apertura en KAPA

`indice2_5min.parquet` tiene 1.003 sesiones de la NYSE de 2022 a 2025 en barras de 5 minutos. La apertura de cada
barra es el cierre de la anterior, salvo la primera de cada sesión, que lleva el **hueco** nocturno: lo que se movió el
precio entre el cierre de ayer y la apertura de hoy.

```python
import numpy as np
import pandas as pd
import statsmodels.api as sm

b = pd.read_parquet("indice2_5min.parquet")
b["ny"] = b["ts"].dt.tz_convert("America/New_York")
b["dia"] = b["ny"].dt.date
b["k"] = b.groupby("dia").cumcount()        # barra 0 = 09:30
barras = b.groupby("dia").size()
print(len(barras), "sesiones;", barras.value_counts().to_dict())
print("primera barra:",
      b.loc[b["k"] == 0, "ny"].dt.strftime("%H:%M").unique().tolist())
```
```output
1003 sesiones; {78: 994, 42: 9}
primera barra: ['09:30']
```

Hay 994 sesiones completas de 78 barras (de 09:30 a 16:00) y 9 medias sesiones de 42 (hasta las 13:00), y la primera
barra de todas empieza a las 09:30 de Nueva York: la conversión desde UTC respeta el horario de verano (lección 1.1.2).
Contar las barras por sesión es la comprobación más barata de que la zona horaria y el calendario están bien.

Para cada sesión se necesitan el hueco y el recorrido de la primera media hora, separado en la primera barra y las
cinco siguientes, y como control, las barras de 10:00 a 10:30 y el resto del día:

$$
h_t = \ln\frac{A_t}{C_{t-1}}, \qquad r^{(0-6)}_t = \ln\frac{C^{(6)}_t}{A_t}, \qquad r^{(2-6)}_t = \ln\frac{C^{(6)}_t}{C^{(1)}_t}
$$

donde $A_t$ es la apertura de la sesión, $C_{t-1}$ el cierre anterior y $C^{(j)}_t$ el cierre de la barra `j`.

```python
g = b.groupby("dia")
cierre_k = lambda k: b[b["k"] == k].set_index("dia")["cierre"]
d = pd.DataFrame({"apertura": g["apertura"].first(),
                  "cierre": g["cierre"].last(),
                  "c1": cierre_k(0), "c6": cierre_k(5),
                  "c12": cierre_k(11)})
d["hueco"] = np.log(d["apertura"] / d["cierre"].shift())
d["r0_6"] = np.log(d["c6"] / d["apertura"])
d["r2_6"] = np.log(d["c6"] / d["c1"])
d["r7_12"] = np.log(d["c12"] / d["c6"])
d["resto"] = np.log(d["cierre"] / d["c12"])
d = d.dropna()
print(len(d), "sesiones con hueco")
(1e4 * d[["hueco", "r0_6", "r7_12", "resto"]]).describe().loc[
    ["mean", "std", "min", "max"]].round(1)
```
```output
1002 sesiones con hueco
       hueco   r0_6  r7_12  resto
mean     3.8   -0.3    0.8    3.8
std     60.8   47.9   26.4   79.8
min   -189.3 -245.5  -83.7 -322.4
max   1028.7  188.2  143.5  285.4
```

En puntos básicos, el hueco tiene una desviación típica de 61 (con un día extremo de +1.029, un hueco de más del
10 %), la primera media hora se mueve 48 y la media hora siguiente, 26: el perfil intradía en U del generador, con
la volatilidad concentrada en la apertura. Las medias son casi cero en todas las ventanas, como corresponde a un
mercado sin deriva visible a esta escala.

## ¿Se cierra el hueco? Una regresión con errores robustos

La pregunta es una regresión del recorrido de la primera media hora sobre el hueco, $r^{(0-6)}_t = a + b\,h_t + e_t$.
Si el hueco tiende a cerrarse, $b < 0$. Los errores típicos se calculan robustos a heterocedasticidad (HC1), porque los
días de hueco grande son también días de volatilidad alta (lección 2.1.1).

```python
def regresion(y, x="hueco"):
    m = sm.OLS(d[y], sm.add_constant(d[x])).fit(cov_type="HC1")
    return pd.Series({"b": m.params[x], "ee": m.bse[x],
                      "t": m.tvalues[x], "p": m.pvalues[x]})

tabla = pd.DataFrame({v: regresion(v)
                      for v in ["r0_6", "r2_6", "r7_12", "resto"]}).T
print(tabla.round(3).to_string())
b_est, ee = tabla.loc["r0_6", ["b", "ee"]]
print(f"distancia a la verdad (−0,10): {(b_est + 0.10) / ee:+.2f} ee")
print(f"intervalo al 95 %: [{b_est - 1.96 * ee:.3f}, "
      f"{b_est + 1.96 * ee:.3f}]")
```
```output
           b     ee      t      p
r0_6  -0.132  0.038 -3.518  0.000
r2_6  -0.108  0.023 -4.768  0.000
r7_12  0.027  0.016  1.668  0.095
resto -0.027  0.044 -0.613  0.540
distancia a la verdad (−0,10): -0.86 ee
intervalo al 95 %: [-0.206, -0.059]
```

La primera media hora entera da $b = -0{,}132$ con t = −3,5. La verdad del generador es −0,10: la estimación está a
0,86 errores típicos, perfectamente compatible. Pero el intervalo al 95 % va de −0,206 a −0,059: con cuatro años de
sesiones, se sabe que el hueco tiende a cerrarse, no cuánto. Desde la segunda barra (la que se puede operar), el
coeficiente es −0,108 con un error menor (0,023): lo plantado en esas cinco barras es $-0{,}10 \cdot 5/6 \approx
-0{,}083$, también a menos de un error típico y medio.

Las dos ventanas de control no tienen efecto en el generador, y sus estimaciones lo reflejan solo a medias: el resto
del día da −0,027 (t = −0,6), sin nada, pero la media hora de 10:00 a 10:30 da +0,027 con t = 1,67 y un p-valor de
0,095. No está plantado. Es el aspecto que tiene un control cualquiera: la mayoría de las veces sale cerca de cero y,
de vez en cuando, cerca del umbral. Si alguien hubiera probado diez ventanas de la sesión y publicado la de 10:00 a
10:30 como «continuación del hueco», tendría un resultado tan convincente como falso.

La figura de la cabecera enseña la misma relación sin modelo: las sesiones agrupadas en veinte grupos por el tamaño del
hueco, con el hueco medio de cada grupo y el recorrido medio de la primera media hora. La recta de pendiente −0,132 es
la estimación; la de −0,10, la verdad del generador.

```python
grupo = pd.qcut(d["hueco"], 20, labels=False)
binned = (1e4 * d.groupby(grupo)[["hueco", "r0_6"]].mean()).round(1)
print(binned.iloc[[0, 1, 9, 10, 18, 19]].to_string())
```
```output
       hueco  r0_6
hueco
0     -100.9   9.4
1      -65.6  -3.1
9       -1.2   6.1
10       4.9   0.0
18      73.6 -11.4
19     150.9 -11.8
```

El grupo de huecos más negativos (−101 puntos básicos de media) recupera 9,4 en la primera media hora, y el de huecos
más positivos (+151) devuelve 11,8: un 9 % y un 8 % del hueco, cerca del 10 % plantado. En el centro, con huecos de
uno o cinco puntos básicos, el recorrido medio es ruido de ±6 puntos. El efecto solo se ve donde el hueco es grande.

## De un coeficiente a una regla

Una regla de cierre de hueco hace la apuesta contraria al hueco durante la primera media hora. Se decide con el primer
precio de la sesión, pero se ejecuta en la barra siguiente, al cierre de la primera barra de 5 minutos, y se cierra al
de la sexta:

```mermaid
flowchart LR
  A[09:30 hueco] --> E[09:35 entra]
  E --> S[10:00 sale]
  S --> X[Resto: nada]
```

La ganancia esperada por operación, si el efecto es lineal, es $0{,}10 \cdot \tfrac{5}{6} \cdot |h_t|$: proporcional al
tamaño del hueco. Por eso tiene sentido operar solo los huecos grandes, que pagan más por operación aunque haya menos.

```python
def regla(umbral):
    s = d[d["hueco"].abs() > umbral]
    return -np.sign(s["hueco"]) * s["r2_6"]   # contra el hueco

filas = []
for umbral in (0.0, 0.002, 0.004, 0.006, 0.008):
    pnl = regla(umbral)
    t = pnl.mean() / pnl.std() * np.sqrt(len(pnl))
    filas.append({"umbral_pb": 1e4 * umbral, "operaciones": len(pnl),
                  "bruto_pb": 1e4 * pnl.mean(), "t": t,
                  "aciertos": (pnl > 0).mean()})
pd.DataFrame(filas).round(2)
```
```output
   umbral_pb  operaciones  bruto_pb     t  aciertos
0        0.0          999      4.00  3.18      0.55
1       20.0          623      4.39  2.66      0.56
2       40.0          342      7.48  3.19      0.60
3       60.0          183      6.96  1.93      0.57
4       80.0          112     11.50  2.30      0.60
```

Operando todos los huecos, 999 operaciones ganan 4,0 puntos básicos de media en bruto (t = 3,2), con un 55 % de
aciertos. Con huecos de más de 40 puntos básicos quedan 342 operaciones que ganan 7,5, y con más de 80, 112 que ganan
11,5: la ganancia por operación crece con el umbral, como predice un efecto proporcional al hueco, pero el número de
operaciones cae y el t no mejora (2,3 en el umbral más alto). El 0,55-0,60 de aciertos recuerda que una ventaja
estadística real se parece mucho a lanzar una moneda.

Cada operación paga dos lados: la entrada y la salida. Con un coste $c$ por lado, el neto por operación es el bruto
menos $2c$, y el **coste de equilibrio** por lado es la mitad del bruto:

```python
costes_pb = np.arange(0, 6.5, 0.5)
sens = pd.DataFrame({
    f">{1e4 * u:.0f} pb": [1e4 * regla(u).mean() - 2 * c
                           for c in costes_pb]
    for u in (0.0, 0.004, 0.008)}, index=costes_pb)
sens.index.name = "coste_lado_pb"
print(sens.iloc[::2].round(2).to_string())
for u in (0.0, 0.004, 0.008):
    print(f"equilibrio con huecos > {1e4 * u:.0f} pb: "
          f"{1e4 * regla(u).mean() / 2:.2f} pb por lado")
```
```output
               >0 pb  >40 pb  >80 pb
coste_lado_pb
0.0              4.0    7.48    11.5
1.0              2.0    5.48     9.5
2.0              0.0    3.48     7.5
3.0             -2.0    1.48     5.5
4.0             -4.0   -0.52     3.5
5.0             -6.0   -2.52     1.5
6.0             -8.0   -4.52    -0.5
equilibrio con huecos > 0 pb: 2.00 pb por lado
equilibrio con huecos > 40 pb: 3.74 pb por lado
equilibrio con huecos > 80 pb: 5.75 pb por lado
```

*Fig. — Regla de cierre de hueco en KAPA: ganancia neta por operación según el coste por lado, para tres tamaños mínimos de hueco.* Tres rectas descendentes de pendiente −2: cada punto básico de coste por lado resta dos al neto. Con todos los huecos, la regla empieza en 4,0 puntos básicos y cruza el cero en 2 por lado; con huecos de más de 40 (destacada) empieza en 7,5 y cruza en 3,7; con huecos de más de 80, empieza en 11,5 y cruza en 5,8.

Con todos los huecos, el equilibrio está en 2 puntos básicos por lado: a partir de ahí la regla pierde. Filtrando
huecos de más de 40 puntos básicos aguanta 3,7, y con más de 80, 5,8. La figura enseña las tres rectas: cada punto
básico de coste por lado resta dos al neto de cada operación.

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

El coste de un ETF muy líquido sobre un índice puede ser de un punto básico por lado o menos en la horquilla, pero a
las 09:35 la horquilla suele estar más abierta que a mediodía (lección 1.1.3) y una orden a mercado paga además el
deslizamiento. Un margen de equilibrio de 2 a 4 puntos básicos se come con muy poco. La regla también concentra el
riesgo en la parte más volátil de la sesión: una pérdida en un día de noticias puede borrar muchas ganancias pequeñas.

</aside>

## El resto de la sesión: un control con un susto

El generador no tiene ningún efecto fuera de la primera media hora. Además de la ventana de 10:00 a 10:30 que ya está
en la tabla, el control más general es la autocorrelación de los rendimientos intradía: si una barra de 5 minutos, o
una media hora, predijera la siguiente, habría reversión (o continuación) intradía que explotar.

```python
b["r"] = np.log(b["cierre"] / b["apertura"])
tarde = b[b["k"] >= 6].copy()
tarde["previa"] = tarde.groupby("dia")["r"].shift(1)
media_h = (tarde.assign(h=tarde["k"] // 6)
           .groupby(["dia", "h"])["r"].sum().rename("r").to_frame())
media_h["previa"] = media_h.groupby(level="dia")["r"].shift(1)

for nombre, df in (("5 minutos", tarde), ("30 minutos", media_h)):
    df = df.dropna(subset=["r", "previa"])
    m = sm.OLS(df["r"], sm.add_constant(df["previa"])
               ).fit(cov_type="HC1")
    print(f"{nombre:>10}: b = {m.params['previa']:+.4f}  "
          f"t = {m.tvalues['previa']:+.2f}  n = {len(df)}")
```
```output
 5 minutos: b = +0.0005  t = +0.10  n = 70889
30 minutos: b = -0.0083  t = -0.66  n = 10979
```

Ni una barra de 5 minutos predice la siguiente (b = +0,0005, t = 0,1, con 70.889 pares) ni una media hora predice la
siguiente (b = −0,008, t = −0,7). Fuera de la apertura no hay reversión intradía en estos datos, y una regla que la
buscara solo pagaría costes. Es también un control del propio análisis: si el código de alineación de barras
estuviera mal (una barra mezclada con la siguiente), aparecería una autocorrelación espuria aquí.

## RSI y bandas de Bollinger en acciones

Las dos reglas de reversión más populares en análisis técnico se aplican a precios diarios (lección 1.7.1):

- **RSI** (*relative strength index*) de Wilder: compara las subidas medias con las bajadas medias de las últimas
  `n` sesiones en una escala de 0 a 100. Por debajo de 30 se dice «sobreventa»; por encima de 70, «sobrecompra». La
  variante de 2 sesiones, con umbrales de 10 y 90, es la versión de muy corto plazo.
- **Bandas de Bollinger**: una media de 20 sesiones más y menos dos desviaciones típicas. Un cierre bajo la banda
  inferior se lee como un exceso a la baja.

Una regla de reversión es una máquina de dos estados:

```mermaid
stateDiagram-v2
  [*] --> FUERA
  FUERA --> DENTRO: RSI menor de 30
  DENTRO --> FUERA: RSI mayor de 50
```

TA-Lib calcula los dos indicadores con las mismas fórmulas que la mayoría de las plataformas (lección 1.7.2). Se
aplican sobre los precios ajustados de las 120 acciones:

```python
import talib

aj = pd.read_parquet("universo2_ajustados.parquet")
P = aj.pivot(index="fecha", columns="ticker",
             values="cierre_ajustado")
R = P.pct_change(fill_method=None)

def por_accion(f):
    return P.apply(lambda s: pd.Series(
        f(s.to_numpy(dtype=float)), index=s.index))

rsi14 = por_accion(lambda x: talib.RSI(x, timeperiod=14))
rsi2 = por_accion(lambda x: talib.RSI(x, timeperiod=2))
banda_alta = por_accion(lambda x: talib.BBANDS(x, 20, 2, 2)[0])
media20 = por_accion(lambda x: talib.BBANDS(x, 20, 2, 2)[1])
banda_baja = por_accion(lambda x: talib.BBANDS(x, 20, 2, 2)[2])
ejemplo = pd.DataFrame({"cierre": P["QXAJ"], "rsi14": rsi14["QXAJ"],
                        "rsi2": rsi2["QXAJ"],
                        "banda_baja": banda_baja["QXAJ"]})
ejemplo.loc["2020-03-16":"2020-03-20"].round(2)
```
```output
            cierre  rsi14   rsi2  banda_baja
fecha
2020-03-16   11.06  26.68  13.75       10.48
2020-03-17   10.62  24.53   6.84       10.13
2020-03-18    9.49  20.06   1.91        9.56
2020-03-19    9.25  19.25   1.46        9.05
2020-03-20    9.22  19.15   1.38        8.63
```

En la semana del 16 de marzo de 2020, en plena caída del guion, la acción QXAJ tiene un RSI de 14 sesiones por debajo
de 30 todos los días y un RSI de 2 sesiones que baja hasta 1,38, y cierra bajo su banda inferior el 18 de marzo. Es
exactamente la situación que una regla de reversión compraría, y en la que una regla de tendencia vendería.

La evaluación tiene que separar la regla del mercado. Una cartera larga en acciones gana en estos datos aunque la regla
no sirva, porque el mercado sube en promedio. Por eso se mide la rentabilidad **en exceso sobre el universo
equiponderado** del mismo día: si la regla no sabe nada, el exceso es cero. Cada día, la cartera reparte el capital por
igual entre las acciones en estado DENTRO, se opera al cierre siguiente y paga media horquilla por cada cambio. El t
usa errores HAC (Newey-West) de 5 retardos, porque las posiciones duran varios días y los resultados diarios no son
independientes.

```python
car = pd.read_parquet("universo2_caracteristicas.parquet")
media_horq = (car.pivot(index="fecha", columns="ticker",
                        values="horquilla_pb")
              .reindex(P.index, method="ffill") / 2 / 1e4)

def estado(entra, sale):
    s = pd.DataFrame(np.where(entra, 1.0, np.where(sale, 0.0, np.nan)),
                     index=P.index, columns=P.columns)
    return s.ffill().fillna(0.0)

def evalua(senal, excluir=()):
    cols = [c for c in P.columns if c not in excluir]
    pos = senal[cols].shift(1)                 # al cierre siguiente
    w = pos.div(pos.sum(axis=1).replace(0, np.nan), axis=0).fillna(0)
    ex = R[cols].sub(R[cols].mean(axis=1), axis=0)   # exceso
    bruto = (w.shift(1) * ex).sum(axis=1)
    giro = (w - w.shift()).abs()
    coste = (giro * media_horq[cols]).sum(axis=1)
    activo = w.shift(1).sum(axis=1) > 0
    m = sm.OLS(bruto[activo], np.ones(activo.sum())).fit(
        cov_type="HAC", cov_kwds={"maxlags": 5})
    return pd.Series({
        "bruto_anual": 252 * bruto[activo].mean(),
        "t": m.tvalues.iloc[0],
        "coste_anual": 252 * coste[activo].mean(),
        "neto_anual": 252 * (bruto - coste)[activo].mean(),
        "giro_diario": giro.sum(axis=1)[activo].mean()})

reglas = {
    "rsi14 <30 a >50": estado(rsi14 < 30, rsi14 > 50),
    "rsi2 <10 a >50": estado(rsi2 < 10, rsi2 > 50),
    "bollinger baja a media": estado(P < banda_baja, P > media20),
    "rsi2 >90 a <50 (largo)": estado(rsi2 > 90, rsi2 < 50),
}
res = pd.DataFrame({k: evalua(v) for k, v in reglas.items()}).T
res.round(3)
```
```output
                        bruto_anual      t  coste_anual  neto_anual  giro_diario
rsi14 <30 a >50              -0.054 -3.267        0.008      -0.062        0.111
rsi2 <10 a >50                0.005  0.314        0.043      -0.038        0.616
bollinger baja a media       -0.022 -1.118        0.021      -0.044        0.268
rsi2 >90 a <50 (largo)        0.035  1.449        0.064      -0.029        0.784
```

Ninguna regla gana en neto. La de RSI de 14 pierde un 5,4 % anual frente al universo **en bruto**, con t = −3,3: no
es que no gane, es que acierta al revés de forma significativa. RSI(2) gana un 0,5 % en bruto (t = 0,3, nada) y
paga un 4,3 % de costes, porque cambia cada día el 62 % de la cartera. Bollinger pierde un 2,2 % en bruto. Y la regla
contraria, comprar la sobrecompra con RSI(2) por encima de 90, gana un 3,5 % en bruto (t = 1,4) que sus costes del
6,4 % convierten en una pérdida. La regla de reversión pierde y la de continuación gana un poco: el signo de lo que
hay en los datos es el contrario del que se buscaba.

## Continuación, no reversión

¿Por qué pierde una regla que compra sobreventa? Porque en estos datos no hay reversión de corto plazo y sí algo de
continuación. El universo tiene tres fuentes de continuación plantadas: la prima de momentum (lección 2.3.1), la deriva
tras los resultados (lección 2.3.5) y, sobre todo, cuatro quiebras guiadas: durante las 320 sesiones anteriores a
cada quiebra el precio se lleva hacia un último precio de entre 0,30 y 1,50 USD. Una acción así pasa meses en
sobreventa, y la regla la compra una y otra vez.

```python
quiebras = ["QXQT", "QXKV", "QXZT", "QXYX"]
sin_q = pd.DataFrame({k: evalua(v, excluir=quiebras)
                      for k, v in reglas.items()}).T
sin_q[["bruto_anual", "t", "neto_anual"]].round(3)
```
```output
                        bruto_anual      t  neto_anual
rsi14 <30 a >50              -0.025 -1.289      -0.034
rsi2 <10 a >50                0.021  1.224      -0.026
bollinger baja a media        0.005  0.235      -0.016
rsi2 >90 a <50 (largo)        0.016  0.732      -0.043
```

Sin las cuatro quiebras, el RSI de 14 pasa de −5,4 % a −2,5 % (t = −1,3), RSI(2) de +0,5 % a +2,1 % (t = 1,2) y
Bollinger de −2,2 % a +0,5 %. Ninguna es significativa y todas pierden en neto. Más de la mitad del resultado negativo
del RSI de 14 era una sola cosa: comprar acciones en su camino hacia la quiebra. Ojo con el remedio, sin embargo:
quitar las quiebras a posteriori es un sesgo de supervivencia (lección 1.4.4). Sirve para entender el resultado, no
para mejorarlo: en tiempo real no se sabe qué acción va a quebrar.

La reversión clásica de la literatura es transversal: comprar las perdedoras de la última semana o del último mes
frente a las ganadoras. Es el mismo ejercicio que el momentum de la lección 2.3.1, con el signo cambiado y un horizonte
más corto:

```python
def perdedoras(n):
    rk = (P / P.shift(n) - 1).rank(axis=1, pct=True)
    return (rk <= 0.1).astype(float)

rev = pd.DataFrame({f"perdedoras {n}d": evalua(perdedoras(n),
                                                excluir=quiebras)
                    for n in (5, 21)}).T
rev.round(3)
```
```output
                bruto_anual      t  coste_anual  neto_anual  giro_diario
perdedoras 5d        -0.007 -0.262        0.071      -0.078        0.823
perdedoras 21d        0.001  0.021        0.037      -0.036        0.409
```

*Fig. — Reglas de reversión en 120 acciones sintéticas sin reversión plantada (sin las cuatro quiebras): exceso anual bruto y neto de media horquilla, 2006-2025.* Seis pares de barras horizontales, bruta arriba y neta abajo. Brutas entre −2,5 % y +2,1 %, ninguna significativa. Netas todas negativas: RSI de 14, −3,4 %; RSI de 2 por debajo de 10, −2,6 % (destacada); Bollinger, −1,6 %; RSI de 2 por encima de 90, −4,3 %; perdedoras de 5 días, −7,8 %; perdedoras de 21 días, −3,6 %.

Las perdedoras de una semana rinden −0,7 % anual frente al universo (t = −0,3) y las de un mes, +0,1 % (t = 0,0):
nada en las dos, como corresponde a unos datos sin reversión plantada. Con costes, −7,8 % y −3,6 %. La figura
reúne las seis reglas sin quiebras: barras brutas casi nulas y barras netas negativas, de −1,6 % a −7,8 %.

## Los costes deciden

La columna `giro_diario` es la fracción de la cartera que cambia cada día. Multiplicada por el coste de una unidad de
giro y por 252 sesiones da el coste anual, y dividida al revés da la **horquilla de equilibrio**: la media horquilla
máxima con la que la regla no perdería dinero si su rentabilidad bruta fuera la medida.

```python
todas = pd.concat([sin_q, rev])
todas["equilibrio_pb"] = (1e4 * todas["bruto_anual"]
                          / (252 * todas["giro_diario"]))
todas["horquilla_media_pb"] = 1e4 * float(np.nanmean(media_horq))
todas[["bruto_anual", "giro_diario", "equilibrio_pb",
       "horquilla_media_pb"]].round(2)
```
```output
                        bruto_anual  giro_diario  equilibrio_pb  horquilla_media_pb
rsi14 <30 a >50               -0.03         0.13          -7.60                3.15
rsi2 <10 a >50                 0.02         0.69           1.23                3.15
bollinger baja a media         0.01         0.28           0.76                3.15
rsi2 >90 a <50 (largo)         0.02         0.79           0.83                3.15
perdedoras 5d                 -0.01         0.82          -0.34                3.15
perdedoras 21d                 0.00         0.41           0.06                3.15
```

La media horquilla del universo es de 3,15 puntos básicos de media. Para no perder, RSI(2) necesitaría una media
horquilla de 1,2 puntos, Bollinger de 0,8 y la regla de RSI(2) larga de 0,8: entre un cuarto y dos quintos de lo que
cuesta de verdad. Las de RSI(14) y perdedoras de 5 días no tienen equilibrio porque pierden en bruto. Es la forma
general del problema: una ventaja diaria de unos pocos puntos básicos, operada con una rotación del 30-80 % diario,
necesita costes de una fracción de punto básico, que solo tienen los creadores de mercado y quien opera con órdenes
limitadas y cobra la horquilla en vez de pagarla (lección 2.3.6).

## Lo que esto cambia en tus backtests

- **Mide el efecto antes que la regla.** Una regresión con errores robustos dice si hay algo y cuánto; una regla con
  umbrales elegidos a la vista del resultado añade grados de libertad.
- **Ejecuta en la barra siguiente.** En reglas de minutos, entrar al precio que define la señal es la forma más común
  de inflar el resultado.
- **Calcula el coste de equilibrio.** Con una ventaja de pocos puntos básicos por operación, la pregunta no es si gana
  en bruto, sino qué coste por lado aguanta.
- **Compara con el universo, no con cero.** Una regla larga en acciones gana si el mercado sube; lo que mide su valor es
  el exceso.
- **Mira qué compra la regla.** Una regla de sobreventa compra acciones en caída libre; unas pocas quiebras pueden
  decidir el resultado.

## Ejercicios

1. Repite la regla de cierre de hueco operando solo los huecos a la baja (comprar) y solo los huecos al alza
   (vender), con huecos de más de 40 puntos básicos. ¿Es simétrico el efecto?

**Solución.**

```python
grandes = d[d["hueco"].abs() > 0.004]
for lado, s in (("a la baja", grandes[grandes["hueco"] < 0]),
                ("al alza", grandes[grandes["hueco"] > 0])):
    pnl = -np.sign(s["hueco"]) * s["r2_6"]
    t = pnl.mean() / pnl.std() * np.sqrt(len(pnl))
    print(f"{lado:>9}: {len(pnl)} operaciones, "
          f"{1e4 * pnl.mean():+.2f} pb, t = {t:+.2f}")
```
```output
a la baja: 169 operaciones, +8.52 pb, t = +2.60
  al alza: 173 operaciones, +6.45 pb, t = +1.93
```

Los dos lados ganan: 8,5 puntos básicos en los huecos a la baja (t = 2,6) y 6,5 en los huecos al alza (t = 1,9). El
generador planta un efecto simétrico (`−0,10 × hueco`), así que la diferencia de 2 puntos entre los dos lados es
ruido: con 170 operaciones por lado, el error típico de cada media es de unos 3 puntos. Elegir solo el lado «que
funciona mejor» sería ajustar la regla al ruido.

2. Cambia la salida de la regla de hueco: en vez de las 10:00, sal al cierre de la tercera barra (09:45) o al de la
   duodécima (10:30). ¿Qué esperas según el efecto plantado y qué sale?

**Solución.**

```python
c3 = b[b["k"] == 2].set_index("dia")["cierre"]
s = d[d["hueco"].abs() > 0.004]
salidas = {"09:45": np.log(c3.reindex(s.index) / s["c1"]),
           "10:00": s["r2_6"],
           "10:30": np.log(s["c12"] / s["c1"])}
for k, r in salidas.items():
    pnl = -np.sign(s["hueco"]) * r
    print(f"salida {k}: {1e4 * pnl.mean():+.2f} pb por operación")
```
```output
salida 09:45: +2.54 pb por operación
salida 10:00: +7.48 pb por operación
salida 10:30: +7.56 pb por operación
```

Saliendo a las 09:45 se recogen solo dos de las cinco barras con efecto, y la ganancia cae a 2,5 puntos básicos por
operación. Saliendo a las 10:30, después de la media hora sin efecto, queda en 7,6, prácticamente igual que a las
10:00 (7,5): las barras de 10:00 a 10:30 no añaden nada esperado, solo varianza y, en una regla real, más tiempo de
exposición. La salida que mejor recoge un efecto es la que coincide con su duración, y aquí se conoce porque está
plantada; con datos reales habría que estimarla fuera de la muestra (lección 2.5.2).

3. Una web publica que «el RSI(2) por debajo de 10 tiene un 70 % de aciertos a 5 días en el S&P 500». Enumera qué
   comprobarías antes de operarlo.

**Solución.**

Primero, contra qué se miden los aciertos: en un índice que sube a largo plazo, cualquier compra a 5 días acierta más
de la mitad de las veces, así que hay que compararlo con el porcentaje de aciertos de una compra cualquiera de 5 días.
Segundo, el tamaño de los aciertos frente al de los fallos: un 70 % de aciertos pequeños con fallos grandes puede
perder dinero. Tercero, la ejecución: si la señal es al cierre, la compra es al cierre siguiente (o a la apertura), no
al mismo cierre. Cuarto, los costes con la rotación real de la regla: en esta lección, una regla de RSI(2) cambia más
de la mitad de la cartera cada día y su coste anual es del orden de su rentabilidad bruta o mayor. Quinto, cuántas
variantes se probaron (umbrales, horizontes, filtros) hasta llegar a la publicada (lección 2.1.8). Y por último, si
el periodo incluye algún tramo sin reversión: en datos reales la reversión de corto plazo existe en la literatura,
pero varía mucho entre épocas y mercados.

[^1]: N. Jegadeesh, «Evidence of Predictable Behavior of Security Returns», *Journal of Finance* 45(3), 1990,
    881-898. DOI 10.1111/j.1540-6261.1990.tb05110.x.
[^2]: B. N. Lehmann, «Fads, Martingales, and Market Efficiency», *Quarterly Journal of Economics* 105(1), 1990;
    versión de trabajo NBER w2533.

## Autoevaluación

1. En KAPA, la regresión del rendimiento de la primera media hora sobre el hueco da −0,132 con un error típico de 0,038. El efecto plantado es −0,10. ¿Qué concluyes?

   - A. Que el generador está mal: debería salir −0,10 exacto
   - B. Que la estimación es compatible con la verdad (está a menos de un error típico) y que con 1.002 sesiones el error de un coeficiente así es de casi el 40 % de su valor
   - C. Que el efecto real es −0,132
   - D. Que el hueco no tiene efecto

   *Respuesta B: Que la estimación es compatible con la verdad (está a menos de un error típico) y que con 1.002 sesiones el error de un coeficiente así es de casi el 40 % de su valor.* Una estimación es la verdad más ruido. La distancia de −0,132 a −0,10 es 0,032, menos de un error típico. Lo que sí dice el error es que, con cuatro años de datos, el coeficiente podría estar entre −0,06 y −0,21: el tamaño del efecto se conoce mal aunque su signo esté claro.

2. La regla vende el hueco al alza al cierre de la primera barra (09:35) y cierra a las 10:00. ¿Por qué no entrar a la apertura de las 09:30?

   - A. Porque a las 09:30 la bolsa está cerrada
   - B. Porque el hueco solo se conoce con el primer precio de la sesión: entrar a ese mismo precio supone que la orden se ejecuta en el instante en que se observa la señal, y en la práctica se ejecuta después
   - C. Porque la primera barra no tiene volumen
   - D. Por las comisiones

   *Respuesta B: Porque el hueco solo se conoce con el primer precio de la sesión: entrar a ese mismo precio supone que la orden se ejecuta en el instante en que se observa la señal, y en la práctica se ejecuta después.* Es la regla de todo el nivel: la decisión con datos de una barra se ejecuta en la siguiente. Se pierde la parte del efecto de la primera barra (un sexto de lo plantado), pero el resultado es alcanzable. Entrar al precio que define la señal es un sesgo de anticipación pequeño y muy habitual.

3. El rendimiento de 10:00 a 10:30 sobre el hueco da un coeficiente de +0,027 con t = 1,67. ¿Qué es?

   - A. Una continuación del hueco que hay que operar
   - B. Ruido: el generador no tiene ningún efecto después de las 10:00; un t de 1,67 aparece por azar con bastante frecuencia, y más si se prueban muchas ventanas
   - C. La prueba de que el cierre del hueco se revierte
   - D. Un error de zona horaria

   *Respuesta B: Ruido: el generador no tiene ningún efecto después de las 10:00; un t de 1,67 aparece por azar con bastante frecuencia, y más si se prueban muchas ventanas.* Es el control y no tiene efecto por construcción. Un t de 1,67 tiene un p-valor bilateral de ≈ 0,10: si se prueban diez ventanas de la sesión, lo esperable es encontrar una así. Por eso los controles se miran con el mismo rigor que la señal (lección 2.1.8).

4. En el universo de acciones, comprar cuando el RSI de 14 sesiones baja de 30 rinde −5,4 % anual frente al universo (t = −3,3). ¿Qué está pasando?

   - A. Que el RSI está mal calculado
   - B. Que en estos datos no hay reversión y sí continuación: las acciones que caen siguen cayendo un poco (momentum, deriva tras resultados y, sobre todo, cuatro quiebras guiadas a la baja durante más de un año)
   - C. Que hay que vender en vez de comprar siempre
   - D. Que el RSI funciona al revés en todos los mercados

   *Respuesta B: Que en estos datos no hay reversión y sí continuación: las acciones que caen siguen cayendo un poco (momentum, deriva tras resultados y, sobre todo, cuatro quiebras guiadas a la baja durante más de un año).* Sin las cuatro quiebras, el resultado baja a −2,5 % y deja de ser significativo (t = −1,3). La regla compraba sistemáticamente acciones que se dirigían a cero. En datos reales hay evidencia de reversión semanal y mensual (Lehmann, Jegadeesh), pero aquí no está plantada: es la lección de no suponer el efecto que se busca.

5. La regla RSI(2) cambia cada día un 62 % de la cartera y la media horquilla del universo es de unos 3 puntos básicos. ¿Cuánto le cuesta al año, aproximadamente?

   - A. Menos del 0,1 %
   - B. Alrededor del 4-5 %: 0,62 × 3 pb × 252 sesiones
   - C. Alrededor del 50 %
   - D. Nada, porque es una cartera larga

   *Respuesta B: Alrededor del 4-5 %: 0,62 × 3 pb × 252 sesiones.* Rotación diaria por coste por unidad por sesiones: 0,62 × 0,0003 × 252 ≈ 4,7 %. Es más de ocho veces su rentabilidad bruta (+0,5 %). En reglas de pocos días, el coste no es un detalle del backtest: es el resultado.

## Ideas clave

- La reversión a la media de corto plazo es la familia más intuitiva y la más sensible a los costes: gana poco por operación y opera mucho.
- El cierre de hueco plantado en KAPA se recupera con una regresión con errores robustos (−0,132 frente a −0,10, t = −3,5), con un error típico de 0,038 que deja el tamaño del efecto mal medido.
- La regla que entra a las 09:35 y sale a las 10:00 gana unos 4 puntos básicos por operación en bruto; con 2 puntos básicos por lado ya no gana nada si se operan todos los huecos.
- El resto de la sesión no tiene efecto, pero da un t de 1,67 en una ventana: los controles también producen casi falsos positivos.
- RSI y Bollinger en acciones sin reversión plantada no ganan: lo que miden es la continuación heredada de las quiebras y del momentum, y los costes de su rotación diaria se llevan entre el 0,8 % y el 7,1 % al año.

## Recursos

### Research

- [Estacionalidad en Python II - Backtesting](https://pythonparatrading.com/estacionalidad-intradia-backtesting/) · Research-0025 · 8 min

### Documentación

- [Jegadeesh (1990): «Evidence of Predictable Behavior of Security Returns», Journal of Finance 45(3), 881-898](https://doi.org/10.1111/j.1540-6261.1990.tb05110.x) · doi.org
- [Lehmann: «Fads, Martingales, and Market Efficiency» (NBER w2533; Quarterly Journal of Economics 105(1), 1990)](https://www.nber.org/papers/w2533) · nber.org
- [TA-Lib para Python: indicadores de momento (RSI) y de solapamiento (BBANDS)](https://ta-lib.github.io/ta-lib-python/func_groups/momentum_indicators.html) · ta-lib.github.io

### Libros

- [Ernest P. Chan: Algorithmic Trading: Winning Strategies and Their Rationale (Wiley, 2013), capítulo 4, «Buy-on-Gap Model»](https://www.wiley.com/en-us/Algorithmic+Trading%3A+Winning+Strategies+and+Their+Rationale-p-9781118460146) · wiley.com

### Datos

- [Datos del nivel 2: barras de 5 minutos de KAPA, 2022-2025 (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/indice2_5min.parquet) · Descarga · indice2_5min.parquet

Lección anterior: [2.3.1 · Seguimiento de tendencia y momentum](https://pythonparatrading.com/curso-python-trading/nivel-2/seguimiento-de-tendencia-y-momentum/)

Lección siguiente: [2.3.3 · Pairs trading y arbitraje estadístico: distancia, cointegración y Kalman](https://pythonparatrading.com/curso-python-trading/nivel-2/pairs-trading-y-arbitraje-estadistico/)
