Lección 2.1.3 · 65 min

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.

Fig. — ACF y PACF de los 12 primeros retardos: spread verdadero del par A, un AR(1) plantado (arriba), y rentabilidades diarias de IDA (abajo). Líneas discontinuas: banda ±1,96/√n
Lecciones del nivel 2
  1. 2.1 · Estadística y econometría para trading

    1. 2.1.1 Contrastes de hipótesis, p-valores, bootstrap y regresión con errores robustos
    2. 2.1.2 Estacionariedad: ADF, KPSS, diferenciación y diferenciación fraccional
    3. 2.1.3 Modelos ARMA y ARIMA: autocorrelación simple (ACF) y parcial (PACF)
    4. 2.1.4 Modelos de volatilidad: ARCH, GARCH, EGARCH y GJR con la librería arch
    5. 2.1.5 Cointegración: Engle-Granger, Johansen y vida media de la reversión
    6. 2.1.6 Filtro de Kalman para hedge ratios dinámicos
    7. 2.1.7 Detección de regímenes: modelos de Markov ocultos y cambios estructurales
    8. 2.1.8 El problema de las pruebas múltiples: Bonferroni, FDR y el «factor zoo»
  2. 2.2 · Anatomía de un sistema de trading

    1. 2.2.1 Hipótesis de ventaja: riesgo, comportamiento, estructura e información
    2. 2.2.2 Universo, señal, filtro, entrada, salida, sizing y gestión del riesgo
    3. 2.2.3 Frecuencias: intradía, swing y posicional, y la capacidad de una estrategia
    4. 2.2.4 Diario de investigación y registro de experimentos contra el data snooping
  3. 2.3 · Familias de estrategias

    1. 2.3.1 Seguimiento de tendencia y momentum
    2. 2.3.2 Reversión a la media: RSI y Bollinger, reversión intradía y huecos
    3. 2.3.3 Pairs trading y arbitraje estadístico: distancia, cointegración y Kalman
    4. 2.3.4 Carry en divisas, futuros y bonos; estacionalidad y efectos calendario
    5. 2.3.5 Factores (value, calidad, baja volatilidad, tamaño) y estrategias de eventos
    6. 2.3.6 Introducción al market making: inventario, spread y selección adversa (Avellaneda-Stoikov)
    7. 2.3.7 Estrategias específicas de cripto: momentum, arbitraje de funding y basis
  4. 2.4 · Backtesting: motores y paradigmas

    1. 2.4.1 Backtest vectorizado desde cero: matrices, desfase, costes y límites
    2. 2.4.2 Backtest por eventos desde cero: bucle, cartera, órdenes y fills
    3. 2.4.3 vectorbt: barridos masivos de parámetros y simulación de carteras
    4. 2.4.4 backtesting.py y backtrader: dos motores didácticos y su estado
    5. 2.4.5 NautilusTrader: arquitectura, datos, estrategias y el mismo código en backtest y en real
    6. 2.4.6 LEAN/QuantConnect y Zipline Reloaded; bt para asignación de activos
    7. 2.4.7 Modelado de costes: comisiones, horquilla, impacto, financiación y préstamo de títulos
    8. 2.4.8 Modelado de fills: órdenes limitadas, cola, fills parciales, huecos y stops
  5. 2.5 · Sesgos y validación

    1. 2.5.1 Sesgos de backtest: anticipación, supervivencia, selección y narrativa
    2. 2.5.2 In-sample y out-of-sample: walk-forward anclado y móvil
    3. 2.5.3 Validación cruzada combinatoria purgada para estrategias
    4. 2.5.4 Deflated Sharpe Ratio y Probability of Backtest Overfitting (PBO)
    5. 2.5.5 Monte Carlo: remuestreo de trades, bootstrap de bloques y permutaciones de señales
    6. 2.5.6 Sensibilidad de parámetros: mesetas frente a picos, robustez entre activos y periodos
    7. 2.5.7 Datos sintéticos para pruebas de estrés: GBM, GARCH y bootstrapping
    8. 2.5.8 Checklist de «backtest honesto» antes de pasar a paper trading
  6. 2.6 · Métricas de rendimiento

    1. 2.6.1 CAGR, Sharpe, Sortino, Calmar, Omega, drawdown y Ulcer Index
    2. 2.6.2 Win rate, payoff, expectativa, alpha, beta, information ratio, turnover y capacidad
    3. 2.6.3 Informes con QuantStats o pyfolio-reloaded y tearsheets propios

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 pp valores anteriores, yt=c+ϕ1yt−1+⋯+ϕpyt−p+εty_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 qq choques anteriores, yt=μ+εt+θ1εt−1+⋯+θqεt−qy_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 dd 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:

residuos limpiosqueda memoriaIdentificarEstimarDiagnosticarPrever
Fuente 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⁡xu = \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 ϕ=0,51/5=0,871\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.
IN [01]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}")
OUT [01]Dos series: un spread que revierte y unas rentabilidades
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 kk periodos antes. La PACF (autocorrelación parcial) es la correlación en el retardo kk descontando los retardos intermedios: el último coeficiente de una regresión de yty_t sobre yt−1,…,yt−ky_{t-1}, \dots, y_{t-k}. Cada modelo deja una huella distinta en las dos:

