Quant500 Academy

6. Estacionariedad, Dickey-Fuller y cointegración

Nivel 8: Series Temporales - Estacionariedad

Los datos son estacionarios cuando los parámetros del proceso generador de datos no cambian con el tiempo. Sin fórmulas: la media de la serie en el instante t es un valor esperado que puede depender de t, y la varianza es el valor esperado del cuadrado de la desviación respecto a esa media, y también puede depender de t. Una serie es estacionaria en media si esa media es constante a lo largo del tiempo, y estacionaria en varianza si la varianza es constante a lo largo del tiempo.

Las series son estacionarias si no tienen tendencia ni efectos estacionales. Los estadísticos calculados sobre ellas son consistentes en el tiempo, como la media o la varianza de las observaciones. Cuando una serie es estacionaria es más fácil de modelar. Los métodos de modelización estadística asumen o exigen estacionariedad para ser efectivos; muchos contrastes estadísticos, en la letra pequeña de sus supuestos, requieren que los datos analizados sean estacionarios. Una serie estacionaria es sencilla de predecir porque podemos asumir que sus propiedades estadísticas futuras serán iguales o proporcionales a las actuales.

Cómo comprobar la estacionariedad. Hay tres vías: mirar los gráficos y comprobar visualmente si hay tendencias o estacionalidad evidentes; revisar los estadísticos descriptivos por subperiodos o particiones aleatorias y buscar diferencias significativas; y aplicar contrastes estadísticos formales.

Un experimento sintético lo deja claro:

def generate_datapoint(params):
    mu, sigma = params[0], params[1]
    return np.random.normal(mu, sigma)

T = 100

A = pd.Series(index=range(T), dtype=float)     # media 0, sigma 1: estacionaria
for t in range(T):
    A[t] = generate_datapoint((0, 1))

B = pd.Series(index=range(T), dtype=float)     # la media crece con el tiempo
for t in range(T):
    B[t] = generate_datapoint((t * 0.1, 1))

C = pd.Series(index=range(T), dtype=float)     # la varianza crece con el tiempo
for t in range(T):
    C[t] = generate_datapoint((0, t * 1))

D = pd.Series(index=range(T), dtype=float)     # la media oscila cíclicamente
for t in range(T):
    D[t] = generate_datapoint((np.sin(t), 1))

La serie A procede de un proceso estacionario con parámetros fijos; B y C se generan con parámetros que cambian en el tiempo. Si dibujas la media de B como línea horizontal sobre la propia serie, verás el problema: la media calculada muestra el promedio de todos los datos hasta la fecha, pero no sirve para predecir ningún estado futuro. Carece de sentido compararla con un instante concreto, porque es una amalgama de estados distintos en momentos distintos. Es un ejemplo simple y claro de por qué la no estacionariedad estropea el análisis; en la práctica surgen problemas mucho más sutiles. La serie D lo ilustra: un movimiento cíclico de la media es muy difícil de distinguir del ruido aleatorio, y con datos ruidosos y muestras limitadas puede ser complicado determinar si una serie es estacionaria y si una deriva es ruido o parte de una tendencia.

Test de Dickey-Fuller aumentado (ADF). Es un contraste estadístico de raíz unitaria. La intuición es que determina con qué fuerza una serie está definida por una tendencia. Utiliza un modelo autorregresivo y optimiza un criterio de información sobre distintos valores de retardo. La hipótesis nula es que la serie tiene raíz unitaria, es decir, que NO es estacionaria (tiene alguna estructura dependiente del tiempo). La hipótesis alternativa, rechazar la nula, es que la serie es estacionaria.

X = df.values          # precios
result = adfuller(X)
print('ADF Statistic: %f' % result[0])
print('p-value: %f' % result[1])
print('Critical Values:')
for key, value in result[4].items():
    print('\t%s: %.3f' % (key, value))

X = Re                 # retornos
result = adfuller(X)

Sobre los retornos obtenemos un estadístico de -14.536258, menor que el valor crítico de -2.862 al 5%. Esto indica que podemos rechazar la hipótesis nula con un nivel de significación inferior al 5% (baja probabilidad de que el resultado sea una casualidad estadística). Rechazar la nula significa que el proceso no tiene raíz unitaria y, por tanto, que la serie es estacionaria o no tiene estructura dependiente del tiempo. Sobre los precios, en cambio, el estadístico se queda muy por encima de los valores críticos y el p-valor es alto: no se puede rechazar la nula.

Cointegración. Una combinación lineal de varias series es una serie nueva construida sumando cada serie multiplicada por un coeficiente real. Si todas las series de un conjunto son I(1), es decir, necesitan una diferenciación para ser estacionarias, y existe alguna combinación lineal de ellas que es I(0), es decir, estacionaria sin diferenciar, decimos que el conjunto está cointegrado.

La intuición: para alguna combinación lineal de las series, el resultado carece de autocovarianza apreciable y es esencialmente ruido. Eso es útil en casos como el pairs trading, donde buscamos dos activos cuyos precios estén cointegrados. Como la combinación lineal de sus precios es ruido, podemos apostar a que esa relación revierte a la media y colocar operaciones en consecuencia.

N = 100
X1 = np.cumsum(np.random.normal(0, 1, N))   # ruido estacionario integrado: I(1)
X1 = pd.Series(X1); X1.name = 'X1'

X2 = X1 + np.random.normal(0, 1, N)         # X1 más ruido estacionario
X2.name = 'X2'

Z = X2 - X1                                  # la combinación lineal: estacionaria por diseño
Z.name = 'Z'
plt.plot(Z); plt.show()

Como X2 es una serie I(1) más ruido estacionario, sigue siendo I(1). Para mostrar la cointegración necesitamos encontrar una combinación lineal de X1 y X2 que sea estacionaria: restando obtenemos exactamente el ruido con el que construimos X2.

El resto de Quant500

Lo que estás aprendiendo, aplicado al índice todos los días:

La cartera del modelo, cada semana en tu correo

Cada sábado, su semana en un minuto: cómo fue, lo mejor y lo peor, y qué directivos compraron con su dinero. Y el día que el modelo se rehace, qué entra y qué sale. Es una simulación, no un consejo. Como mucho un correo a la semana, sin publicidad y sin cuenta; te das de baja con un clic. Lo hace una persona: Pol Gracia.

Cartera modelo con resultados simulados, no una recomendación personalizada. Tu correo se usa solo para esto: privacidad.

Detrás de la Academia y del resto de Quant500 hay una sola persona: Pol Gracia Serrahima.