---
title: "2.1.5 Cointegración: Engle-Granger, Johansen y vida media"
description: "Correlación no es cointegración: Engle-Granger y Johansen sobre pares sintéticos con respuesta conocida, vida media de Ornstein-Uhlenbeck y una ruptura."
url: "https://pythonparatrading.com/curso-python-trading/nivel-2/cointegracion-engle-granger-johansen-y-half-life/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-2/cointegracion-engle-granger-johansen-y-half-life.png"
---

# 2.1.5 · Cointegración: Engle-Granger, Johansen y vida media de la reversión

> Dos precios pueden moverse juntos día a día y separarse sin límite, o moverse con poca correlación y volver siempre a la misma distancia. Esta lección separa las dos cosas con ocho pares sintéticos cuya respuesta se conoce: cuáles están cointegrados, con qué vida media y cuándo se rompe la relación.

- Curso: [Nivel 2: Estrategias sistemáticas y backtesting riguroso](https://pythonparatrading.com/curso-python-trading/nivel-2/)
- Módulo: 2.1 · Estadística y econometría para trading
- Duración: 70 min
- Requisitos: [0.7.2 · Probabilidad y distribuciones, correlación frente a causalidad y regresión simple](https://pythonparatrading.com/curso-python-trading/nivel-0/probabilidad-distribuciones-y-regresion/), [1.6.1 · Rentabilidades, volatilidad, correlaciones móviles y drawdown](https://pythonparatrading.com/curso-python-trading/nivel-1/volatilidad-correlaciones-y-drawdown/)

## Al terminar esta lección

- Distinguir correlación de rendimientos y cointegración de precios, y explicar por qué la primera no implica la segunda.
- Aplicar el contraste de Engle-Granger en dos pasos y el de Johansen con `statsmodels`, y leer sus estadísticos y valores críticos.
- Estimar la vida media de la reversión de un spread como un proceso de Ornstein-Uhlenbeck y medir el sesgo del estimador.
- Reconocer los errores típicos: niveles frente a logaritmos, cribas de muchos pares, β estáticos que aprueban y tramos cortos tras una ruptura.

## Correlación no es cointegración

Un borracho que sale del bar camina sin rumbo: cada paso es independiente del anterior y, con el tiempo, puede acabar
en cualquier parte. Si lleva un perro suelto, el perro también vaga. Pero si el perro va atado con una correa larga,
los dos vagan sin rumbo y, aun así, nunca se separan más que la correa. Murray (1994) usó esta imagen para explicar la
**cointegración**: dos series que por separado son paseos aleatorios pero cuya distancia es estacionaria.

En trading, los dos caminantes son dos precios. La pregunta que importa para un par (lección 2.3.3) no es si suben y
bajan a la vez, sino si la distancia entre ellos vuelve a un nivel. Son preguntas distintas:

- La **correlación de rendimientos** mide si los dos precios se mueven en la misma dirección el mismo día. Es una
  propiedad del corto plazo. Dos paseos aleatorios con un factor común tienen una correlación alta y pueden separarse
  sin límite.
- La **cointegración** dice que existe un $\beta$ tal que el spread $\ln y_t - \beta \ln x_t$ es estacionario: vuelve
  a su media con una velocidad finita. Es una propiedad del largo plazo, y no exige que los rendimientos diarios estén muy
  correlados.

Formalmente: dos series integradas de orden 1 (sus diferencias son estacionarias, lección 2.1.2) están cointegradas
cuando existe un vector $(1, -\beta)$ cuya combinación es estacionaria. Con precios se suele trabajar con sus
logaritmos, porque lo que vuelve es un cociente y el spread queda en porcentaje:

$$
\ln y_t = \alpha + \beta \ln x_t + u_t, \qquad u_t \sim I(0).
$$

```mermaid
flowchart TB
  P["Precios I(1)"] --> R{Correlados}
  P --> S{"Spread I(0)"}
  R -- sí --> C1[Cubrir a corto]
  R -- no --> N1[Poca relación]
  S -- sí --> C2[Par que revierte]
  S -- no --> N2[Se separan]
```

Las dos ramas son independientes: un par puede estar en cualquiera de las cuatro combinaciones. La figura de la
cabecera muestra la diferencia con datos: arriba, el spread del par B (cointegrado) cruza el cero sin parar; abajo,
el residuo del control E (más correlado que B, no cointegrado) pasa años enteros a un lado del cero.

## Los ocho pares del curso

Los datos de esta lección son ocho pares sintéticos (`pares2.parquet`), sesiones de la NYSE de 2014 a 2025, con la
**respuesta conocida** en `pares2_verdad.csv`. Todas las relaciones se plantaron entre logaritmos. Tres pares están
cointegrados con vidas medias distintas (A, B y C); D tiene un β que cambia con el tiempo (lección 2.1.6); E, F y G
son controles: sus rendimientos están muy correlados, pero la relación entre sus logaritmos lleva un paseo aleatorio,
así que no están cointegrados; H está cointegrado hasta el 15 de junio de 2021 y desde ese día deja de estarlo. Son datos inventados con el efecto plantado: sirven para
comprobar si un método recupera lo que se sabe que hay, no como evidencia de que un par real funcione.

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

pares = pd.read_parquet("pares2.parquet")
verdad = pd.read_csv("pares2_verdad.csv", index_col="par")
precios = pares.pivot(index="fecha", columns="activo",
                      values="precio")
lp = np.log(precios)      # las relaciones son entre logaritmos
print(precios.shape, precios.index[0].date(),
      precios.index[-1].date())
verdad[["tipo", "formulacion", "beta", "vida_media_sesiones",
        "sigma_spread"]]
```
```output
(3018, 16) 2014-01-02 2025-12-31
                         tipo formulacion  beta  vida_media_sesiones  sigma_spread
par
A                 cointegrado         log   1.5                  5.0         0.020
B                 cointegrado         log   0.8                 15.0         0.030
C                 cointegrado         log   1.1                 40.0         0.045
D               beta_variable         log   NaN                 10.0         0.025
E    correlado_no_cointegrado         log   1.0                  NaN           NaN
F    correlado_no_cointegrado         log   0.9                  NaN           NaN
G    correlado_no_cointegrado         log   1.2                  NaN           NaN
H                     ruptura         log   0.9                 10.0         0.025
```

Cada par tiene un activo `x` y un activo `y` (`PZAX` y `PZAY` para A). La columna `formulacion` dice que la
relación es entre logaritmos (`log`), y `sigma_spread` es la desviación típica del spread en logaritmos: 0,02 es un
2 % de distancia típica entre los dos precios. Lo primero es medir lo que suele medir un analista con prisa, la
correlación, junto al contraste de Engle-Granger que se explica en la sección siguiente:

```python
from statsmodels.tsa.stattools import coint

filas = []
for par, v in verdad.iterrows():
    y, x = lp[v.activo_y], lp[v.activo_x]
    t, p, _ = coint(y, x)
    filas.append({"par": par, "tipo": v.tipo,
                  "corr_rend": y.diff().corr(x.diff()),
                  "corr_logprecios": y.corr(x),
                  "t_eg": t, "p_eg": p})
tabla = pd.DataFrame(filas).set_index("par")
tabla.round(3)
```
```output
                         tipo  corr_rend  corr_logprecios    t_eg   p_eg
par
A                 cointegrado      0.912            1.000 -14.971  0.000
B                 cointegrado      0.792            0.990  -8.979  0.000
C                 cointegrado      0.875            0.992  -5.586  0.000
D               beta_variable      0.871            0.994  -3.472  0.035
E    correlado_no_cointegrado      0.859            0.978  -2.599  0.237
F    correlado_no_cointegrado      0.856            0.976  -2.036  0.510
G    correlado_no_cointegrado      0.856            0.931  -2.371  0.339
H                     ruptura      0.635            0.941  -2.647  0.219
```

*Fig. — Correlación de los rendimientos diarios frente al estadístico de Engle-Granger en los ocho pares: la correlación no ordena la cointegración.* Dispersión de ocho pares. Eje horizontal, correlación de rendimientos de 0,6 a 0,95; vertical, estadístico t de Engle-Granger de −16 a 0, con el valor crítico del 5 % en −3,34. Por debajo, cointegrados: A (0,91; −15,0), B, destacado (0,79; −9,0), C (0,88; −5,6) y, por poco, D (0,87; −3,5). Por encima, los controles E, F y G, con 0,86 de correlación (−2,6, −2,0 y −2,4), y H (0,64; −2,6), cuya ruptura hunde también la correlación.

Los tres controles tienen una correlación de rendimientos de 0,86, más que el par B (0,79), y la correlación de sus
logaritmos pasa de 0,93. Si el criterio fuera la correlación, E sería mejor candidato que B. El contraste dice lo
contrario: B rechaza la hipótesis de «no cointegración» con holgura (t = −8,98, muy por debajo del valor crítico de
−3,34 al 5 %) y E, F y G no la rechazan (p entre 0,24 y 0,51). La correlación de los precios es todavía peor guía:
dos series con tendencia tienen correlaciones de niveles cercanas a 1 aunque no tengan nada que ver (regresión
espuria).

D y H son los casos con trampa. H no rechaza en la muestra completa (p = 0,22): la relación existió y se rompió, y el
salto de la ruptura hunde incluso su correlación de rendimientos (0,64). D sí rechaza (p = 0,035), y es un aprobado
engañoso: su β verdadero se mueve y la regresión de toda la muestra estima uno medio; el residuo de ese β fijo revierte,
pero mucho más despacio que el spread de verdad (se ve con la vida media, más abajo). Los dos se tratan en esta
lección y en la 2.1.6.

## Engle-Granger en dos pasos

El contraste de Engle y Granger (1987) es la forma más directa de comprobar la definición:

1. Estimar por mínimos cuadrados $\ln y_t = \alpha + \beta \ln x_t + u_t$.
2. Contrastar si el residuo $\hat u_t$ tiene raíz unitaria con una regresión de Dickey-Fuller aumentada,
   $\Delta \hat u_t = \gamma \hat u_{t-1} + \sum_{i=1}^{k} c_i \Delta \hat u_{t-i} + e_t$. La hipótesis nula es
   $\gamma = 0$: el residuo es un paseo aleatorio y **no** hay cointegración.

```mermaid
flowchart TB
  A["ln y, ln x"] --> B[MCO: y sobre x]
  B --> C[Residuo u]
  C --> D[ADF de u]
  D --> E{t bajo el crítico}
  E -- sí --> F[Cointegrados]
  E -- no --> G[Sin evidencia]
```

El detalle que se olvida es el valor crítico. En el primer paso, MCO elige el β que hace mínima la varianza del
residuo, es decir, el que más estacionario lo hace parecer. Por eso el ADF del residuo no puede compararse con las
tablas del ADF de una serie observada: hacen falta valores críticos más negativos, los de MacKinnon para residuos de
cointegración, que son los que usa `coint`. El paso a mano lo enseña:

```python
import statsmodels.api as sm
from statsmodels.tsa.stattools import adfuller

def dos_pasos(y, x):
    mco = sm.OLS(y, sm.add_constant(x)).fit()
    adf = adfuller(mco.resid, result_object=True)
    eg = coint(y, x)
    return {"alfa": mco.params.iloc[0], "beta": mco.params.iloc[1],
            "t": adf.statistic, "p_adf": adf.pvalue,
            "crit5_adf": adf.critical_values["5%"],
            "p_coint": eg[1], "crit5_coint": eg[2][1]}

pd.DataFrame({par: dos_pasos(lp[f"PZ{par}Y"], lp[f"PZ{par}X"])
              for par in ["B", "E"]}).round(3)
```
```output
                 B      E
alfa         0.683  0.913
beta         0.805  0.821
t           -8.978 -2.598
p_adf        0.000  0.093
crit5_adf   -2.862 -2.862
p_coint      0.000  0.237
crit5_coint -3.338 -3.338
```

En B, MCO recupera la relación plantada ($\alpha$ = 0,7 y $\beta$ = 0,8; salen 0,68 y 0,805) y el residuo es
claramente estacionario con cualquier tabla. En E el estadístico es el mismo en las dos columnas (−2,60), pero el ADF
directo da p = 0,09, un rechazo al 10 %, y `coint`, p = 0,24: el valor crítico al 5 % pasa de −2,86 a −3,34. Con una criba de muchos
pares, usar la tabla equivocada multiplica los falsos positivos.

Engle-Granger tiene dos debilidades conocidas. La primera, que no es simétrico: regresar `y` sobre `x` o `x` sobre `y`
da estadísticos y p-valores distintos, y en un caso dudoso cada dirección puede llevar a una conclusión diferente. La
segunda, que con más de dos series solo
encuentra una relación, aunque haya varias. Johansen resuelve las dos.

## Johansen: cuántas relaciones hay entre varias series

Johansen (1991) parte de un modelo vectorial de corrección del error (VECM) para un vector $Y_t$ de $n$ series, aquí
los logaritmos de los precios:

$$
\Delta Y_t = \Pi Y_{t-1} + \sum_{i=1}^{k} \Gamma_i \Delta Y_{t-i} + \varepsilon_t .
$$

Si las series no están cointegradas, $\Pi = 0$ y el modelo es un VAR en diferencias. Si hay $r$ relaciones de
cointegración, $\Pi = \alpha \beta'$ tiene rango $r$: las $r$ columnas de $\beta$ son las combinaciones estacionarias
y $\alpha$ dice con qué fuerza vuelve cada serie al equilibrio. El contraste de la traza pregunta en secuencia
«¿rango ≤ 0?», «¿rango ≤ 1?»… y el rango estimado es el primero que no se rechaza.

Con las cuatro series de A y B, la respuesta conocida es $r = 2$:

```python
import warnings
from statsmodels.tsa.vector_ar.vecm import coint_johansen

# Con numpy 2.5, los autovectores salen como complejos de parte
# imaginaria nula y statsmodels avisa (ComplexWarning) al pasarlos
# a reales: el aviso no cambia ningún número
warnings.filterwarnings("ignore",
                        category=np.exceptions.ComplexWarning)

def johansen(datos, k_ar_diff=1):
    j = coint_johansen(datos, det_order=0, k_ar_diff=k_ar_diff)
    n = datos.shape[1]
    tabla = pd.DataFrame(
        {"traza": j.lr1, "crit95_traza": j.cvt[:, 1],
         "max_autov": j.lr2, "crit95_max": j.cvm[:, 1]},
        index=[f"rango <= {r}" for r in range(n)])
    rango = int((j.lr1 > j.cvt[:, 1]).cumprod().sum())
    return tabla, rango, np.real(j.evec)

cuatro = lp[["PZAY", "PZAX", "PZBY", "PZBX"]]
tabla_j, rango, vectores = johansen(cuatro)
print("Rango estimado:", rango)
tabla_j.round(1)
```
```output
Rango estimado: 2
            traza  crit95_traza  max_autov  crit95_max
rango <= 0  296.2          47.9      209.6        27.6
rango <= 1   86.6          29.8       80.0        21.1
rango <= 2    6.6          15.5        5.6        14.3
rango <= 3    0.9           3.8        0.9         3.8
```

La traza rechaza «rango ≤ 0» (296,2 frente a 47,9) y «rango ≤ 1» (86,6 frente a 29,8), y no rechaza «rango ≤ 2»
(6,6 frente a 15,5): dos relaciones, las plantadas. Los vectores de cointegración no son únicos (cualquier
combinación de dos vectores estacionarios también lo es), pero normalizados se reconocen:

```python
v1 = vectores[:, 0] / vectores[0, 0]   # normalizado por PZAY
v2 = vectores[:, 1] / vectores[2, 1]   # normalizado por PZBY
pd.DataFrame({"vector_1": v1, "vector_2": v2},
             index=cuatro.columns).round(3)
```
```output
        vector_1  vector_2
activo
PZAY       1.000    -0.040
PZAX      -1.501     0.065
PZBY       0.070     1.000
PZBX      -0.059    -0.812
```

El primer vector es casi exactamente la relación de A, $\ln y - 1{,}5 \ln x$ (−1,501), con pesos pequeños en B; el
segundo, casi la de B, $\ln y - 0{,}8 \ln x$ (−0,812), con pesos pequeños en A. Aquí salen limpios porque las dos relaciones tienen fuerzas de
reversión muy distintas, pero no está garantizado: cualquier combinación de dos vectores estacionarios también lo es,
y lo que Johansen identifica de forma única es el número de relaciones y el espacio que generan, no una base
concreta.

Si se añade a un par cointegrado una serie que no tiene nada que ver con él, Johansen debería encontrar una sola
relación y dar peso nulo a la serie ajena. Y con series que no están cointegradas entre sí debería dar rango 0, salvo
por azar (al 5 %, en uno de cada veinte casos). Esta última comprobación depende de la formulación:

```python
from itertools import combinations

tabla_3, rango_3, vec_3 = johansen(lp[["PZAY", "PZAX", "PZEX"]])
print("A + PZEX: rango", rango_3, "vector",
      (vec_3[:, 0] / vec_3[0, 0]).round(3))

controles = ["PZEY", "PZEX", "PZFY", "PZFX", "PZGY", "PZGX"]
trios = list(combinations(controles, 3))
falsos = {"niveles": 0, "logaritmos": 0}
for trio in trios:
    datos = precios[list(trio)]
    falsos["niveles"] += johansen(datos)[1] > 0
    falsos["logaritmos"] += johansen(lp[list(trio)])[1] > 0
print(len(trios), "tríos de controles; con rango > 0:", falsos)
crec = precios[controles].iloc[-1] / precios[controles].iloc[0]
print("Precio final / inicial:", crec.round(1).to_dict())
```
```output
A + PZEX: rango 1 vector [ 1.    -1.51   0.008]
20 tríos de controles; con rango > 0: {'niveles': 4, 'logaritmos': 0}
Precio final / inicial: {'PZEY': 2.4, 'PZEX': 3.1, 'PZFY': 3.3, 'PZFX': 3.4, 'PZGY': 5.1, 'PZGX': 3.2}
```

Con la serie ajena, rango 1 y peso casi nulo (0,008) para `PZEX`: correcto. Con los controles, en logaritmos ningún
trío de los 20 da rango mayor que cero, lo esperable (el azar daría uno); en niveles, 4. La razón está en la última
línea: los controles se multiplican por entre 2,4 y 5,1 en doce años. El contraste supone errores de varianza
constante, y en niveles la variación diaria de un precio es proporcional a su nivel: un precio que se ha multiplicado
por 5 tiene variaciones cinco veces mayores al final que al principio, y la distribución de la traza deja de ser la de
las tablas. En logaritmos la variación diaria es un porcentaje y su tamaño no depende del nivel.

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

Niveles o logaritmos no es un detalle estético. Una relación en niveles ($y = \alpha + \beta x + u$) dice que la
distancia en dinero vuelve; una en logaritmos, que el cociente vuelve. Con precios que crecen de forma multiplicativa,
la formulación equivocada falla en los dos sentidos: en niveles, Johansen da falsos positivos en los controles, y
Engle-Granger deja de ver la cointegración del par A (ejercicio 1). Decide la formulación antes de mirar el p-valor,
no después.

</aside>

## Vida media: el spread como proceso de Ornstein-Uhlenbeck

Saber que un spread revierte no basta para operarlo: hay que saber a qué velocidad. El modelo estándar es el proceso
de **Ornstein-Uhlenbeck**, un paseo aleatorio con una fuerza que tira hacia la media:

$$
dS_t = \theta (\mu - S_t)\,dt + \sigma\, dW_t .
$$

$\theta$ es la velocidad de reversión: la distancia esperada a la media cae como $e^{-\theta t}$. Muestreado una vez
por sesión, es un AR(1), $S_t = c + \varphi S_{t-1} + \varepsilon_t$ con $\varphi = e^{-\theta}$, y la **vida media**
(el tiempo en que la distancia esperada a la media se reduce a la mitad) es

$$
h = \frac{\ln 2}{\theta} = -\frac{\ln 2}{\ln \varphi} .
$$

Se estima con la regresión de la variación del spread sobre su nivel anterior, $\Delta S_t = c + b\, S_{t-1} + e_t$,
con $\varphi = 1 + b$. En los pares cointegrados se aplica al residuo de Engle-Granger:

```python
def vida_media(spread):
    """Vida media en sesiones del AR(1) dS_t = c + b * S_(t-1)."""
    s = pd.Series(spread).dropna()
    X = sm.add_constant(s.shift(1).iloc[1:])
    b = sm.OLS(s.diff().iloc[1:], X).fit().params.iloc[1]
    return -np.log(2) / np.log(1 + b)

def residuo_eg(par):
    y, x = lp[f"PZ{par}Y"], lp[f"PZ{par}X"]
    return sm.OLS(y, sm.add_constant(x)).fit().resid

vidas = pd.DataFrame({
    "estimada": {p: vida_media(residuo_eg(p)) for p in "ABCDEFG"},
    "verdadera": verdad["vida_media_sesiones"]}).dropna(
        subset=["estimada"])
vidas.round(1)
```
```output
   estimada  verdadera
A       4.7        5.0
B      13.0       15.0
C      35.4       40.0
D      38.4       10.0
E     162.1        NaN
F     259.4        NaN
G     270.0        NaN
```

En A, B y C la estimación queda cerca de la verdad pero siempre por debajo: 4,7 frente a 5, 13,0 frente a 15 y 35,4
frente a 40. En E, F y G la función devuelve vidas medias de 162 a 270 sesiones, aunque no hay ninguna: un AR(1)
siempre da un $\varphi$ menor que 1 en una muestra finita, así que la vida media de un paseo aleatorio sale finita.
**Una vida media solo tiene sentido después de un contraste de cointegración que la respalde.** Y D enseña lo
contrario: el contraste aprobó, pero el residuo de su β fijo tiene una vida media de 38,4 sesiones cuando la del spread
verdadero es 10. Lo que revierte en D es otra cosa que lo que se estimó: con un β que se mueve, el residuo de un β
medio mezcla el spread con la deriva lenta del β (lección 2.1.6).

¿Es mala suerte o sesgo? Se puede responder simulando: generar muchos procesos de Ornstein-Uhlenbeck con la vida media
verdadera y la longitud de estos datos, y estimar cada uno igual.

```python
from scipy.signal import lfilter

rng = np.random.default_rng(2028)
n, caminos = len(lp), 400
filas = {}
for h in [5, 15, 40]:
    phi = 0.5 ** (1 / h)
    e = rng.standard_normal((caminos, n)) * np.sqrt(1 - phi**2)
    s = lfilter([1.0], [1.0, -phi], e, axis=1)
    ds, s0 = np.diff(s, axis=1), s[:, :-1]
    s0c = s0 - s0.mean(axis=1, keepdims=True)
    b = (ds * s0c).sum(axis=1) / (s0c**2).sum(axis=1)
    est = -np.log(2) / np.log(1 + b)
    filas[h] = np.percentile(est, [5, 50, 95])
pd.DataFrame(filas, index=["p5", "mediana", "p95"]).T.round(1)
```
```output
      p5  mediana   p95
5    4.4      4.9   5.6
15  11.8     14.7  17.8
40  27.3     37.2  51.2
```

*Fig. — Vida media de los pares cointegrados: estimada en los datos (rombo), verdadera (raya) y banda del 90 % de 400 simulaciones de la misma longitud.* Tres filas en escala logarítmica de 3 a 70 sesiones. A: verdad 5, estimada 4,7, banda de 4,4 a 5,6. B: verdad 15, estimada 13,0, banda de 11,8 a 17,8. C, destacada: verdad 40, estimada 35,4, banda de 27,3 a 51,2. Las tres estimaciones quedan por debajo de la verdad y dentro de su banda, que se ensancha cuanto más lenta es la reversión.

La mediana de las simulaciones queda por debajo de la verdad, y más cuanto más lenta es la reversión: es el sesgo del
coeficiente autorregresivo en muestras finitas, que empuja $\hat\varphi$ hacia abajo. Las tres estimaciones de los
datos caen dentro de la banda del 90 %, así que son lo esperable. La banda es lo que más importa: con doce años de
datos, la vida media de 40 sesiones podría haberse estimado en 27 o en 51. Un par con vida media de 40 sesiones tiene
apenas 75 vidas medias en la muestra, pocas observaciones independientes de la reversión.

La vida media sirve después para dos decisiones del sistema de la lección 2.3.3: cuánto tiempo mantener una posición
antes de darla por fallida y qué ventana usar para estimar media y desviación del spread. Una vida media mal estimada
estropea las dos.

## Cuando la cointegración se rompe: el par H

El par H está cointegrado (vida media 10, spread de desviación 0,025 en logaritmos) hasta el 15 de junio de 2021.
Ese día el spread salta cinco desviaciones típicas y desde entonces es un paseo aleatorio. Es la situación real más peligrosa: una relación que funcionó durante
años, que cualquier contraste confirma con los datos del pasado, y que deja de existir.

```python
y, x = lp["PZHY"], lp["PZHX"]
antes = lp.index < "2021-06-15"
for nombre, m in [("muestra completa", slice(None)),
                  ("antes de la ruptura", antes),
                  ("después de la ruptura", ~antes)]:
    t, p, _ = coint(y[m], x[m])
    print(f"{nombre:22} sesiones {len(y[m]):5}  t {t:6.2f}  "
          f"p {p:.4f}")
```
```output
muestra completa       sesiones  3018  t  -2.65  p 0.2191
antes de la ruptura    sesiones  1875  t  -5.91  p 0.0000
después de la ruptura  sesiones  1143  t  -3.66  p 0.0205
```

*Fig. — Spread del par H con el β estimado antes de la ruptura, 2014–2025: cointegrado hasta el 15 de junio de 2021, paseo aleatorio desde entonces.* Línea de 2014 a 2025. Hasta junio de 2021 el spread oscila alrededor de cero con una amplitud de pocos puntos porcentuales (Engle-Granger, p < 0,0001). La ruptura del 15 de junio de 2021, destacada, lo hace saltar y desde ahí vaga sin volver, entre 12 y 47 %; aun así, Engle-Granger sobre ese tramo da p = 0,021, un falso positivo.

Antes de la ruptura la cointegración es inequívoca (t = −5,91; p < 0,0001). La muestra completa ya no la detecta
(p = 0,22). Y el tramo posterior, que por construcción es un paseo aleatorio, da t = −3,66 y p = 0,021: un **falso
positivo**. Un analista que
mirase solo los cuatro últimos años concluiría que la relación sigue viva. Un contraste al 5 % rechaza una de cada
veinte veces cuando no hay nada; si se mira un solo tramo, el azar puede tocar.

Una forma de ver el problema completo es contrastar en ventanas móviles de un año, como haría un sistema que reestima
la relación cada mes:

```python
def ventanas(y, x, n=252, paso=21):
    return np.array([coint(y.iloc[i:i + n], x.iloc[i:i + n],
                           maxlag=10)[1]
                     for i in range(0, len(y) - n + 1, paso)])

casos = {"A (vida media 5)": "A", "C (vida media 40)": "C",
         "E (control)": "E", "F (control)": "F",
         "G (control)": "G"}
filas = {}
for nombre, par in casos.items():
    p = ventanas(lp[f"PZ{par}Y"], lp[f"PZ{par}X"])
    filas[nombre] = (len(p), (p < 0.05).mean())
p = ventanas(y[~antes], x[~antes])
filas["H tras la ruptura"] = (len(p), (p < 0.05).mean())
pd.DataFrame(filas, index=["ventanas", "rechazo_5pct"]).T.round(3)
```
```output
                   ventanas  rechazo_5pct
A (vida media 5)      132.0         0.977
C (vida media 40)     132.0         0.091
E (control)           132.0         0.038
F (control)           132.0         0.098
G (control)           132.0         0.061
H tras la ruptura      43.0         0.070
```

Tres lecturas. En A, con una vida media de 5 sesiones, un año de datos basta y el contraste rechaza en el 98 % de
las ventanas. En C, cointegrado de verdad pero con vida media de 40, un año contiene apenas seis vidas medias y el
contraste rechaza en el 9 %, casi como si no hubiera nada: es un problema de **potencia**, no de ausencia de relación.
En los controles la tasa de rechazo va del 3,8 % al 9,8 %, alrededor del 5 % que se espera del azar (las ventanas se
solapan, así que esas tasas tienen mucho ruido), y en H tras la ruptura es del 7 % (3 de 43 ventanas). La ventana
corta no distingue
bien un par lento de uno inexistente; la larga mezcla tramos con y sin relación. No hay una ventana correcta: hay que
saber qué se pierde con cada una. La detección de la ruptura en sí es materia de la lección 2.1.7.

## Cribar pares: el sesgo de selección

En la práctica nadie contrasta un par: se criban cientos y se quedan los que pasan. Con los dieciséis activos de los
datos hay 120 parejas posibles, y solo las de A, B y C (y H en parte de la muestra) tienen relación. Las 112 parejas
de activos de pares distintos no están cointegradas por construcción:

```python
filas = []
for a, b in combinations(precios.columns, 2):
    filas.append({
        "mismo_par": a[:3] == b[:3],
        "p_logs": coint(lp[a], lp[b], maxlag=10)[1],
        "p_niveles": coint(precios[a], precios[b], maxlag=10)[1]})
criba = pd.DataFrame(filas)
ajenas = criba[~criba["mismo_par"]]
print("Parejas de pares distintos:", len(ajenas))
(ajenas[["p_logs", "p_niveles"]] < 0.05).sum()
```
```output
Parejas de pares distintos: 112
p_logs       4
p_niveles    2
dtype: int64
```

En logaritmos, 4 de las 112 parejas sin relación pasan el contraste al 5 %, y en niveles, 2: los dos números están
cerca de los 5,6 que daría el azar, y entre ellos no hay ninguna lección sobre la formulación (las parejas comparten un
factor de mercado y no son contrastes independientes, así que el número de rechazos varía mucho de una muestra a
otra). Lo que importa es que existen: cualquiera de esas cuatro parejas, presentada sola, parecería un hallazgo. El problema de elegir lo mejor de muchas pruebas tiene nombre y
correcciones (lección 2.1.8); la defensa más sencilla es exigir una razón económica antes de mirar el p-valor y
validar fuera de la muestra en la que se eligió el par (lección 2.5.2).

## Errores típicos con la cointegración

- **Estimar β con toda la muestra y operar en ella.** El β y la media del spread de un backtest tienen que estimarse
  con datos anteriores a cada operación; con toda la muestra, el spread revierte «por construcción» (lección 2.5.1).
- **Cribar sin corregir.** De 112 parejas sin relación, 4 pasan en logaritmos. Con cientos de candidatos, los «pares»
  que pasan son en buena parte azar.
- **Confundir correlación y cointegración.** Los controles E, F y G tienen más correlación que B y no revierten.
- **Fiarse de un aprobado con β fijo.** D pasa Engle-Granger con un β medio, pero el residuo que revierte tiene una vida
  media cuatro veces mayor que el spread real.
- **Elegir mal la formulación.** En niveles, Johansen da falsos positivos y Engle-Granger pierde el par A.
- **Ignorar la potencia.** Un par lento parece no cointegrado en ventanas cortas; un par rápido parece cointegrado en
  casi cualquier ventana.
- **Suponer que dura.** La relación de H funcionó siete años y se rompió en un día. El seguimiento del spread y de la
  estabilidad del β (lecciones 2.1.6 y 2.1.7) es parte del sistema, no un extra.
- **Precios sin ajustar.** Un split o un dividendo sin ajustar mete un salto en uno de los dos precios y rompe una
  cointegración que existe (lección 1.4.3).

## Ejercicios

1. Repite el contraste de Engle-Granger de los pares A, B, C y de los controles E, F y G con los precios en niveles,
   en lugar de en logaritmos. ¿Cambia alguna conclusión?

**Solución.**

```python
for par in "ABCEFG":
    t, p, _ = coint(precios[f"PZ{par}Y"], precios[f"PZ{par}X"])
    print(par, f"t {t:6.2f}  p {p:.3f}")
```
```output
A t  -3.30  p 0.055
B t  -8.56  p 0.000
C t  -5.44  p 0.000
E t  -2.32  p 0.366
F t  -1.85  p 0.607
G t  -2.04  p 0.508
```

Los controles siguen sin rechazar, B y C siguen rechazando, pero A deja de hacerlo: en niveles su estadístico pasa
de −14,97 a −3,30 y el p-valor a 0,055. A es el par cuyo `y` más ha crecido (se multiplica por 5,5 en doce años), y una
relación entre logaritmos con β = 1,5 no es lineal en niveles: la distancia en dinero entre los dos precios crece con
el nivel y el residuo de la regresión en niveles se parece a un paseo aleatorio. Es el otro lado del error de
formulación: no solo falsos positivos, también relaciones reales que no se ven.

2. Estima la vida media del par B con los tres primeros años (2014-2016) y con los tres últimos (2023-2025), usando
   en cada tramo su propio β. ¿Cuánto varía? ¿Cae dentro de la banda simulada para una vida media de 15?

**Solución.**

```python
yb, xb = lp["PZBY"], lp["PZBX"]
for desde, hasta in [("2014", "2016"), ("2023", "2025")]:
    ys, xs = yb.loc[desde:hasta], xb.loc[desde:hasta]
    u = sm.OLS(ys, sm.add_constant(xs)).fit().resid
    print(desde, hasta, f"sesiones {len(u)}",
          f"vida media {vida_media(u):.1f}")
```
```output
2014 2016 sesiones 756 vida media 9.7
2023 2025 sesiones 752 vida media 13.5
```

Con tres años, unas 750 sesiones, la vida media sale en 9,7 y en 13,5 sesiones, las dos por debajo de 15 y la primera
fuera de la banda simulada para 3.018 sesiones (de 11,8 a 17,8): con la cuarta parte de datos la banda se ensancha y
el sesgo hacia abajo crece. Además, cada tramo estima su propio β, y un β estimado en poca muestra absorbe parte del
spread y lo hace parecer más rápido. Una vida media es una
estimación con un error grande: úsala como orden de magnitud.

3. Un compañero criba 300 parejas de acciones del mismo sector en niveles, encuentra 24 con p < 0,05 en Engle-Granger y
   propone operarlas todas. Con lo visto en esta lección, ¿qué le dirías?

**Solución.**

Que con 300 contrastes al 5 % el azar solo ya produce unos 15 rechazos (en los datos del curso, 4 de 112 parejas sin
relación), y que en niveles el contraste ni siquiera mide lo que debe: con precios que crecen de forma multiplicativa,
da falsos positivos y pierde relaciones reales. Las 24 pueden ser casi todas falsas. Antes de operar: fijar la
formulación (logaritmos), exigir una razón económica para cada par, corregir por pruebas múltiples (lección 2.1.8),
estimar la vida media y comprobar que es compatible con el horizonte del sistema, y validar fuera de la muestra en
la que se cribó, vigilando la estabilidad de la relación como en el par H.

## Autoevaluación

1. Los rendimientos diarios del par E tienen una correlación de 0,86 y los del par B, de 0,79. ¿Qué se puede concluir sobre su cointegración?

   - A. Que E está más cointegrado que B
   - B. Nada: la correlación de rendimientos mide el movimiento conjunto de un día, no si la distancia entre los precios vuelve a un nivel
   - C. Que ninguno de los dos está cointegrado, porque la correlación no llega a 0,9
   - D. Que los dos están cointegrados, porque la correlación supera 0,8

   *Respuesta B: Nada: la correlación de rendimientos mide el movimiento conjunto de un día, no si la distancia entre los precios vuelve a un nivel.* En los datos del curso B está cointegrado (Engle-Granger da t = −8,98) y E no (t = −2,60, p = 0,24), aunque E tiene más correlación. La correlación de rendimientos y la cointegración miden cosas distintas: la primera, el corto plazo; la segunda, un equilibrio a largo plazo.

2. ¿Por qué Engle-Granger no puede usar los valores críticos del ADF habitual en el segundo paso?

   - A. Porque el residuo no tiene media cero
   - B. Porque el β del primer paso se elige por MCO para que el residuo tenga la menor varianza posible: el residuo parece más estacionario de lo que es, y los valores críticos tienen que ser más negativos
   - C. Porque el ADF solo funciona con rendimientos
   - D. Porque el residuo siempre es un paseo aleatorio

   *Respuesta B: Porque el β del primer paso se elige por MCO para que el residuo tenga la menor varianza posible: el residuo parece más estacionario de lo que es, y los valores críticos tienen que ser más negativos.* Con los mismos −2,60 de estadístico, el ADF directo sobre el residuo del par E da p = 0,09 y `coint`, con los valores críticos de MacKinnon para residuos, p = 0,24. El valor crítico al 5 % pasa de −2,86 a −3,34.

3. Johansen sobre cuatro series (los dos activos de A y los dos de B) da rango 2. ¿Qué significa?

   - A. Que dos de las cuatro series son estacionarias
   - B. Que hay dos combinaciones lineales independientes de las cuatro series que son estacionarias: las relaciones de A y de B
   - C. Que hay que diferenciar dos veces
   - D. Que el contraste ha fallado

   *Respuesta B: Que hay dos combinaciones lineales independientes de las cuatro series que son estacionarias: las relaciones de A y de B.* El rango de la matriz Π del VECM es el número de relaciones de cointegración. Con cuatro logaritmos de precios no estacionarios y dos pares independientes, la respuesta conocida es 2, y es la que sale.

4. El estimador de la vida media por la regresión de ΔS sobre S tiene sesgo. ¿En qué dirección y cuándo importa más?

   - A. Hacia arriba, y más cuanto más rápida es la reversión
   - B. Hacia abajo (el spread parece revertir más deprisa), y más cuanto más larga es la vida media en relación con la muestra
   - C. No tiene sesgo si la muestra es de más de mil sesiones
   - D. Hacia abajo solo si el spread tiene colas gruesas

   *Respuesta B: Hacia abajo (el spread parece revertir más deprisa), y más cuanto más larga es la vida media en relación con la muestra.* Es el sesgo del coeficiente autorregresivo en muestras finitas: φ estimado queda por debajo del real. Con 3.018 sesiones, la vida media de 40 del par C sale en 35,4, y la simulación da una mediana de 37,2 con un 90 % de los casos entre 27,3 y 51,2.

5. Tras la ruptura de 2021, el tramo final del par H da p = 0,021 en Engle-Granger. ¿Qué es?

   - A. La prueba de que la cointegración ha vuelto
   - B. Un falso positivo: desde la ruptura el spread es un paseo aleatorio por construcción, y un tramo de pocos años puede pasar el contraste por azar
   - C. Un error de los datos
   - D. La señal de que el β ha cambiado y hay que reestimarlo

   *Respuesta B: Un falso positivo: desde la ruptura el spread es un paseo aleatorio por construcción, y un tramo de pocos años puede pasar el contraste por azar.* La respuesta conocida es que desde el 2021-06-15 el spread de H es un paseo aleatorio. Un contraste al 5 % rechaza por azar una de cada veinte veces; en ventanas de un año tras la ruptura, rechaza en 3 de 43, cerca de lo que se espera del azar.

## Ideas clave

- Correlación de rendimientos y cointegración miden cosas distintas: los controles E, F y G tienen una correlación de 0,86 y no están cointegrados; B tiene 0,79 y sí.
- Engle-Granger: MCO de un logaritmo de precio sobre el otro y un ADF del residuo con valores críticos propios (MacKinnon), más exigentes que los del ADF.
- Johansen contrasta el número de relaciones de cointegración entre varias series: rango 2 en las cuatro series de A y B, rango 1 con una serie ajena.
- La vida media del spread, ln 2 / θ, sale de un AR(1); el estimador la infravalora, más cuanto más lenta es la reversión.
- Los errores vienen de la formulación (en niveles, Johansen da rango mayor que 0 en 4 de 20 tríos de controles; en logaritmos, en ninguno), de cribar muchos pares, de β estáticos que aprueban con un spread lento (D) y de tramos cortos tras una ruptura (H).

## Recursos

### Research

- [Procesos Estocásticos - Paseos Aleatorios](https://pythonparatrading.com/procesos-estocasticos/) · Research-0005 · 12 min

### Documentación

- [statsmodels: coint (Engle-Granger)](https://www.statsmodels.org/stable/generated/statsmodels.tsa.stattools.coint.html) · statsmodels.org
- [statsmodels: coint_johansen](https://www.statsmodels.org/stable/generated/statsmodels.tsa.vector_ar.vecm.coint_johansen.html) · statsmodels.org

### Libros

- [Engle, R. F. y Granger, C. W. J. (1987): Co-Integration and Error Correction: Representation, Estimation, and Testing. Econometrica 55(2), 251-276](https://doi.org/10.2307/1913236) · doi.org
- [Johansen, S. (1991): Estimation and Hypothesis Testing of Cointegration Vectors in Gaussian Vector Autoregressive Models. Econometrica 59(6), 1551-1580](https://doi.org/10.2307/2938278) · doi.org
- [Murray, M. P. (1994): A Drunk and Her Dog: An Illustration of Cointegration and Error Correction. The American Statistician 48(1), 37-39](https://doi.org/10.1080/00031305.1994.10476017) · doi.org

### Datos

- [Datos del nivel 2: ocho pares sintéticos, 2014-2025 (Parquet) y su tabla de verdad](https://pythonparatrading.com/curso/datos/nivel-2/pares2.parquet) · Descarga · pares2.parquet

Lección anterior: [2.1.4 · Modelos de volatilidad: ARCH, GARCH, EGARCH y GJR con la librería arch](https://pythonparatrading.com/curso-python-trading/nivel-2/volatilidad-arch-garch-egarch-gjr/)

Lección siguiente: [2.1.6 · Filtro de Kalman para hedge ratios dinámicos](https://pythonparatrading.com/curso-python-trading/nivel-2/filtro-de-kalman-hedge-ratio-dinamico/)
