Es sobradamente conocido el efecto de la estacionalidad en determinados activos. A fin de cuentas, existen factores estacionales que pueden influir en el precio. Por ejemplo:
- Incremento del consumo de gas natural en invierno
- Calendarios de siembra y cosecha en el trigo, el maíz o la soja
- Incremento en las ventas en ciertos periodos del año (Amazon durante la época navideña).
En el caso de tener la posibilidad de calcular estos periodos, podríamos disponer de una ventaja estadística que permita sincronizar la exposición al activo en aquellos periodos históricamente más favorables.
Para determinar los patrones de estacionalidad vamos a utilizar la librería Prophet en Python. Esta librería está desarrollada por Meta y permite la modelización de series temporales descomponiendo el modelo en tres factores;
- Un factor tendencial (g)
- Un factor estacional (s)
- El error residual (ε)
Lo que podría representarse de la siguiente forma:
y(t) = g(t)+s(t)+ε(t)
El uso de la librería permite la descomposición del modelo en cada una de sus tres fases a partir de un periodo de muestra, con lo que podemos obtener:
- Un rango de predicción del movimiento del precio en caso de mantenerse los factores calculados.
- Un factor de estacionalidad representado para días de la semana, días del mes o días del año.
En este artículo vamos a centrarnos en este último punto, el factor de estacionalidad s(t). Es decir, la parte del movimiento del precio que el modelo considera como estacional.
La propia librería se encarga de modelizar el movimiento a partir de series de Fourier. Es decir, generar una función periódica basada en combinaciones de senos y cosenos que aproxima el componente estacional separándolo de la tendencia estructural.
EL ORO Y EL S&P 500 COMO ACTIVOS TENDENCIALES CON CARÁCTER ESTACIONAL
Vamos a trabajar con dos activos muy concretos, como son el Oro y el índice S&P 500, los cuales no se caracterizan por ser activos cíclicos, sino más bien tendenciales y fundamentalmente alcistas a largo plazo.
Esto implica que si aplicamos el modelo Prophet el peso del componente tendencial será mucho mayor que el estacional. Pero a pesar de ello, que el factor estacional aporte menos al modelo, no implica que no exista una ventaja estadística en operar ciertos periodos de la semana, del mes o del año.
MODELIZACIÓN CON EL ORO
Vamos a descargar los datos del futuro del oro a través de Yahoo Finance a un dataframe y usar la librería Prophet para realizar el modelo.
Descomponemos el dataframe en dos periodos, uno para entrenar el modelo (df_train) y otro para la predicción (df_test), si bien, nuestro objetivo en este informe no será tratar de acertar el comportamiento del oro, sino únicamente descomponer su parte estacional.
Posteriormente creamos el modelo en una variable model y le fijamos el periodo de estacionalidad, así como el orden de la descomposición de Fourier.
En nuestro caso estamos buscando patrones estacionales de un periodo corto de duración por lo que debemos seleccionar monthly como parámetro principal. En caso de que buscar patrones estacionales en días determinados de la semana este ajuste sería a daily.
Vamos a fijar también un periodo de unos 30 días de media. Eso no implica que esa vaya a ser la duración real de los periodos calculados pero nos indica una duración estimada de la estacionalidad.
Por otra parte, el orden de la transformada de Fourier nos da la complejidad del modelo según el número de armónicos empleado. Un valor demasiado bajo nos dará una curva muy suavizada y un valor demasiado alto una curva más nerviosa y más sobreajustada. Consideramos un valor de entre 5 el adecuado para el cálculo de este tipo de estacionalidad.
Simplemente ejecutando la función plot_components podremos tener el desarrollo gráfico de los componentes del modelo.
# SMQuantum - Uso de la libería Prophet
import pandas as pd
from prophet import Prophet
import yfinance as yf
import matplotlib.pyplot as plt
YEAR = 2020 # PONER AÑO HASTA DONDE ANALIZAMOS LA MUESTRA
# === DESCARGAR DATOS DE FUTUROS DEL ORO DESDE YAHOO FINANCE ===
ticker = "GC=F" # Gold Futures
df = yf.download(ticker, start="2000-01-01", progress=False)
# Limpiamos el DataFrame
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
# === PREPARAR DATAFRAME PARA PROPHET ===
df = df.reset_index()
df.rename(columns={"Date": "ds", "Close": "y"}, inplace=True)
df = df[["ds", "y"]].dropna()
df["ds"] = pd.to_datetime(df["ds"])
# === FILTRAR DATOS HASTA YEAR PARA ENTRENAMIENTO ===
df_train = df[df["ds"].dt.year < YEAR]
df_test = df[df["ds"].dt.year >= YEAR]
# === ENTRENAR MODELO PROPHET ===
model = Prophet()
model.add_seasonality(name='monthly', period=30, fourier_order=5)
model.fit(df_train)
# === CREAR PREDICCIÓN ===
future = model.make_future_dataframe(periods=365)
forecast = model.predict(future)
# === MANTENER PLOT DE COMPONENTES ===
model.plot_components(forecast)
plt.tight_layout()
plt.show()
Si ejecutamos el modelo en Python con datos desde el 1 de enero del año 2000 hasta el 31 de diciembre del 2019 la librería Prophet nos da la siguiente modelización.