ModeloACFPACF
Ruido blancoTodo dentro de la bandaTodo dentro de la banda
AR(p)Decae (geométrica u oscilante)Se corta tras el retardo pp
MA(q)Se corta tras el retardo qqDecae
ARMA(p, q)Decae a partir de qqDecae a partir de pp

La banda de ruido es ±1,96/n\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í:

sínosínosíno¿ACF dentro banda?Sin memoria¿PACF se corta?AR de orden p¿ACF se corta?MA de orden qARMA p, q
Fuente 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.

IN [02]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)
OUT [02]ACF y PACF de los dos casos
{'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 kk días comparten 5−k5-k días, así que su ACF teórica es (5−k)/5(5-k)/5 hasta el retardo 4 y cero después.

IN [03]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)
OUT [03]Un MA(4) de fabricación propia: sumas solapadas de 5 días
     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)

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.

IN [04]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)
OUT [04]AR(1) del spread A
        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. σ2\sigma^2 (0,95, en %²) es la varianza del choque diario; la del spread es mayor, σ2/(1−ϕ2)\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:

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

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

IN [05]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))
OUT [05]Rejilla de órdenes para el spread y para las rentabilidades
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é:

IN [06]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)
OUT [06]El ARMA(1,1) de las rentabilidades
media     0.0251
phi      -0.9201
theta     0.9240
sigma2    1.6084
dtype: float64

ϕ=−0,920\phi = -0{,}920 y θ=0,924\theta = 0{,}924: casi se anulan. En (1−ϕB) yt=(1+θB) εt(1 - \phi B)\,y_t = (1 + \theta B)\,\varepsilon_t, con ϕ≈−θ\phi \approx -\theta los dos factores se cancelan y queda yt≈εty_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 mm primeras autocorrelaciones ρ^k\hat\rho_k de una serie en un estadístico,

Q=n(n+2)∑k=1mρ^k2n−kQ = n(n+2)\sum_{k=1}^{m}\frac{\hat\rho_k^2}{n-k}

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

IN [07]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
OUT [07]Ljung-Box antes y después de modelar
                      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).

IN [08]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}")
OUT [08]Rentabilidades de IDA: ARMA(1,1) frente a no predecir
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 hh pasos de un AR(1) tiene forma cerrada, u^t+h=μ+ϕh(ut−μ)\hat u_{t+h} = \mu + \phi^h (u_t - \mu), y se compara con el paseo aleatorio (u^t+h=ut\hat u_{t+h} = u_t) en varios horizontes; para las rentabilidades, se compara la previsión de la suma de las hh siguientes con cero:

IN [09]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)
OUT [09]Ganancia frente a la previsión ingenua según el horizonte
    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

A un día, el AR(1) del spread solo reduce el error un 8 % frente al paseo aleatorio: con ϕ=0,87\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 (ϕ10≈0,25\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.

ARIMA sobre precios

Con d=1d = 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:

IN [10]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)
OUT [10]ARIMA(1,1,1) del log precio = ARMA(1,1) de las rentabilidades
        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.

SerieQué esperar de un ARMAUso
Rentabilidades diariasCoeficientes diminutos o que se cancelan, previsión ≈ ceroFiltrar la media antes de un GARCH (lección 2.1.4)
Spread cointegradoAR(1) con φ < 1 claraVida media, bandas de entrada (lecciones 2.1.5 y 2.3.3)
Rentabilidades solapadasMA(h − 1) por construcciónRecordar que el t está inflado (lección 2.1.1)
Volatilidad o valor absolutoPersistencia altaMejor 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\ln y - 0{,}7 - 0{,}8\,\ln x, en %) y calcula su vida media, ln⁡0,5/ln⁡ϕ\ln 0{,}5 / \ln \phi. Compárala con la verdadera, 15 sesiones.
Solución
IN [11]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")
OUT [11]Vida media del spread B
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⁡ϕ\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.

  1. Simula con semilla 2131 un AR(2) de 3.000 observaciones con ϕ1=0,5\phi_1 = 0{,}5 y ϕ2=0,3\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
IN [12]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))
OUT [12]AR(2) simulado: PACF y BIC
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,710{,}5/(1-0{,}3) = 0{,}71), ≈ 0,3 en el 2 (el propio ϕ2\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.

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

Respuesta plegada en cada pregunta

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?

    • AUn MA(1)
    • BUn AR(1) con φ ≈ 0,86
    • CUn paseo aleatorio
    • DUn ARMA(2,2)
    Ver respuesta

    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?

    • AUn AR(4)
    • BUn MA(4): la ACF se corta en el retardo q = 4 y sigue la teoría (5 − k)/5
    • CUn proceso no estacionario
    • DRuido blanco
    Ver respuesta

    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?

    • AQue el modelo predice muy bien
    • BQue 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
    • CQue hay un error de estimación
    • DQue la serie es un paseo aleatorio
    Ver respuesta

    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?

    • APara elegir el número de retardos
    • BPara comprobar que no queda autocorrelación sin explicar: si los residuos tienen memoria, el modelo se ha dejado estructura
    • CPara medir la volatilidad
    • DPara comparar dos modelos fuera de la muestra
    Ver respuesta

    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 %)?

    • APor suerte
    • BPorque 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
    • CPorque el modelo se reestima cada día
    • DPorque la volatilidad baja con el horizonte
    Ver respuesta

    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