---
title: "2.1.3 Modelos ARMA y ARIMA; ACF y PACF"
description: "Autocorrelación simple y parcial, identificación de AR y MA, ajuste con statsmodels, criterios de información, Ljung-Box y previsión fuera de la muestra."
url: "https://pythonparatrading.com/curso-python-trading/nivel-2/modelos-arma-arima-acf-y-pacf/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-2/modelos-arma-arima-acf-y-pacf.png"
---

# 2.1.3 · Modelos ARMA y ARIMA: autocorrelación simple (ACF) y parcial (PACF)

> Un modelo ARMA describe cuánto depende una serie de sus propios valores y errores pasados. En un spread que revierte, esa dependencia es fuerte y se aprovecha; en las rentabilidades de un índice, es tan débil que un ARMA no mejora a no predecir nada. Esta lección ajusta los dos casos con datos sintéticos de respuesta conocida y mide la diferencia fuera de la muestra.

- 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: 65 min
- Requisitos: [1.6.2 · Hechos estilizados: colas gruesas, agrupamiento, autocorrelación y apalancamiento](https://pythonparatrading.com/curso-python-trading/nivel-1/hechos-estilizados-de-los-mercados/), [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/), [2.1.2 · Estacionariedad: ADF, KPSS, diferenciación y diferenciación fraccional](https://pythonparatrading.com/curso-python-trading/nivel-2/estacionariedad-adf-kpss-y-diferenciacion-fraccional/)

## Al terminar esta lección

- Leer la ACF y la PACF para identificar un AR(p), un MA(q) o una serie sin memoria, con sus bandas de confianza.
- Ajustar modelos ARMA y ARIMA con `statsmodels.tsa.arima.model.ARIMA` y elegir el orden con AIC y BIC.
- Diagnosticar los residuos con el contraste de Ljung-Box.
- Comparar fuera de la muestra un ARMA con la previsión ingenua (paseo aleatorio o cero) y contrastar la diferencia con Diebold-Mariano.

## Memoria de una serie

La lección 1.6.2 midió la autocorrelación de las rentabilidades y concluyó que la dirección casi no tiene memoria. La
lección 2.1.2 separó las series estacionarias de las que no lo son. Esta lección da el paso siguiente: describir con
un modelo **cuánta** memoria tiene una serie estacionaria y usarlo para prever.

Los modelos ARMA combinan dos ingredientes:

- **AR(p), autorregresivo**: el valor de hoy depende de los $p$ valores anteriores,
  $y_t = c + \phi_1 y_{t-1} + \dots + \phi_p y_{t-p} + \varepsilon_t$.
- **MA(q), media móvil** (de los errores, no una media móvil de precios): el valor de hoy depende de los $q$ choques
  anteriores, $y_t = \mu + \varepsilon_t + \theta_1 \varepsilon_{t-1} + \dots + \theta_q \varepsilon_{t-q}$.

Un ARMA(p, q) tiene los dos. Un **ARIMA(p, d, q)** es un ARMA sobre la serie diferenciada $d$ veces: un ARIMA(1,1,1)
del logaritmo del precio es un ARMA(1,1) de sus rentabilidades. El procedimiento clásico de Box y Jenkins tiene cuatro
pasos que se repiten hasta que el diagnóstico es limpio:

```mermaid
flowchart TB
  I[Identificar] --> E[Estimar]
  E --> D[Diagnosticar]
  D -- residuos limpios --> P[Prever]
  D -- queda memoria --> I
```

Se usan dos series sintéticas del nivel 2 con respuesta conocida (README de los datos):

- el **spread verdadero del par A**, $u = \ln y + 2{,}1 - 1{,}5\,\ln x$ (en el código, multiplicado por 100 para
  leerlo en %), un Ornstein-Uhlenbeck discreto: un AR(1) con $\phi = 0{,}5^{1/5} = 0{,}871$ (vida media de 5
  sesiones);
- las **rentabilidades diarias del futuro IDA**, un índice ficticio de renta variable sin autocorrelación propia salvo
  la que inducen la deriva por tramos del generador y un momentum de series temporales pequeño.

```python
import warnings
import numpy as np
import pandas as pd
from statsmodels.tsa.stattools import acf, pacf

cont = pd.read_parquet("futuros2_continuos.parquet")
ida = np.log(cont[cont["mercado"] == "IDA"]
             .set_index("fecha")["cierre_ajustado"])
r = 100 * ida.diff().dropna()               # rentabilidad en %

pares = pd.read_parquet("pares2.parquet").pivot(
    index="fecha", columns="activo", values="precio")
# spread verdadero en log (ln y = -2,1 + 1,5·ln x + u), en %
sA = 100 * (np.log(pares["PZAY"]) + 2.1
            - 1.5 * np.log(pares["PZAX"]))

print(f"IDA: {len(r)} sesiones; spread A: {len(sA)} sesiones")
print(f"φ verdadero del spread A: {0.5 ** (1 / 5):.4f}")
```
```output
IDA: 6538 sesiones; spread A: 3018 sesiones
φ verdadero del spread A: 0.8706
```

## ACF y PACF: la huella de cada modelo

La **ACF** (función de autocorrelación) es la correlación de la serie con ella misma $k$ periodos antes. La **PACF**
(autocorrelación parcial) es la correlación en el retardo $k$ **descontando** los retardos intermedios: el último
coeficiente de una regresión de $y_t$ sobre $y_{t-1}, \dots, y_{t-k}$. Cada modelo deja una huella distinta en las dos:

| Modelo | ACF | PACF |
|---|---|---|
| Ruido blanco | Todo dentro de la banda | Todo dentro de la banda |
| AR(p) | Decae (geométrica u oscilante) | Se corta tras el retardo $p$ |
| MA(q) | Se corta tras el retardo $q$ | Decae |
| ARMA(p, q) | Decae a partir de $q$ | Decae a partir de $p$ |

La banda de ruido es $\pm 1{,}96/\sqrt{n}$: si la serie no tuviera memoria, un 95 % de las autocorrelaciones caería
dentro. Leída como un árbol de decisión, la tabla queda así:

```mermaid
flowchart TB
  A{¿ACF dentro banda?} -- sí --> W[Sin memoria]
  A -- no --> B{¿PACF se corta?}
  B -- sí --> AR[AR de orden p]
  B -- no --> C{¿ACF se corta?}
  C -- sí --> MA[MA de orden q]
  C -- no --> AM[ARMA p, q]
```

En la práctica las huellas se mezclan con el ruido de muestreo y la rejilla de criterios de información (más abajo)
decide entre los candidatos.

```python
def huella(x, k=5):
    x = pd.Series(x).dropna()
    return pd.DataFrame({"acf": acf(x, nlags=k)[1:],
                         "pacf": pacf(x, nlags=k)[1:]},
                        index=range(1, k + 1))

banda = {"spread A": 1.96 / np.sqrt(len(sA)),
         "IDA": 1.96 / np.sqrt(len(r))}
print({k: round(v, 3) for k, v in banda.items()})
pd.concat({"spread A": huella(sA), "rent. IDA": huella(r)},
          axis=1).round(3)
```
```output
{'spread A': np.float64(0.036), 'IDA': np.float64(0.024)}
  spread A        rent. IDA
       acf   pacf       acf   pacf
1    0.863  0.864     0.020  0.020
2    0.742 -0.013    -0.015 -0.016
3    0.640  0.007     0.017  0.018
4    0.549 -0.007     0.060  0.059
5    0.469 -0.011     0.025  0.024
```

La figura de la cabecera dibuja los 12 primeros retardos. El spread A tiene la huella de libro de un AR(1): la ACF baja
como una potencia de 0,86 (0,74 es casi 0,86²; 0,64, casi 0,86³) y la PACF vale 0,86 en el retardo 1 y nada después.
Las rentabilidades de IDA tienen autocorrelaciones minúsculas, de −0,015 a 0,06; la del retardo 4 (0,060) y la del 5
(0,025) se salen de la banda (±0,024), pero con 6.538 observaciones la banda es estrecha y cualquier dependencia
diminuta la supera.

Un MA se ve en un sitio familiar: las **rentabilidades solapadas** de la lección 2.1.1. La suma de 5 rentabilidades
diarias calculada cada día es un MA(4): dos sumas separadas $k$ días comparten $5-k$ días, así que su ACF teórica es
$(5-k)/5$ hasta el retardo 4 y cero después.

```python
r5 = r.rolling(5).sum().dropna()
ma = huella(r5, k=7)
ma["acf_teorica"] = [max(5 - k, 0) / 5 for k in ma.index]
ma.round(3)
```
```output
     acf   pacf  acf_teorica
1  0.814  0.815          0.8
2  0.628 -0.105          0.6
3  0.454 -0.079          0.4
4  0.278 -0.125          0.2
5  0.084 -0.193          0.0
6  0.072  0.402          0.0
7  0.078 -0.021          0.0
```

*Fig. — ACF de la suma de 5 rentabilidades diarias de IDA calculada cada día (barras) frente a la teórica de un MA(4), (5 − k)/5 (cuadrados).* Barras de autocorrelación por retardo, de 1 a 10: 0,81, 0,63, 0,45 y 0,28 en los cuatro primeros, casi encima de los cuadrados teóricos 0,8, 0,6, 0,4 y 0,2, y entre 0,07 y 0,08 desde el retardo 5. Una línea vertical destacada marca el corte tras el retardo 4.

La ACF sigue la teoría (0,81 frente a 0,80; 0,63 frente a 0,60…) y cae casi a cero tras el retardo 4; lo que queda
en 5, 6 y 7 (de 0,07 a 0,08) es la pequeña autocorrelación propia de IDA, sobre todo la del retardo 4, que entra en
varias sumas a la vez. La PACF, en cambio, no se corta: cambia de signo y vuelve a subir en el
retardo 6, la huella de un MA. Este es el motivo técnico de que un objetivo solapado infle los t: es una serie MA por
construcción.

## Estimar: el AR(1) del spread

`statsmodels.tsa.arima.model.ARIMA` estima por máxima verosimilitud (con un modelo de espacio de estados). El orden se
da como `(p, d, q)` y `trend="c"` añade la constante. Se pasan arrays de NumPy: con un índice de fechas sin
frecuencia regular (las sesiones de la NYSE no la tienen), statsmodels avisa de que no puede prever fechas futuras.

```python
from statsmodels.tsa.arima.model import ARIMA

res_a = ARIMA(sA.to_numpy(), order=(1, 0, 0), trend="c").fit()
tabla = pd.DataFrame({"estimado": res_a.params, "error": res_a.bse},
                     index=["media", "phi", "sigma2"])
tabla["verdad"] = [0, 0.5 ** (1 / 5), np.nan]
tabla.round(4)
```
```output
        estimado   error  verdad
media     0.2329  0.1297  0.0000
phi       0.8655  0.0090  0.8706
sigma2    0.9475  0.0245     NaN
```

El $\phi$ estimado, 0,865 con un error típico de 0,009, está a menos de un error típico del verdadero (0,871). La
media del spread (0,23 % con error 0,13) no se distingue de cero al 5 %, como debe ser con los $\alpha$ y $\beta$
verdaderos.
$\sigma^2$ (0,95, en %²) es la varianza del choque diario; la del spread es mayor, $\sigma^2/(1-\phi^2)$, porque los choques se
acumulan.

## Elegir el orden: AIC y BIC

Más parámetros siempre ajustan mejor dentro de la muestra. Los **criterios de información** penalizan cada parámetro:

$$
\text{AIC} = -2\ln L + 2k, \qquad \text{BIC} = -2\ln L + k\ln n
$$

con $L$ la verosimilitud, $k$ el número de parámetros y $n$ las observaciones. Se elige el menor. Con $n$ grande, la
penalización del BIC ($\ln n \approx 8$ con 3.000 datos) es cuatro veces la del AIC, y por eso elige modelos más
sencillos.

```python
from statsmodels.tools.sm_exceptions import (ConvergenceWarning,
                                             EstimationWarning)

def rejilla(x, pmax=2, qmax=2):
    filas = []
    for p in range(pmax + 1):
        for q in range(qmax + 1):
            with warnings.catch_warnings():
                # algunos MA no convergen del todo o arrancan de
                # valores no válidos; no cambia el orden elegido
                warnings.simplefilter("ignore", ConvergenceWarning)
                warnings.simplefilter("ignore", EstimationWarning)
                f = ARIMA(x, order=(p, 0, q), trend="c").fit()
            filas.append({"p": p, "q": q, "aic": f.aic, "bic": f.bic})
    t = pd.DataFrame(filas)
    return t.assign(d_aic=t.aic - t.aic.min(),
                    d_bic=t.bic - t.bic.min())

ra = rejilla(sA.to_numpy())
rr = rejilla(r.to_numpy())
cols = ["p", "q", "d_aic", "d_bic"]
print("mínimo del AIC en el spread (p, q):",
      ra.loc[ra.aic.idxmin(), ["p", "q"]].tolist())
print("spread A, por BIC:")
print(ra.sort_values("bic")[cols].head(3).round(1).to_string(
    index=False))
print("rentabilidades IDA, por BIC (y el ARMA(1,1)):")
orden = rr.sort_values("bic")
orden = pd.concat([orden.head(4),
                   orden[(orden.p == 1) & (orden.q == 1)]])
print(orden[cols].round(1).to_string(index=False))
```
```output
mínimo del AIC en el spread (p, q): [1, 0]
spread A, por BIC:
 p  q  d_aic  d_bic
 1  0    0.0    0.0
 1  1    1.5    7.5
 2  0    1.5    7.5
rentabilidades IDA, por BIC (y el ARMA(1,1)):
 p  q  d_aic  d_bic
 0  0   26.7    0.0
 2  2    0.0    0.4
 1  2   12.2    5.8
 0  1   26.1    6.2
 1  1   30.0   16.9
```

En el spread, los dos criterios eligen el AR(1), la respuesta correcta, pero con márgenes distintos: el BIC le da 7,5
puntos de ventaja sobre el siguiente y el AIC solo 1,5, porque penaliza menos el parámetro de más. En las
rentabilidades de IDA los dos criterios discrepan: el BIC elige el modelo sin memoria (0,0), y el AIC, un ARMA(2,2)
con 26,7 puntos de ventaja sobre él. Con 6.538 observaciones, cuatro parámetros más cuestan 8 puntos de AIC y 35 de
BIC; unas dependencias minúsculas pagan lo primero y no lo segundo. El ARMA(1,1), el punto de partida habitual, queda
por detrás del (0,0) en los dos (30,0 y 16,9 puntos). Los coeficientes explican por qué:

```python
res_r = ARIMA(r.to_numpy(), order=(1, 0, 1), trend="c").fit()
pd.Series(res_r.params, index=["media", "phi", "theta",
                               "sigma2"]).round(4)
```
```output
media     0.0251
phi      -0.9201
theta     0.9240
sigma2    1.6084
dtype: float64
```

$\phi = -0{,}920$ y $\theta = 0{,}924$: casi se anulan. En $(1 - \phi B)\,y_t = (1 + \theta B)\,\varepsilon_t$, con
$\phi \approx -\theta$ los dos factores se cancelan y queda $y_t \approx \varepsilon_t$: ruido blanco con
parámetros de más. Es la forma típica en que un ARMA ajustado a rentabilidades dice «aquí no hay nada». Las
dependencias minúsculas que sí hay (la deriva por tramos del generador, con años alcistas y crisis de fechas fijas, y
el momentum plantado, que es lento) no tienen la forma de un ARMA de orden bajo.

## Diagnosticar: Ljung-Box

Si el modelo es bueno, sus residuos no deben tener memoria. El contraste de **Ljung-Box** resume las $m$ primeras
autocorrelaciones $\hat\rho_k$ de una serie en un estadístico,

$$
Q = n(n+2)\sum_{k=1}^{m}\frac{\hat\rho_k^2}{n-k}
$$

que, sin autocorrelación, se reparte como una $\chi^2$ con $m$ grados de libertad (menos los parámetros ARMA
estimados, si se aplica a residuos).

```python
from statsmodels.stats.diagnostic import acorr_ljungbox

def lb(x, gl=0):
    t = acorr_ljungbox(x, lags=[10], model_df=gl)
    return t.iloc[0].round(4).to_dict()

pd.DataFrame({
    "spread A": lb(sA),
    "residuos AR(1)": lb(res_a.resid, gl=1),
    "rent. IDA": lb(r),
    "residuos ARMA(1,1)": lb(res_r.resid, gl=2)}).T
```
```output
                      lb_stat  lb_pvalue
spread A            8347.1553     0.0000
residuos AR(1)         6.8739     0.6502
rent. IDA             50.5221     0.0000
residuos ARMA(1,1)    49.6918     0.0000
```

El spread A tiene una memoria enorme (Q = 8.347); los residuos del AR(1) ya no (p = 0,65): el modelo recoge toda la
estructura. Las rentabilidades de IDA rechazan la ausencia de autocorrelación (Q = 50,5, p < 0,0001), y los residuos
del ARMA(1,1) casi igual (Q = 49,7): el modelo no ha quitado nada, como anunciaban sus coeficientes. La dependencia
que detecta Ljung-Box está sobre todo en el retardo 4 (0,060), fuera del alcance de un ARMA(1,1). Se podría subir el
orden hasta limpiar los residuos, pero la pregunta que importa es otra: si algo de esto sirve para prever.

## Prever fuera de la muestra

Se estima cada modelo con una parte de la historia y se prevé el resto **sin reestimar**: rentabilidades de IDA con
2000-2019 y previsión de 2020-2025; spread A con 2014-2021 y previsión de 2022-2025. `res.apply(nuevos_datos)` aplica
los parámetros ya estimados a datos nuevos y devuelve en `fittedvalues` la previsión a un paso de cada fecha, hecha con
la información del día anterior. La referencia es la previsión **ingenua**: cero para la rentabilidad (o la media
de entrenamiento) y el último valor para el spread (paseo aleatorio).

```python
import statsmodels.api as sm

tr, te = r[:"2019"].to_numpy(), r["2020":].to_numpy()
m = ARIMA(tr, order=(1, 0, 1), trend="c").fit()
f1 = m.apply(te).fittedvalues            # E[r_t | hasta t-1]

def ecm(real, prev):
    return np.mean((real - prev) ** 2)

print(f"ECM ARMA / ECM cero: {ecm(te, f1) / ecm(te, 0):.4f}")
print(f"ECM ARMA / ECM media: {ecm(te, f1) / ecm(te, tr.mean()):.4f}")
print(f"aciertos de signo: {np.mean(np.sign(f1) == np.sign(te)):.1%}"
      f" (días al alza: {np.mean(te > 0):.1%})")

# Diebold-Mariano: ¿la diferencia de pérdidas es distinta de cero?
dif = (te - f1) ** 2 - te ** 2
dm = sm.OLS(dif, np.ones(len(dif))).fit(
    cov_type="HAC", cov_kwds={"maxlags": 5})
print(f"Diebold-Mariano: t = {dm.tvalues[0]:.2f}, "
      f"p = {dm.pvalues[0]:.3f}")
```
```output
ECM ARMA / ECM cero: 1.0027
ECM ARMA / ECM media: 1.0030
aciertos de signo: 53.4% (días al alza: 53.2%)
Diebold-Mariano: t = 1.11, p = 0.265
```

El ARMA(1,1) **aumenta** el error cuadrático medio un 0,3 % frente a prever cero. Acierta el signo el 53,4 % de los
días, pero los días al alza son el 53,2 %: una regla «siempre comprado» acierta lo mismo. El contraste de **Diebold y
Mariano** (1995) toma la diferencia de pérdidas de cada día y contrasta si su media es cero, con errores de Newey-West
(lección 2.1.1): t = 1,11, p = 0,27. La diferencia no se distingue del azar, y va en contra del modelo. Este es el
resultado habitual de un ARMA sobre rentabilidades: alguna autocorrelación significativa dentro de la muestra, nada
aprovechable fuera.

En el spread pasa lo contrario. La previsión a $h$ pasos de un AR(1) tiene forma cerrada,
$\hat u_{t+h} = \mu + \phi^h (u_t - \mu)$, y se compara con el paseo aleatorio ($\hat u_{t+h} = u_t$) en varios
horizontes; para las rentabilidades, se compara la previsión de la suma de las $h$ siguientes con cero:

```python
ta, ea = sA[:"2021"].to_numpy(), sA["2022":].to_numpy()
mu, phi, _ = ARIMA(ta, order=(1, 0, 0), trend="c").fit().params
todo, n0 = np.r_[ta, ea], len(ta)
c, phi_r, _, _ = m.params

filas = {}
for h in [1, 2, 5, 10, 20]:
    t0 = np.arange(n0, len(todo) - h + 1)  # se prevé desde t0 - 1
    ultimo, real = todo[t0 - 1], todo[t0 + h - 1]
    ar = mu + phi ** h * (ultimo - mu)
    # rentabilidades: suma prevista de las h siguientes frente a 0
    idx = np.arange(len(te) - h + 1)
    prev = np.array([sum(c + phi_r ** j * (f1[i] - c)
                         for j in range(h)) for i in idx])
    suma = np.array([te[i:i + h].sum() for i in idx])
    filas[h] = {"spread: AR/paseo": ecm(real, ar) / ecm(real, ultimo),
                "rent.: ARMA/cero": ecm(suma, prev) / ecm(suma, 0)}
pd.DataFrame(filas).T.rename_axis("h").round(3)
```
```output
    spread: AR/paseo  rent.: ARMA/cero
h
1              0.924             1.003
2              0.857             1.002
5              0.710             1.003
10             0.587             1.001
20             0.535             0.999
```

*Fig. — Error cuadrático medio fuera de la muestra (2020-2025 o 2022-2025) relativo a la previsión ingenua, según el horizonte: AR(1) del spread A frente al paseo aleatorio y ARMA(1,1) de las rentabilidades de IDA frente a prever cero.* Dos curvas con el horizonte de 1 a 20 sesiones. La del spread A baja de 0,92 a un día a 0,59 a diez sesiones, destacado (41 % menos error que el paseo aleatorio), y a 0,53 a veinte. La de las rentabilidades de IDA se queda pegada a la línea de 1: entre 0,999 y 1,003 en todos los horizontes.

A un día, el AR(1) del spread solo reduce el error un 8 % frente al paseo aleatorio: con $\phi = 0{,}87$, el último
valor ya es casi la mejor previsión. A 10 sesiones, la reducción es del 41 %, porque el spread ha recorrido buena parte
del camino de vuelta ($\phi^{10} \approx 0{,}25$) y el paseo aleatorio no lo sabe. Con las rentabilidades, el
cociente se queda entre 0,999 y 1,003 en todos los horizontes: ninguna ganancia. La diferencia entre las dos líneas es la diferencia entre una estructura que se puede
operar (lección 2.3.3) y una que no.

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

Un error típico con ARIMA es la **previsión dentro de la muestra**: `res.fittedvalues` sobre los mismos datos con los
que se estimó el modelo usa parámetros que ya conocían el futuro. Para evaluar, estima con un tramo y prevé otro
(`apply`, o reestimando en ventana móvil, lección 2.5.2). Otro es elegir el orden con la muestra completa y luego
«validar» fuera: el orden también es un parámetro.

</aside>

## ARIMA sobre precios

Con $d = 1$, ARIMA diferencia por dentro. Un ARIMA(1,1,1) del logaritmo del precio es el ARMA(1,1) de sus
rentabilidades, sin constante salvo que se pida una tendencia:

```python
lp = 100 * ida.to_numpy()
a111 = ARIMA(lp, order=(1, 1, 1), trend="t").fit()
pd.DataFrame({"ARIMA(1,1,1) log precio": a111.params[:3],
              "ARMA(1,1) rentabilidad": res_r.params[:3]},
             index=["deriva", "phi", "theta"]).round(4)
```
```output
        ARIMA(1,1,1) log precio  ARMA(1,1) rentabilidad
deriva                   0.0251                  0.0251
phi                     -0.9201                 -0.9201
theta                    0.9240                  0.9240
```

Los coeficientes coinciden casi exactamente (con `trend="t"` la deriva del precio es la media de la rentabilidad). La
lección 2.1.2 explica cuándo diferenciar; ARIMA solo lo hace por ti. Para el trading, la parte útil del ARIMA está casi
siempre en el ARMA de algo estacionario: un spread, una base, un diferencial de tipos.

| Serie | Qué esperar de un ARMA | Uso |
|---|---|---|
| Rentabilidades diarias | Coeficientes diminutos o que se cancelan, previsión ≈ cero | Filtrar la media antes de un GARCH (lección 2.1.4) |
| Spread cointegrado | AR(1) con φ < 1 clara | Vida media, bandas de entrada (lecciones 2.1.5 y 2.3.3) |
| Rentabilidades solapadas | MA(h − 1) por construcción | Recordar que el t está inflado (lección 2.1.1) |
| Volatilidad o valor absoluto | Persistencia alta | Mejor un GARCH (lección 2.1.4) |

## Ejercicios

1. Ajusta un AR(1) al spread verdadero del par B ($\ln y - 0{,}7 - 0{,}8\,\ln x$, en %) y calcula su vida media,
   $\ln 0{,}5 / \ln \phi$. Compárala con la verdadera, 15 sesiones.

**Solución.**

```python
sB = 100 * (np.log(pares["PZBY"]) - 0.7
            - 0.8 * np.log(pares["PZBX"]))
fb = ARIMA(sB.to_numpy(), order=(1, 0, 0), trend="c").fit()
phi_b = fb.params[1]
print(f"phi = {phi_b:.4f} (verdad {0.5 ** (1 / 15):.4f}); "
      f"vida media = {np.log(0.5) / np.log(phi_b):.1f} sesiones")
```
```output
phi = 0.9479 (verdad 0.9548); vida media = 13.0 sesiones
```

El AR(1) estima 13,0 sesiones frente a las 15 verdaderas, aunque $\phi$ solo se equivoca en 0,007. Con $\phi$ cerca de 1,
un error pequeño en $\phi$ es un error grande en la vida media (la función $\ln 0{,}5/\ln\phi$ es muy empinada cerca de 1): por eso las vidas
medias largas se estiman peor. La lección 2.1.5 hace lo mismo con el $\beta$ estimado.

2. Simula con semilla 2131 un AR(2) de 3.000 observaciones con $\phi_1 = 0{,}5$ y $\phi_2 = 0{,}3$. Comprueba que la
   PACF se corta tras el retardo 2 y que el BIC elige el orden correcto en la rejilla.

**Solución.**

```python
rng = np.random.default_rng(2131)
e = rng.standard_normal(3000)
x = np.zeros(3000)
for t in range(2, 3000):
    x[t] = 0.5 * x[t - 1] + 0.3 * x[t - 2] + e[t]
print("PACF:", np.round(pacf(x, nlags=4)[1:], 3))
g = rejilla(x)
print(g.sort_values("bic")[["p", "q", "d_bic"]].head(3)
      .round(1).to_string(index=False))
```
```output
PACF: [0.677 0.306 0.02  0.001]
 p  q  d_bic
 2  0    0.0
 2  1    6.9
 1  2    7.6
```

La PACF vale ≈ 0,7 en el retardo 1 (la autocorrelación de un AR(2) con estos coeficientes es $0{,}5/(1-0{,}3) = 0{,}71$),
≈ 0,3 en el 2 (el propio $\phi_2$) y casi cero después: la huella de un AR(2). El BIC lo elige. Con datos simulados
la identificación es limpia; con datos de mercado, rara vez.

3. Un compañero ajusta un ARMA(3,3) a las rentabilidades diarias de una acción, obtiene el menor AIC de su rejilla y un
   acierto de signo del 54 % dentro de la muestra. ¿Qué tres comprobaciones le pedirías antes de creérselo?

**Solución.**

Primera: previsión fuera de la muestra con los parámetros y el orden fijados con datos anteriores, sin reestimar con el
tramo de prueba. Segunda: comparar con la referencia correcta, que no es el 50 % sino la proporción de días al alza
(una regla «siempre comprado») y el error de prever cero, con un contraste de Diebold-Mariano. Tercera: costes; un 54 %
de acierto con rentabilidades diarias pequeñas y una operación cada día se queda en nada tras horquilla y comisiones
(lección 2.4.7). Y una cuarta, de propina: cuántos órdenes y acciones probó antes de este (lección 2.1.8).

## Autoevaluación

1. La ACF del spread A decae despacio (0,86, 0,74, 0,64…) y su PACF es 0,86 en el retardo 1 y casi cero después. ¿Qué modelo sugiere?

   - A. Un MA(1)
   - B. Un AR(1) con φ ≈ 0,86
   - C. Un paseo aleatorio
   - D. Un ARMA(2,2)

   *Respuesta B: Un AR(1) con φ ≈ 0,86.* En un AR(1), la ACF decae geométricamente (φ, φ², φ³…) y la PACF se corta tras el primer retardo. La respuesta conocida del par A es un AR(1) con φ = 0,5^(1/5) = 0,871; el ajuste da 0,866.

2. La suma móvil de 5 rentabilidades diarias tiene una ACF de 0,81, 0,63, 0,45, 0,28 y menos de 0,1 desde el retardo 5. ¿Qué proceso es?

   - A. Un AR(4)
   - B. Un MA(4): la ACF se corta en el retardo q = 4 y sigue la teoría (5 − k)/5
   - C. Un proceso no estacionario
   - D. Ruido blanco

   *Respuesta B: Un MA(4): la ACF se corta en el retardo q = 4 y sigue la teoría (5 − k)/5.* Dos sumas de 5 días separadas k días comparten 5 − k días. Es el solape de la lección 2.1.1 visto como modelo: un MA(4) con todos los coeficientes iguales a 1. La PACF, en cambio, decae y oscila.

3. Un ARMA(1,1) de las rentabilidades de IDA da φ = −0,920 y θ = 0,924. ¿Qué indica que los dos coeficientes casi se anulen?

   - A. Que el modelo predice muy bien
   - B. Que el modelo es casi ruido blanco: el término AR y el MA se compensan y apenas queda memoria. Por eso el BIC prefiere el modelo sin memoria y, fuera de la muestra, el ARMA es un 0,3 % peor que prever cero
   - C. Que hay un error de estimación
   - D. Que la serie es un paseo aleatorio

   *Respuesta B: Que el modelo es casi ruido blanco: el término AR y el MA se compensan y apenas queda memoria. Por eso el BIC prefiere el modelo sin memoria y, fuera de la muestra, el ARMA es un 0,3 % peor que prever cero.* Con φ ≈ −θ, los factores (1 − φB) y (1 + θB) casi se cancelan y el ARMA(1,1) se reduce a ruido blanco. Los dos criterios lo ponen por detrás del modelo (0,0) (16,9 puntos de BIC, 30,0 de AIC), y el contraste de Diebold-Mariano no distingue su previsión de prever cero (p = 0,27).

4. ¿Para qué sirve el contraste de Ljung-Box sobre los residuos de un modelo?

   - A. Para elegir el número de retardos
   - B. Para comprobar que no queda autocorrelación sin explicar: si los residuos tienen memoria, el modelo se ha dejado estructura
   - C. Para medir la volatilidad
   - D. Para comparar dos modelos fuera de la muestra

   *Respuesta B: Para comprobar que no queda autocorrelación sin explicar: si los residuos tienen memoria, el modelo se ha dejado estructura.* Ljung-Box resume las autocorrelaciones de los primeros retardos en un estadístico; su nula es que todas son cero. Con los residuos del AR(1) del spread A, p = 0,65: no queda estructura. Con las rentabilidades de IDA y con los residuos de su ARMA(1,1), p < 0,0001: hay dependencias pequeñas que ese modelo no recoge.

5. Fuera de la muestra, el AR(1) del spread A reduce el error cuadrático de la previsión a 10 sesiones un 41 % frente al paseo aleatorio. ¿Por qué tanto más que a 1 sesión (8 %)?

   - A. Por suerte
   - B. Porque a un día el último valor ya es casi la mejor previsión (φ = 0,87); a 10 días el spread ha recorrido buena parte del camino hacia su media (φ elevado a 10 ≈ 0,25) y el paseo aleatorio no lo sabe
   - C. Porque el modelo se reestima cada día
   - D. Porque la volatilidad baja con el horizonte

   *Respuesta B: Porque a un día el último valor ya es casi la mejor previsión (φ = 0,87); a 10 días el spread ha recorrido buena parte del camino hacia su media (φ elevado a 10 ≈ 0,25) y el paseo aleatorio no lo sabe.* La previsión del AR(1) a h pasos es la media más φ elevado a h por la distancia de hoy a la media: converge a la media. Es la razón de que la reversión de un spread se opere a días vista y no al día siguiente.

## Ideas clave

- AR(p): ACF que decae y PACF que se corta en p. MA(q): ACF que se corta en q y PACF que decae. Sin memoria: las dos dentro de la banda ±1,96/√n.
- El spread del par A es un AR(1) plantado con φ = 0,871: la ACF y la PACF lo identifican y el ajuste da 0,866.
- Una rentabilidad a h días calculada cada día es un MA(h − 1): el solape fabrica autocorrelación.
- BIC penaliza más los parámetros que AIC y suele elegir modelos más sencillos; los dos se calculan dentro de la muestra y no garantizan nada fuera.
- En las rentabilidades de IDA, el BIC elige el modelo sin memoria y el ARMA(1,1) es, fuera de la muestra, un 0,3 % peor que prever cero (no significativo): lo normal en rentabilidades reales.
- Donde hay estructura (un spread estacionario), el AR(1) bate al paseo aleatorio, y más cuanto más largo el horizonte: un 41 % a 10 sesiones.

## Recursos

### Research

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

### Documentación

- [statsmodels: ARIMA (statsmodels.tsa.arima.model)](https://www.statsmodels.org/stable/generated/statsmodels.tsa.arima.model.ARIMA.html) · statsmodels.org
- [statsmodels: acorr_ljungbox](https://www.statsmodels.org/stable/generated/statsmodels.stats.diagnostic.acorr_ljungbox.html) · statsmodels.org

### Libros

- [Hyndman, R. J. y Athanasopoulos, G.: Forecasting: Principles and Practice, 3.ª ed., capítulo 9 «ARIMA models» (OTexts)](https://otexts.com/fpp3/arima.html) · otexts.com
- [Diebold, F. X. y Mariano, R. S. (1995): Comparing Predictive Accuracy. Journal of Business & Economic Statistics 13(3), 253-263](https://doi.org/10.1080/07350015.1995.10524599) · doi.org

### Datos

- [Datos del nivel 2: futuros continuos sintéticos de 24 mercados (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/futuros2_continuos.parquet) · Descarga · futuros2_continuos.parquet

Lección anterior: [2.1.2 · Estacionariedad: ADF, KPSS, diferenciación y diferenciación fraccional](https://pythonparatrading.com/curso-python-trading/nivel-2/estacionariedad-adf-kpss-y-diferenciacion-fraccional/)

Lección siguiente: [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/)