En el gráfico vemos como además de la tendencia se muestra la representación en series de Fourier de la estacionalidad en diferentes componentes:
- Semanal (los valores estimados para fines de semana deben interpretarse como extrapolaciones matemáticas al no existir cotización real).
- Anual, que es donde vamos a centrarnos en nuestra operativa.
- Mensual, con un mejor funcionamiento la primera quincena de cada mes.
Para este caso vemos como existe una tendencia a que el precio del oro comience a subir desde mediados de diciembre hasta finales de febrero, así como durante el mes de julio y agosto. El resto del año el componente estacional muestra una desviación media negativa respecto a la tendencia.
MODELIZACIÓN CON EL S&P500
Si hacemos lo propio con el índice S&P500 modificando el símbolo por “^GSPC” para descargar los datos desde Yahoo Finance debemos modificar las siguientes líneas de código:
# === DESCARGAR DATOS DE FUTUROS DEL SP500 DESDE YAHOO FINANCE ===
ticker = "^GSPC" # S&P 500 Index
df = yf.download(ticker, start="2000-01-01", progress=False)
El resto de código lo mantenemos igual y obtenemos la siguiente gráfica:

Vemos como en este caso parece que hay una ventaja estadística mayor en operar los martes y a mediados de mes. No obstante, en nuestra estrategia vamos a centrarnos en la estacionalidad mensual con un mejor rendimiento desde mediados de octubre hasta mayo, en línea con el dicho “Sell in may and go away”.
EXPORTAR DATOS A UN CSV
Ahora vamos a exportar los datos obtenidos a un archivo csv que nos guarde para cada día del año el valor de descomposición de Fourier que el modelo había calculado. Esto nos permitirá más adelante poder importar estos datos a un indicador gráfico en AmiBroker para visualizar y testear el modelo.
# === EXPORTAR ESTACIONALIDAD ANUAL A CSV (POR DÍA DEL AÑO) ===
path = "C:/SMQuantum/"
symbol = "$GC"
#symbol = "$SPX"
# Día del año
forecast["day_of_year"] = forecast["ds"].dt.dayofyear
by_dayofyear = forecast.groupby("day_of_year")["yearly"].mean().reset_index()
by_dayofyear.to_csv(path+symbol+"-"+"yearly.csv", index=False)
ESTRATEGIA EN AMIBROKER
El modelo se entrena exclusivamente con datos hasta 2019. A partir de ese punto no se recalibran los parámetros, utilizando la estacionalidad estimada como patrón fijo para evaluar su robustez fuera de muestra.
Ahora podemos considerar la estrategia con las siguientes reglas:
- Repartiremos el 50% del capital disponible a cada uno de los dos activos.
- Entraremos en largo en un activo justo cuando el indicador de estacionalidad alcanza un mínimo (circulo verde) para salir en un máximo (círculo rojo)
- Establecemos una separación mínima entre máximos o mínimos por lo que en caso de tener, por ejemplo, dos máximos muy seguidos nos quedaremos únicamente con el último.
Para el indicador gráfico en AmiBroker establecemos un bucle vela a vela y hacemos corresponder cada día del año con su correspondiente valor importando del archivo csv.


