Existe una multitud de publicaciones en la red (que es vasta e infinita) sobre le análisis de series temporales utilizando Python y sus módulos. En esta serie de publicaciones recrearemos los modelos estadísticos de análisis e inferencia mas comunes y los aplicaremos a los precios del mercado de valores para diferentes activos y resoluciones temporales. El objetivo es, por supuesto, encontrar una ventaja de capacidad predictiva más allá de un modelo ingenuo y anticipar los movimientos del mercado ya que este es el primer paso para desarrollar una estrategia de gestión de activos o de trading, la principal fuente de alfa.
Utilizaremos el ambiente de investigación de Quantconnect para esta demostración estadística. Primeramente, obtendremos la historia para los últimos 5 años de la serie de precios del índice SPY en una resolución diaria:
self = QuantBook()
spy = self.AddEquity("SPY")
history = self.History(self.Securities.Keys, 365*5, Resolution.Daily)
history.reset_index(inplace = True)
Para poder inspeccionar mejor la serie temporal obtenida con esta petición podemos definir funciones de utilidad para la edición de graficas con los colores y tamaños que más nos gusten:
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import numpy as np
def create_plot(x, y, color='blue', size =(16,7), legend='Close Price', y_label='Price in USD' ):
plt.style.use('dark_background')
plt.rcParams["figure.figsize"] = size
fig,ax1 = plt.subplots()
plt.plot(x,y, color=color)
plt.legend([legend])
plt.ylabel(y_label)
date_form = mdates.DateFormatter("%m-%Y")
ax1.xaxis.set_major_formatter(date_form)
start, end = ax1.get_xlim()
ax1.xaxis.set_ticks(np.arange(start, end, 90))
_=plt.xticks(rotation=45)
Ahora podemos representar nuestra serie de datos llamando a esta función y pasando los valores de x e y:
x = history['time']
y = history['close']
create_plot(x,y, legend = 'SPY Close Price')
Sabemos que esta serie temporal no es estacionaria, nuestras herramientas de inferencia estadística lo tendrán muy difícil para obtener predicciones a medio o largo plazo bajo esta situación de medias y varianzas cambiantes. La forma no es buena. Las buenas maneras estadísticas se encuentran en las series diferenciadas, por ejemplo, ya sea por enteros o de manera fraccional. Para la diferenciación entera mas común, la forma, que si que presenta unas propiedades estadísticas buenas, pero carece de memoria, es:
y_prime = y.pct_change()
create_plot(x,y_prime, legend = 'SPY Price Change')
Podemos analizar más profundamente la forma de nuestra serie de datos utilizando el método de descomposición de la biblioteca statsmodel. Primero necesitamos crear una serie a través de nuestro dataframe alterando el índice:
time_series = history[['close']].set_index(history['time']).asfreq('d').ffill()
Tenemos que construir una serie temporal con una frecuencia distinta a ‘None’, en este caso nuestra frecuencia es diaria pero los fines de semana y festivos en los que el mercado está cerrado no están disponibles (marcados como NaN, Not a Number), tenemos que rellenar los huecos hacia adelante en el tiempo para simular que durante el fin de semana o vacaiones el precio se mantiene tal y como quedó el ultimo cierre, esto no es totalmente correcto aun así es válido para nuestro análisis. La nueva serie en descomposición se puede representar así, observando los componentes de tendencia, estacionalidad y residuo:
decomposition = sm.tsa.seasonal_decompose(time_series, model='additive')
plt.rcParams["figure.figsize"] = (10,10)
fig = decomposition.plot()
plt.show()
En la descomposición se puede observar la serie original arriba, las fuertes tendencias abajo, la falta de patrones estacionarios en el periodo y el residuo, que representa el error o diferencia con respecto a la tendencia. La tendencia es claramente de precios crecientes el periodo de 2014 a 2020, un fuerte mercado en alza. Los periodos de correcciones aparecen representados con errores residuales muy altos en la grafica de residuos, y están además agrupados en estos periodos de corrección.
Es interesante ver el efecto de la crisis del COVID19 en la tendencia del índice SPY cuando lo comparamos con periodos de crisis anteriores. Si extendemos la historia a los 20 años anteriores observamos esta descomposición:
El efecto de rotura de tendencia creado por el COVID19 es mucho mas severo que la crisis dotcom, la crisis subprime de 2008 y la minicrisis de 2015. El impacto de la pandemia global causa una gran hendidura en la tendencia, o la tendencia a partir del 2012 esta exagerada por factores económicos externos al mercado y hace parecer este evento especialmente notorio.
El efecto de esta ultima crisis puede estar exagerado por las dos direcciones: por el impacto, global, persistente y de alto alcance de la pandemia y la exuberancia anormal del periodo 2012 a 2020. Estos valores no están ajustados a la inflación y por lo tanto enmascaran un poco el efecto de la crisis en el poder adquisitivo, el efecto es todavía grande incluso si ajustamos los precios con valores aproximados de inflación:
Esta descomposición que obtenemos se utiliza normalmente para ajustar la serie a un modelo ARIMA (AutoRegressive Integrated Moving Average) con la intención de obtener predicciones futuras que tengan en cuenta la tendencia del precio, la estacionalidad o falta de ella (en general siendo este último caso para activos de bolsa de valores) y la desviación residual de la media móvil como ruido. Ya probamos una modelo ARIMA simple en esta publicación, extendemos el tema ahora con una selección de parámetros mas elaborada. Aumentar la variable RANGE en el código incrementara el tamaño de la parrilla de parámetros para nuestros modelos, luego comprobamos si cada uno de estos parámetros genera una solucion, si no hay solución continuamos al siguiente parámetro en la lista, si existe solución anotamos la métrica "Criterio de Información de Akaike" como media de puntuación del modelo:
import itertools
RANGE = 5
p = d = q = range(0, RANGE)
pdq = list(itertools.product(p, d, q))
y = time_series['close']
AIC_results = {}
for param in pdq:
try:
mod = sm.tsa.ARIMA(y, order=param)
results = mod.fit()
print(f'ARIMA{param} - AIC: {results.aic:.2f}')
AIC_results[results.aic] = param
except:
continue
Para esta serie temporal una parrilla de tamaño 5 es posiblemente demasiado grande, consumiendo mucho tiempo de procesador para comprobar la convergencia y el ajuste. En esta serie corta los resultados quedan en el diccionario:
best_params = AIC_results[min(AIC_results.keys())]
Este conjunto de parámetros se utiliza para ajustar un único modelo (el mejor modelo local y obtener el resumen de parámetros resultantes:
best_model = sm.tsa.ARIMA(y, order=best_params)
best_results = best_model.fit()
print(best_results.summary().tables[1])
En general valores de P>|z| por encima de 0.5 indican un parámetro “poco fiable”, así que nuestro modelo parece encajar bien el precio de SPY en los periodos 2015 a 2020. Si seguimos el mismo procedimiento para ajustar el periodo largo de 2000 a 2020 la tabla contiene los mejores parámetros de los modelos de acuerdo con el Criterio de Información de Akaike:
El modelo parece también sorprendentemente bueno y parece ser un caso típico de sobreajuste contra el pasado. El pasado es capaz de representarlo perfectamente, ¿será capaz el modelo de predecir el futuro? Las herramientas tsatools_plot_predict nos permite representar los valores de la serie temporal sobre un rango de predicciones para comprobar como de bien persigue a los valores reales:
start = datetime(2020, 1, 1)
end = datetime(2020, 1, 20)
plt.style.use('dark_background')
plt.rcParams["figure.figsize"] = (16,10)
pred = best_results.plot_predict(start, end, dynamic=False)
Con esto mostramos la predicción estática (la opción “dynamic” ajustada a False) y representamos la predicción a 1 paso futuro para el periodo dado. El modelo es capaz de perseguir este valor, pero con algunas predicciones que pueden destruir el valor de nuestra cuenta en los que la direccionalidad de la predicción esta completamente equivocada, cuando las líneas de predicción y realidad se cruzan hemos cometido un error de dirección y no solo de magnitud:
La opción dinámica nos permite usar el modelo ARIMA para predecir varios pasos futuros utilizando una única predicción inicial y dejando al modelo caminar solo. Cualquier predicción a un futuro más allá de un día acumula un error enorme según mas y más puntos de predicción erróneos se añaden al camino. Como ejemplo, si intentamos predecir 15 días a futuro de manera dinámica desde el 2020-10-1 al 2020-10-16 el resultado parece muy pobre:
¿Qué tal se comportará este modelo ARIMA más avanzado en una prueba retroactiva? Veamos los resultados en una prueba en la que entramos en posiciones diarias de SPY dependiendo de la dirección que predice el modelo ARIMA y la mantenemos durante ese día. El modelo busca valores óptimos de los parámetros pdq del modelo cada 60 predicciones y tiene el rango de la parrilla limitado a 5, para obtener resultados relativamente rápido:
La primera aproximación a que podemos usar para arreglar este problema es usar series fraccionalmente diferenciadas como nuestra serie de precios para obtener una serie estacionaria con la que el modelo ARIMA pueda comportarse mejor. También podemos evaluar otros métodos, el Prophet creado por Facebook, por ejemplo,aunque nos dé escalofríos y nos haga pensar que nosotros somos el producto. Trataremos estos remedios en futuras publicaciones.
Las publicaciones en Ostirion.net no son consejos financieros ni forman parte de un servicio de asesoramiento financiero. Ostirion.net no mantiene posiciones en ninguno de los instrumentos financieros que se mencionan en esta publicación en el momento de la publicación. Si necesita más información, apoyo con la gestión de activos financieros, desarrollo de estrategias de trading automatizados o despliegue táctico de estrategias existentes no dude en contactar con nosotros aquí.
Comments