Grandes estudiosos del mercado, hablan sobre el comportamiento cíclico de los mercados de valores, y por eso considero importante dar una lectura a la estacionalidad de algunos activos, hoy quiero mostrarles cómo identificar pautas estacionales con python.
Pero antes de empezar, ¿Qué es una pauta estacional? José Martínez, en un artículo para www.traders-mag.es en Diciembre 2016, titulado “Las pautas estacionales: Un comportamiento cíclico” Define la pauta estacional como “comportamiento bursátil de algunos subyacentes, dentro de un marco temporal concreto”.
Partiendo de esta definición es fácil imaginar que los Commodities como soya, trigo y maíz o gas natural y petróleo, tienen un comportamiento bursátil que responde a las condiciones climáticas que afecten los procesos productivos de los mismos.
¿El resto de activos financieros responde a pautas estacionales? Bueno, citando nuevamente a José Martínez, “La pauta estacional no hace referencia únicamente a las estaciones del año sino también a comportamientos excepcionales de la bolsa frente a un dato concreto, bien sea semanal, mensual, anual, etc.. Como por ejemplo el comportamiento de determinadas acciones, dentro de la publicación de algún dato regular como puede ser el dato de empleo, stocks, consumo, etc”. Y esto amigos míos, es lo que quiero que observemos, si aún no está convencido sobre porque ponerle un ojito a las pautas estacionales, lo invito a leer el artículo “Is Seasonality Is The Best Investment Strategy?” de Dimitri Speck (Spoiler Alert!!!), en su artículo Dimitri Speck muestra los resultados de un estudio de más de 200 años donde ¡La estacionalidad supera a todas las demás estrategias!
Sources: Guido Baltussen, Laurens Swinkels, Pim van Vliet Illustration: Seasonax.
Atención: La idea no es que abandone el resto de las estrategias, la intención es ponerle un ojito a la estacionalidad y bien lo dice Dimitri Speck “piensa por ti mismo en lugar de seguir a la manada. ¡Ten en cuenta la estacionalidad!”, existe toda una serie de documentación con respecto a la estacionalidad y si desea realizar una revisión no está de más que detalle documentación como la que aparece en el Stock Trader’s Almanac de Jeffrey A. Hirsch o paginas como https://charts.equityclock.com/
Estudio con Cuartiles y Diagrama de Bigotes y Cajas
¿Qué son los Cuartiles? y ¿Que es un Diagrama de Bigotes y Cajas?
En estadística, dada una distribución de datos estadísticos, los cuartiles son aquellos valores que la dividen en cuartos. El primer cuartil o cuartil inferior es aquel valor de la variable tal que la cuarta parte (25%) de las observaciones son inferiores o iguales a él, y el resto (75%) es superior o igual. El segundo cuartil es la mediana, ya que se trata del valor localizado en la mitad de la distribución. Finalmente, el tercer cuartil o cuartil superior es un valor tal que las tres cuartas partes de las observaciones son inferiores o iguales a él. Coinciden con los percentiles 25, 50 y 75 respectivamente.
Mientras que el Diagrama de bigotes y cajas es un método estandarizado para representar gráficamente una serie de datos numéricos a través de sus cuartiles. De esta manera, el diagrama de caja muestra a simple vista la mediana y los cuartiles de los datos.
Componentes del diagrama de caja. Fuente: https://es.wikipedia.org/wiki/Diagrama_de_caja
Manos a la Obra
Tomaremos una serie de datos del contrato del Maíz, para determinar su estacionalidad a través del uso de cuartiles y el diagrama de bigotes y cajas. Y nos fijaremos en el comportamiento de los retornos observando el diagrama, ejecutamos el siguiente código:
1 | #Import necesary libraries 2 | from datetime import date 3 | import numpy as np 4 | import pandas as pd 5 | import matplotlib.pyplot as plt 6 | import seaborn as sns 7 | import yfinance as yf 8 | 9 | 10 | %matplotlib inline 11 | plt.rcParams["figure.figsize"] = (12, 6) 12 | sns.set() 13 | 14 | #Descarga de Datos y Grafico de Quartiles Mensuales (incrementos mensuales medios por años) 15 | 16 | start_date ='2010-01-01' 17 | today = date.today() 18 | end_date = today.strftime("%Y-%m-%d") 19 | data = yf.download("ZC=F", start=start_date, end=end_date) 20 | data.drop(['Open', 'Low', 'High', 'Volume', 'Close'], axis=1) 21 | 22 | returns = np.log(data["Adj Close"]).diff().dropna().to_frame("Returns") 23 | Monthly_Returns = returns.groupby([returns.index.year.rename('year'), 24 | returns.index.month.rename('month')]).mean() 25 | Monthly_Returns.boxplot(column='Returns', by='month');
Bueno como podemos observar, para los meses de Junio (6), Julio (7) y Septiembre (9) tienen a tener grandes variaciones, ahora prestemos atención a lo siguiente la mediana demuestra que de haber invertido en Julio hasta Septiembre habríamos obtenidos retornos positivos, o bien de Noviembre a Diciembre, a excepción de un caso en Diciembre donde el retorno es inferior a la mediana de Noviembre, algo similar ocurre de Marzo a Abril, este es un primer vistazo.
También podemos observar la variación del Maíz por día en todos estos años bajo estudio, veamos como:
1 | def boxplot_group_day(returns_slice): 2 | Daily_Returns = returns_slice.groupby([returns_slice.index.isocalendar().week.rename('week'), 3 | returns_slice.index.dayofweek.rename('day')]).mean() 4 | return Daily_Returns.boxplot(column='Returns', by='day') 5 | 6 | boxplot_group_day(returns);
No se observan grandes variaciones para este caso, los retornos tienden a mantenerse cercanos a 0 salvo algunos puntos atípicos. Y ¿qué tal si ahora observamos la variación de los retornos para un mes en específico?
Vamos:
1 | returns_of_june = returns.loc[returns.index.month.isin([6])] 2 | boxplot_group_day(returns_of_june);
Qué curioso pareciera que para el mes de Junio (6), si invertimos un lunes (0) y cerramos la operación un Martes (1) o Miércoles (2), obtendríamos retornos positivos. Ahora veamos el comportamiento para otro mes, que tal Diciembre (12).
Vamos:
1 | returns_of_june = returns.loc[returns.index.month.isin([6])] 2 | boxplot_group_day(returns_of_june);
Qué curioso pareciera que para el mes de Junio (6), si invertimos un lunes (0) y cerramos la operación un Martes (1) o Miércoles (2), obtendríamos retornos positivos. Ahora veamos el comportamiento para otro mes, que tal Diciembre (12).
1 | returns_of_december = returns.loc[returns.index.month.isin([12])] 2 | boxplot_group_day(returns_of_december);
Interesante, para diciembre no se obtienen mayores variaciones, aunque quizás exista una ventaja estadística operando de Miércoles a Viernes.
Pareciera que se podrían extraer algunas estrategias interesantes luego de estos estudios, antes de continuar veremos otro tipo de estudios, basado en los Mapas de Calor.
¿Qué es un mapa de calor? Un Mapa de Calor, es una técnica de visualización de datos que muestra la magnitud de un fenómeno como color en dos dimensiones. La variación de color puede deberse al tono o la intensidad, lo que proporciona al lector pistas visuales obvias sobre cómo el fenómeno se agrupa o varía en el espacio. Hay dos categorías fundamentalmente diferentes de mapas de calor: el mapa de calor del clúster y el mapa de calor espacial. En un mapa de calor de conglomerados, las magnitudes se presentan en una matriz de tamaño de celda fijo cuyas filas y columnas son fenómenos y categorías discretos, y la clasificación de filas y columnas es intencionada y algo arbitraria, con el objetivo de sugerir conglomerados o representarlos como descubierto mediante análisis estadístico. El tamaño de la celda es arbitrario pero lo suficientemente grande para ser claramente visible. Por el contrario, la posición de una magnitud en un mapa de calor espacial está forzada por la ubicación de la magnitud en ese espacio, y no hay noción de células; se considera que el fenómeno varía continuamente.
1 | md_returns = returns.squeeze().groupby([returns.index.month.rename('month'), 2 | returns.index.dayofweek.rename('day')]).mean().unstack(0) 3 | 4 | fig, ax = plt.subplots(figsize=(16,6)) 5 | sns.heatmap(md_returns, ax=ax, cmap="Spectral");
Pareciera buena estrategia comprar los lunes y vender los jueves del mes de Julio.
A continuación veremos otro tipo de estudio, eliminaremos la componente tendencial de nuestros datos para evitar tener datos con sesgos, muy bien hagámoslo:
1 | window = 253 2 | # detrend tome series by MA 3 | ratesM = data['Adj Close'].rolling(window).mean().dropna() 4 | ratesD = data['Adj Close'].reindex(ratesM.index).sub(ratesM) 5 | 6 | fig, axs = plt.subplots(2, figsize=(16, 8), sharex=True) 7 | data['Adj Close'].plot(ax=axs[0], title="Trend") 8 | ratesM.plot(ax=axs[0]) 9 | ratesD.plot(ax=axs[1], c="g", title="Residuals");
Ahora bien, actualicemos el estudio, vamos otra vez con los meses:
1 | Monthly_Returns = ratesD.groupby([ratesD.index.year.rename('year'), 2 | ratesD.index.month.rename('month')] 3 | ).median().to_frame("Returns") 4 | Monthly_Returns.boxplot(by='month', figsize=(16, 8));
Como podemos observar, ya no queda tan claro que de Julio a Septiembre obtendremos retornos positivos, de hecho ya no están muchas cosas claras, se mantiene que obtengamos posibles incrementos de Noviembre a Diciembre pero no se ve tan claro.
Que podemos hacer en estos casos, bueno busquemos una confirmación más visual, incorporaremos otra librería a nuestro estudio.
Descomposición
Utilizaremos la librería statsmodels para hacer una descomposición estacional y lo graficaremos, de la siguiente manera:
1 | import statsmodels.api as sm 2 | decomposition = sm.tsa.seasonal_decompose(data['Adj Close'], model="additive", period = 253) 3 | 4 | trend = decomposition.trend 5 | seasonal = decomposition.seasonal 6 | residual = decomposition.resid 7 | 8 | fig, axs = plt.subplots(4, figsize=(14, 10), sharex=True) 9 | data['Adj Close'].plot(title='Original', color="blue", ax=axs[0]) 10 | trend.plot(title='Trend', color="red", ax=axs[1]) 11 | seasonal.plot(title='Seasonality', color="orange", ax=axs[2]) 12 | residual.plot(title='Residuals', color="green", ax=axs[3]) 13 | plt.tight_layout();seasonal["2019"].plot(label='Seasonality', color="blue", figsize=(20,8));
¿Que observamos acá? Bueno los precios de cierre de la serie da datos, la componente tendencia, la componente estacional y el residuo de residual que no es más que lo que le falta a la suma de la componente tendencial + la componente estacional para ser igual a la gráfica original, es importante destacar que en este caso es así porque se ha dicho que las componentes son “aditivas”.
Observemos la estacionalidad del año 2019.
1 | seasonal["2019"].plot(label='Seasonality', color="blue", figsize=(20,8));
Queríamos comprobar si de verdad de noviembre a diciembre ha incrementado el precio, bueno pareciera ser que sí.
Hagamos un Zoom a este periodo:
Hagamos un Zoom a este periodo:
1 | seasonal["2019-11":"2019-12"].plot(label='Seasonality', color="blue", figsize=(20,8));
Pues pareciera que a partir del 15 de Noviembre hasta finales de Diciembre el precio recupera 15 puntos de su precio.
Bueno solo para verificar lo que hemos realizado, podemos compararlo con las gráficas que presenta https://charts.equityclock.com actualizada a Diciembre 2019.
Conclusión
Como podrá observar se encuentran cosas interesantes al realizar búsqueda de pautas estacionales, invito al lector aplicar el estudio a diversos activos y se sorprenderá gratamente, al identificar pautas estacionales podemos contar con una ventaja estadística a nuestro favor al momento de abrir operaciones. Invito al lector a que replique el estudio en otros activos.
Para ampliar la información:
Un agradecimiento especial para Antonio Rodríguez (@paduel_py) por el feedback en cuando al desarrollo del código que se presenta en este artículo.
Artículo escrito por Alexander Ríos en colaboración con Quantified Models.