A continuación, desarrollamos el código en AmiBroker que nos permita visualizar el indicador y testear nuestra estrategia en el periodo completo en timeframe diario.
/* SMQuantum
Indicador de Estacionalidad en periodos anuales
23/02/2025
*/
//Directorio donde tendremos nuestros archivos
path= "C:\\SMQuantum\\";
// Separacion para considerar un maximo o un mínimo en el patrón estacional de Fourier
separacion=10;
// Stop loss en porcentaje
SL=9;
//Configuracion del sistema
SetOption("initialequity", 100000); // Capital inicial
SetOption("MaxOpenPositions", 2); // Capital inicial
SetTradeDelays(1,1,1,1);
BuyPrice=SellPrice=ShortPrice=CoverPrice=O;
// Definimos el nombre de los simbolos adaptados a la base de datos de AmiBroker
sym1 = "$SPX";
sym2 = "$GC";
// Cargamos los archivos con los datos de estacionalidad de cada archivo
fh1 = fopen( path+sym1+"-yearly.csv", "r" );
days1="";
fh2 = fopen( path+sym2+"-yearly.csv", "r" );
days2="";
contador=0;
if( fh1 )
{
while( ! feof( fh1 ) )
{
line = fgets( fh1 );
value=StrExtract( line, 1 );
if (contador>0) days1 = days1 + "," + value;
else days1 = days1 + value;
contador++;
}
fclose(fh1);
}
contador=0;
if( fh2 )
{
while( ! feof( fh2 ) )
{
line = fgets( fh2 );
value=StrExtract( line, 1 );
if (contador>0) days2 = days2 + "," + value;
else days2 = days2 + value;
contador++;
}
fclose(fh2);
}
doy = DayOfYear();
//Bucle para calcular el indicador según el modo seleccionado
// A cada día del año proporcionamos el valor correspondiente importado del csv
for ( i = 0; i <= BarCount-1; i++ )
{
estacionalidad1[i]=StrToNum(StrExtract(days1,doy[i]-1));
estacionalidad2[i]=StrToNum(StrExtract(days2,doy[i]-1));
}
// **************** S&P500 *********************************************
// Establecemos maximos y minimos genericos en el indicador
maximo=estacionalidad1>Ref(estacionalidad1,-1) AND estacionalidad1>Ref(estacionalidad1,1);
minimo=estacionalidad1<Ref(estacionalidad1,-1) AND estacionalidad1<Ref(estacionalidad1,1);
// Solo nos quedamos con los maximos y minimos absolutos. Evitamos salir en un maximo intermedio (separacion minima)
maximosp500 = maximo AND estacionalidad1>Ref(HHV(estacionalidad1,separacion),-1) AND estacionalidad1>Ref(HHV(estacionalidad1,separacion),separacion);
minimosp500 = minimo AND estacionalidad1<Ref(LLV(estacionalidad1,separacion),-1) AND estacionalidad1<Ref(LLV(estacionalidad1,separacion),separacion);
// Array con el estado alcista
alcistasp500 = Flip(minimosp500,maximosp500);
// Definimos minimo de entrada y maximo de salida en funcion de si estamos en largo
maximook_sp500 = maximosp500 AND Ref(alcistasp500,-1);
minimook_sp500 = minimosp500 AND Ref(alcistasp500,-1)==False;
// **************** ORO *********************************************
maximo_oro=estacionalidad2>Ref(estacionalidad2,-1) AND estacionalidad2>Ref(estacionalidad2,1);
minimo_oro=estacionalidad2<Ref(estacionalidad2,-1) AND estacionalidad2<Ref(estacionalidad2,1);
maximo1_oro=maximo_oro AND estacionalidad2>Ref(HHV(estacionalidad2,separacion),-1) AND estacionalidad2>Ref(HHV(estacionalidad2,separacion),separacion);
minimo1_oro=minimo_oro AND estacionalidad2<Ref(LLV(estacionalidad2,separacion),-1) AND estacionalidad2<Ref(LLV(estacionalidad2,separacion),separacion);
alcista_oro=Flip(minimo_oro,maximo_oro);
maximook_oro=maximo1_oro AND Ref(alcista_oro,-1);
minimook_oro=minimo1_oro AND Ref(alcista_oro,-1)==False;
//SISTEMA TRADING
buy1 = (minimook_sp500 AND Name()==sym1);
buy2 = (minimook_oro AND Name()==sym2);
Buy= buy1 OR buy2;
Sell=(maximook_sp500 AND Name()==sym1) OR (maximook_oro AND Name()==sym2);
SetPositionSize(50,spsPercentOfEquity);
ApplyStop(stopTypeLoss,stopModePercent,SL,True);
// SALIDAS GRÁFICAS
Title= "ESTACIONALIDAD "+ FullName();
// REPRESENTACIӎ GRFICA
SetChartOptions( 0, chartShowDates );
Plot(IIf(Name()==sym1,estacionalidad1,estacionalidad2),"Estacionalidad",colorBlueGrey,styleLine);
PlotShapes(IIf(IIf(Name()==sym1,maximook_sp500,maximook_oro),shapehollowCircle,shapeNone),colorRed,0,IIf(Name()==sym1,estacionalidad1,estacionalidad2),0);
PlotShapes(IIf(IIf(Name()==sym1,minimook_sp500,minimook_oro),shapehollowCircle,shapeNone),colorGreen,0,IIf(Name()==sym1,estacionalidad1,estacionalidad2),0);
Los resultados que obtenemos serían los siguientes:

Teniendo en cuenta que el modelo se ha entrenado con datos hasta el 31 de diciembre de 2019 podemos representar únicamente los resultados en el periodo fuera de muestra, lo que arrojaría los siguientes resultados:

CONCLUSIÓN
Como podemos ver en los resultados anteriores, la estrategia sigue funcionando bien en el periodo fuera de muestra manteniendo una alta rentabilidad anual con un drawdown controlado y una exposición al mercado de aproximadamente un 44%.
Los resultados obtenidos muestran que, incluso en activos estructuralmente tendenciales como el oro y el S&P500, es posible identificar un componente estacional cuantificable mediante la descomposición de Fourier propuesta por la librería Prophet.
Un saludo,
Sergio Meana
SMQuantum






