English Русский 中文 Deutsch 日本語
preview
Aprendizaje automático y Data Science (Parte 46): Pronóstico de los mercados bursátiles con N-BEATS en Python

Aprendizaje automático y Data Science (Parte 46): Pronóstico de los mercados bursátiles con N-BEATS en Python

MetaTrader 5Sistemas comerciales |
66 1
Omega J Msigwa
Omega J Msigwa

Contenido


¿Qué es N-BEATS?

N-BEATS (Neural Basis Expansion Analysis for Time Series) es un modelo de aprendizaje profundo diseñado específicamente para la predicción de series temporales. Proporciona un marco flexible para tareas de pronóstico univariadas y multivariadas.

Fue presentado por investigadores de Element AI (que ahora forma parte de ServiceNow) en 2019, en el artículo N-BEATS: Neural basis expansion analysis for interpretable time series forecasting.

Los desarrolladores de Element AI crearon este modelo para desafiar el predominio de los modelos estadísticos clásicos como ARIMA y ETS, sin renunciar por ello a ciertas capacidades de los modelos clásicos de aprendizaje automático.

Todos sabemos que la predicción de series temporales es una tarea compleja, por lo que los expertos en aprendizaje automático y los usuarios recurren a veces a modelos de aprendizaje profundo como las RNN, las LSTM, etc., que suelen ser:

  • Demasiado complicados para algunas tareas sencillas.
  • Difícil de interpretar.
  • A pesar de su complejidad, no superan sistemáticamente los métodos de referencia estadísticos.

Mientras tanto, los modelos tradicionales para la previsión de series temporales, como ARIMA, suelen ser demasiado simples para muchas tareas.

Así pues, los autores/desarrolladores decidieron crear un modelo de aprendizaje profundo para la predicción de series temporales que funcionara bien, fuera interpretable y no necesitara ajustes específicos del dominio.



Objetivos principales del modelo N-BEATS

Los desarrolladores tenían objetivos y motivaciones claras para crear esta herramienta de aprendizaje automático; su objetivo era abordar las limitaciones de la previsión de series temporales tanto clásicas como basadas en el aprendizaje profundo.

A continuación se presenta una descripción detallada de los objetivos principales de N-BEATS:

  1. Simplicidad del modelo sin sacrificar la precisión

    Dado que los modelos más sencillos o lineales para la predicción de series temporales, como el ARIMA, no son capaces de captar relaciones complejas —que se plasman mejor en los modelos basados en redes neuronales (modelos de aprendizaje profundo),— los desarrolladores decidieron utilizar arquitecturas sencillas de redes neuronales (MLP) para la predicción de series temporales, ya que son más interpretables, más rápidas y más fáciles de depurar.

    El uso de modelos de aprendizaje profundo, como las redes neuronales recurrentes (RNN), las LSTM o los Transformers, añade un nivel de complejidad al sistema, lo que hace que el modelo sea más difícil de ajustar y que su entrenamiento resulte más lento.

  2. Interpretabilidad a través de la estructura

    Dado que los modelos MLP y otros modelos basados en redes neuronales no ofrecen resultados interpretables, los desarrolladores se propusieron crear un modelo basado en redes neuronales capaz de proporcionar pronósticos interpretables para las personas mediante la descomposición de la salida en componentes de tendencia y estacionales, de forma similar a los modelos clásicos de series temporales como el ETS.

    El modelo N-BEATS permite realizar atribuciones claras, por ejemplo: «Este pico en los datos se debe a la tendencia» o «esta caída es estacional»; esto se consigue mediante capas de expansión de bases (como bases polinómicas o de Fourier).

  3. Precisión competitiva sin ajustes específicos para cada ámbito

    Otro de los objetivos que persigue este modelo es diseñar un modelo de uso general que funcione bien con una amplia variedad de series temporales, con un mínimo de ingeniería de características manual.

    Esto se debe a que modelos como Prophet exigen que los usuarios especifiquen los patrones de tendencia y estacionalidad.

    N-BEATS aprende estos patrones de forma automática, directamente a partir de los datos.

  4. Compatibilidad con el modelado global en múltiples series temporales

    Dado que muchos modelos solo pueden pronosticar una serie temporal cada vez, este modelo está diseñado para trabajar con múltiples series temporales a la vez.
    Esto resulta muy práctico porque, en los datos financieros, podemos tener más de una característica que queramos pronosticar. Por ejemplo, pronosticar los precios de cierre del NASDAQ y del S&P 500 al mismo tiempo.
  5. Entrenamiento rápido y escalable

    El objetivo de N-BEATS era lograr que el modelo fuera rápido y fácil de paralelizar, a diferencia de las redes neuronales recurrentes (RNN) o los modelos basados en atención.

  6. Sólido rendimiento de referencia

    N-BEATS pretende superar a los métodos clásicos más avanzados, como ARIMA y ETS, en una evaluación justa y basada en pruebas retrospectivas.

  7. Diseño modular y extensible

    Los modelos clásicos para la previsión de series temporales son estáticos e inmodificables. N-BEATS cuenta con una arquitectura fácil de modificar que permite añadir fácilmente bloques personalizados, como bloques de tendencias, bloques de estacionalidad o bloques genéricos.

Antes de implementar este modelo, dediquemos un breve momento a comprender de qué se trata.


¿Cómo funciona el modelo N-BEATS? (Una breve intuición matemática)

Analicemos la arquitectura del modelo N-BEATS.

Figura 01

En la parte superior, tenemos los datos de series temporales, que se filtran y procesan en varias pilas diferentes, desde 1 hasta M pilas. 

Cada pila se compone de diferentes bloques, del 1 al K; a partir de cada bloque, el modelo produce un valor pronosticado o un residuo, que luego se pasa a la siguiente pila.

Figura 02

Cada bloque está compuesto por cuatro capas de una red neuronal totalmente conectada que generan una reconstrucción (backcast) o un pronóstico (forecast).

El flujo de datos hacia el modelo

01: En las pilas

Al principio, el modelo necesita tener el período de lookback y el período de pronóstico.

El período de análisis retrospectivo se refiere a cuánto nos remontamos al pasado para predecir el futuro, mientras que el período de pronóstico representa cuánto queremos predecir hacia el futuro.

Tras determinar el período de análisis retrospectivo y el período de pronóstico, comenzamos con la pila 01, que toma los datos del período de análisis retrospectivo y comienza a procesarlos para realizar predicciones iniciales. Por ejemplo, si nuestro período de análisis retrospectivo son los precios de cierre de las últimas horas de un determinado instrumento, Pila 01 utiliza estos datos para pronosticar las próximas 24 horas.

La predicción inicial obtenida y sus residuos (es decir, el valor real menos el valor predicho) se pasan a la siguiente pila (Pila 02) para su posterior refinamiento.

Este proceso se repite para todas las pilas subsiguientes hasta la pila M; cada pila mejora las predicciones realizadas por la pila anterior.

Finalmente, las previsiones de todas las pilas se combinan para producir la previsión global. Por ejemplo, si Pila 01 predice un pico, Pila 02 se ajusta a la tendencia y Pila M refina los patrones a largo plazo. El pronóstico global integra todas estas contribuciones para ofrecer la predicción más precisa posible.

Puedes pensar en las pilas como diferentes capas de análisis. La pila 01 podría centrarse en capturar patrones a corto plazo, como las fluctuaciones horarias de los precios de cierre, mientras que la pila 02 podría centrarse en patrones a largo plazo, como las tendencias diarias de los precios de cierre.

Cada pila procesa los datos de entrada para contribuir de forma única al pronóstico general.

02: Dentro de los bloques de entrada

Partiendo del Bloque 01, que recibe la entrada de la pila, esta puede corresponder a los datos históricos originales de la ventana de entrada o a los residuos de la pila anterior. A continuación, utiliza esta entrada para generar un pronóstico (forecast) y una reconstrucción (backcast). Por ejemplo, si el bloque recibe como entrada los datos de consumo eléctrico de las últimas 24 horas, genera el pronóstico para las siguientes 24 horas y la reconstrucción (backcast) para aproximar los datos de entrada.

La reconstrucción (backcast) ayuda a perfeccionar la comprensión del modelo sobre cómo el pronóstico contribuye a las predicciones globales.

De nuevo, cada pila está compuesta por varios bloques que funcionan secuencialmente. Una vez que el primer bloque procesa la entrada de la pila y genera su pronóstico y su reconstrucción (backcast), el siguiente bloque toma como entrada tanto los residuos del bloque anterior como la entrada original de la pila. Estas dos entradas ayudan al bloque actual a realizar pronósticos más precisos que el anterior, al tiempo que tienen en cuenta los datos originales para mejorar la precisión general.

Este refinamiento iterativo dentro de cada bloque de una pila garantiza que las predicciones sean cada vez más precisas a través de los bloques. Una vez que todos los bloques dentro de una pila han procesado los datos, el residuo final del último bloque (Bloque K) se pasa a la siguiente pila.

03: Disección de un bloque

Dentro de cada bloque, los datos de entrada se procesan a través de una pila totalmente conectada de cuatro capas. Esta pila transforma la entrada del bloque extrayendo características que ayudan a generar el análisis retrospectivo y la previsión.

La capa totalmente conectada de cada bloque está diseñada para realizar transformaciones de los datos y extraer características. Después de pasar por las capas totalmente conectadas, los datos se dividen en dos partes (véase la Figura 02). Una parte corresponde al backcast y la otra al forecast.

El resultado del análisis retrospectivo tiene como objetivo aproximar los datos de entrada, lo que ayuda a refinar los residuos que se pasan al siguiente bloque, mientras que el resultado del pronóstico proporciona valores previstos para el período de pronóstico.


Construyendo el modelo N-BEATS en Python

Empieza instalando todos los módulos que aparecen en el archivo requirements.txt que se encuentra en la tabla de archivos adjuntos, al final de este artículo.

pip install -r requirements.txt

En el archivo test.ipynb, empezamos importando todos los módulos necesarios.

import MetaTrader5 as mt5
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
import warnings

sns.set_style("darkgrid")
warnings.filterwarnings("ignore")

A continuación, se inicia MetaTrader 5.

if not mt5.initialize():
    print("Metratrader5 initialization failed, Error code =", mt5.last_error())
    mt5.shutdown()

Recopilamos 1000 barras del marco temporal diario del símbolo NASDAQ (NAS100).

rates = mt5.copy_rates_from_pos("NAS100", mt5.TIMEFRAME_D1, 1, 1000)
rates_df = pd.DataFrame(rates)

A pesar de que este modelo utiliza técnicas propias de las técnicas clásicas de aprendizaje automático, que suelen ser multivariables, N-BEATS adopta un enfoque univariable similar al empleado en los modelos tradicionales de series temporales como ARIMA y VAR.

A continuación se muestra cómo construimos datos univariados.

univariate_df = rates_df[["time", "close"]].copy()
univariate_df["ds"] = pd.to_datetime(univariate_df["time"], unit="s") # convert the time column to datetime
univariate_df["y"] = univariate_df["close"] # closing prices
univariate_df["unique_id"] = "NAS100" # add a unique_id column | very important for univariate models

# Final dataframe
univariate_df = univariate_df[["unique_id", "ds", "y"]].copy()

univariate_df

Resultados.

unique_id ds y
0 NAS100 2021-08-30 9.655648
1 NAS100 2021-08-31 9.654988
2 NAS100 2021-09-01 9.655763
3 NAS100 2021-09-02 9.654981
4 NAS100 2021-09-03 9.658335
... ... ... ...
995 NAS100 2025-07-07 10.028180
996 NAS100 2025-07-08 10.031142
997 NAS100 2025-07-09 10.037376
998 NAS100 2025-07-10 10.036098
999 NAS100 2025-07-11 10.033283
univariate_df["unique_id"] = "NAS100" # add a unique_id column | very important for univariate models

El módulo neuralforecast, que incorpora el modelo N-BEATS, está diseñado para realizar previsiones tanto univariantes como de panel (multiserie). La característica unique_id indica al modelo a qué serie temporal pertenece cada fila. Esto resulta especialmente importante cuando:

  • Estás realizando previsiones sobre varios activos o símbolos (por ejemplo, AAPL, TSLA, MSFT, EURUSD).
  • Quieres entrenar por lotes un único modelo con muchas series temporales.

Esta variable es obligatoria (incluso para una sola serie) debido a los mecanismos internos de agrupación e indexación.

Entrenar este modelo solo requiere unas pocas líneas de código.

from neuralforecast import NeuralForecast 
from neuralforecast.models import NBEATS # Neural Basis Expansion Analysis for Time Series

# Define model and horizon
horizon = 30  # forecast 30 days into the future

model = NeuralForecast(
    models=[NBEATS(h=horizon, # predictive horizon of the model
                   input_size=90, # considered autorregresive inputs (lags), y=[1,2,3,4] input_size=2 -> lags=[1,2].
                   max_steps=100, # maximum number of training steps (epochs)
                   scaler_type='robust', # scaler type for the time series data
                   )], 
    freq='D' # frequency of the time series data
)

# Fit the model
model.fit(df=univariate_df)

Resultados.

Seed set to 1
GPU available: False, used: False
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs

  | Name         | Type          | Params | Mode 
-------------------------------------------------------
0 | loss         | MAE           | 0      | train
1 | padder_train | ConstantPad1d | 0      | train
2 | scaler       | TemporalNorm  | 0      | train
3 | blocks       | ModuleList    | 2.6 M  | train
-------------------------------------------------------
2.6 M     Trainable params
7.3 K     Non-trainable params
2.6 M     Total params
10.541    Total estimated model params size (MB)
31        Modules in train mode
0         Modules in eval mode

Epoch 99: 100%
 1/1 [00:01<00:00,  0.88it/s, v_num=32, train_loss_step=0.259, train_loss_epoch=0.259]
`Trainer.fit` stopped: `max_steps=100` reached.

Podemos visualizar las predicciones y los valores reales en el mismo eje.

forecast = model.predict() # predict future values based on the fitted model

# Merge forecast with original data
plot_df = pd.merge(univariate_df, forecast, on='ds', how='outer') 

plt.figure(figsize=(7,5))
plt.plot(plot_df['ds'], plot_df['y'], label='Actual')
plt.plot(plot_df['ds'], plot_df['NBEATS'], label='Forecast')
plt.axvline(plot_df['ds'].max() - pd.Timedelta(days=horizon), color='gray', linestyle='--')
plt.legend()
plt.title('N-BEATS Forecast')
plt.show()

Resultados.

A continuación se muestra el aspecto del Dataframe combinado.

unique_id_x ds y unique_id_y NBEATS
0 NAS100 2021-08-31   15599.4   NaN NaN
1 NAS100 2021-09-01 15611.5 NaN NaN
2 NAS100 2021-09-02 15599.3 NaN NaN
3 NAS100 2021-09-03 15651.7 NaN NaN
4 NAS100 2021-09-06 15700.4 NaN NaN
... ... ... ... ... ...
1025 NaN 2025-08-09 NaN NAS100 24235.187500  
1026 NaN 2025-08-10 NaN NAS100 24466.316406
1027 NaN 2025-08-11 NaN NAS100 24454.646484
1028 NaN 2025-08-12 NaN NAS100 24405.820312
1029 NaN 2025-08-13 NaN NAS100 24571.919922

Genial, el modelo ha realizado una predicción con 30 días de antelación.

Para fines de evaluación, vamos a entrenar este modelo con un conjunto de datos y probarlo con otro, como siempre hacemos al evaluar cualquier modelo típico de aprendizaje automático.


Pronóstico fuera de muestra utilizando el modelo N-BEATS

Comenzamos dividiendo los datos en dataframes de entrenamiento y de prueba.

split_date = '2024-01-01'  # the split date for training and testing

train_df = univariate_df[univariate_df['ds'] < split_date]
test_df = univariate_df[univariate_df['ds'] >= split_date]

Entrenamos el modelo con el conjunto de datos de entrenamiento.

model = NeuralForecast(
    models=[NBEATS(h=horizon, # predictive horizon of the model
                   input_size=90, # considered autorregresive inputs (lags), y=[1,2,3,4] input_size=2 -> lags=[1,2].
                   max_steps=100, # maximum number of training steps (epochs)
                   scaler_type='robust', # scaler type for the time series data
                   )], 
    freq='D' # frequency of the time series data
)

# Fit the model
model.fit(df=train_df)

Dado que la función predict predice los siguientes "N" días según el horizonte predictivo, para evaluar este modelo con pronósticos fuera de la muestra, tenemos que combinar el Dataframe del resultado previsto con el Dataframe real.

test_forecast = model.predict() # predict future 30 days based on the training data

df_test = pd.merge(test_df, test_forecast, on=['ds', 'unique_id'], how='outer') # merge the test data with the forecast
df_test.dropna(inplace=True) # drop rows with NaN values

df_test

Resultados.

unique_id ds y NBEATS
3 NAS100 2024-01-02   16554.3   16569.835938  
4 NAS100 2024-01-03 16368.1 16596.839844
5 NAS100 2024-01-04 16287.2 16603.513672
6 NAS100 2024-01-05 16307.1 16729.607422
9 NAS100 2024-01-08 16631.0 16854.746094
10 NAS100 2024-01-09 16672.4 16918.466797
11 NAS100 2024-01-10 16804.7 16958.833984
12 NAS100 2024-01-11 16814.3 17130.972656
13 NAS100 2024-01-12 16808.8 17055.396484
16 NAS100 2024-01-15 16828.7 17272.376953
17 NAS100 2024-01-16 16841.9 17227.498047
18 NAS100 2024-01-17 16727.7 17408.158203
19 NAS100 2024-01-18 16987.0 17499.619141
20 NAS100 2024-01-19 17336.7 17318.767578
23 NAS100 2024-01-22 17329.3 17399.562500
24 NAS100 2024-01-23 17426.1 17289.140625
25 NAS100 2024-01-24 17503.1 17236.478516
26 NAS100 2024-01-25 17469.4 17188.691406
27 NAS100 2024-01-26 17390.1 17315.134766


Procedemos a evaluar este resultado.

from sklearn.metrics import mean_absolute_percentage_error, r2_score

mape = mean_absolute_percentage_error(df_test['y'], df_test['NBEATS'])
r2_score_ = r2_score(df_test['y'], df_test['NBEATS'])

print(f"mean_absolute_percentage_error (MAPE): {mape} \n R2 Score: {r2_score_}")

Resultados.

mean_absolute_percentage_error (MAPE): 0.015779373328172166 
R2 Score: 0.35350182943487285

Según la métrica MAPE, las predicciones del modelo son muy precisas en términos porcentuales; mientras tanto, el valor de la puntuación R2 de 0,35 significa que solo se explica el 35% de la variación en la variable objetivo.

A continuación se muestra el gráfico que contiene los valores reales y previstos en un solo eje.

Al igual que cualquier otro modelo de previsión de series temporales, N-BEATS necesita actualizarse periódicamente con nuevos datos secuenciales para seguir siendo relevante y preciso. En los ejemplos anteriores, hemos evaluado el modelo basándonos en las previsiones realizadas con 30 días de antelación sobre los datos diarios, pero esta no es la forma correcta, ya que el modelo omite mucha información diaria intermedia.

Lo correcto es actualizar el modelo con nuevos datos tan pronto como aparezcan.

El modelo N-BEATS proporciona una manera sencilla de actualizar el modelo con nuevos datos sin necesidad de volver a entrenarlo, lo que ahorra mucho tiempo.

Al ejecutar:

NBEATS.predict(df=new_dataframe)

El modelo aplica los pesos entrenados a los nuevos datos mientras realiza una inferencia, lo que actualiza el modelo con nueva información y lo hace relevante para los datos recibidos recientemente de un dataframe.


Pronóstico multiserie 

Tal y como se ha descrito anteriormente, en los objetivos principales de la sección N-BEATS. Este modelo está diseñado para abordar eficazmente el pronóstico multiserie.

Esta es una capacidad impresionante de este modelo, ya que aprovecha los patrones aprendidos en una serie y los utiliza para mejorar el pronóstico general para ambas series temporales.

A continuación se muestra cómo aprovechar esta capacidad:

Comenzamos recopilando datos para cada símbolo de MetaTrader 5.

rates_nq = mt5.copy_rates_from_pos("NAS100", mt5.TIMEFRAME_D1, 1, 1000)
rates_df_nq = pd.DataFrame(rates_nq)

rates_snp = mt5.copy_rates_from_pos("US500", mt5.TIMEFRAME_D1, 1, 1000)
rates_df_snp = pd.DataFrame(rates_snp)

Preparamos cada Dataframe univariado por separado.

# NAS100
rates_df_nq["ds"] = pd.to_datetime(rates_df_nq["time"], unit="s")
rates_df_nq["y"] = rates_df_nq["close"]
rates_df_nq["unique_id"] = "NAS100"
df_nq = rates_df_nq[["unique_id", "ds", "y"]]

# US500
rates_df_snp["ds"] = pd.to_datetime(rates_df_snp["time"], unit="s")
rates_df_snp["y"] = rates_df_snp["close"]
rates_df_snp["unique_id"] = "US500"
df_snp = rates_df_snp[["unique_id", "ds", "y"]]

Combinamos ambos dataframes y ordenamos los valores por unique_id y por fecha (ds).

multivariate_df = pd.concat([df_nq, df_snp], ignore_index=True) # combine both dataframes
multivariate_df = multivariate_df.sort_values(['unique_id', 'ds']).reset_index(drop=True) # sort by unique_id and date

multivariate_df

Resultados.

unique_id ds y
0 NAS100 2021-08-31   15599.4  
1 NAS100 2021-09-01 15611.5
2 NAS100 2021-09-02 15599.3
3 NAS100 2021-09-03 15651.7
4 NAS100 2021-09-06 15700.4
... ... ... ...
1995 US500 2025-07-08 6229.9
1996 US500 2025-07-09 6264.9
1997 US500 2025-07-10 6280.3
1998 US500 2025-07-11 6255.8
1999 US500 2025-07-14 6271.9


Al igual que hicimos anteriormente, hemos dividido los datos en Dataframes de entrenamiento y de prueba.

split_date = '2024-01-01'  # the split date for training and testing

train_df = multivariate_df[multivariate_df['ds'] < split_date] 
test_df = multivariate_df[multivariate_df['ds'] >= split_date]

A continuación, entrenamos el modelo de la misma forma que lo hicimos anteriormente.

from neuralforecast import NeuralForecast 
from neuralforecast.models import NBEATS # Neural Basis Expansion Analysis for Time Series

# Define model and horizon
horizon = 30  # forecast 30 days into the future

model = NeuralForecast(
    models=[NBEATS(h=horizon, # predictive horizon of the model
                   input_size=90, # considered autorregresive inputs (lags), y=[1,2,3,4] input_size=2 -> lags=[1,2].
                   max_steps=100, # maximum number of training steps (epochs)
                   scaler_type='robust', # scaler type for the time series data
                   )], 
    freq='D' # frequency of the time series data
)

# Fit the model
model.fit(df=train_df)

Realizamos previsiones a partir de datos fuera de la muestra.

test_forecast = model.predict() # predict future 30 days based on the training data

df_test = pd.merge(test_df, test_forecast, on=['ds', 'unique_id'], how='outer') # merge the test data with the forecast
df_test.dropna(inplace=True) # drop rows with NaN values

df_test

Resultados.

unique_id ds y NBEATS
6 NAS100 2024-01-02 16554.3 16267.765625
7 US500 2024-01-02 4747.4 4706.230957
8 NAS100 2024-01-03 16368.1 16230.808594
9 US500 2024-01-03 4707.3 4706.517090
10 NAS100 2024-01-04 16287.2 16136.568359
11 US500 2024-01-04 4690.9 4686.380859
12 NAS100 2024-01-05 16307.1 16218.930664
13 US500 2024-01-05 4695.8 4704.896484


Por último, evaluamos el modelo en ambos instrumentos y visualizamos los valores reales y los previstos en el mismo eje.

from sklearn.metrics import mean_absolute_percentage_error, r2_score

unique_ids = df_test['unique_id'].unique()

for unique_id in unique_ids:
    
    df_unique = df_test[df_test['unique_id'] == unique_id].copy()
    
    mape = mean_absolute_percentage_error(df_unique['y'], df_unique['NBEATS'])
    r2_score_ = r2_score(df_unique['y'], df_unique['NBEATS'])

        
    print(f"Unique ID: {unique_id} - MAPE: {mape}, R2 Score: {r2_score_}")
    
    
    plt.figure(figsize=(7, 4))
    plt.plot(df_unique['ds'], df_unique['y'], label='Actual', color='blue')
    plt.plot(df_unique['ds'], df_unique['NBEATS'], label='Forecast', color='orange')
    plt.title(f'Actual vs Forecast for {unique_id}')
    plt.xlabel('Date')
    plt.ylabel('Value')
    plt.legend()
    plt.show()

Resultados.

Unique ID: NAS100 - MAPE: 0.0221775184381915, R2 Score: -0.16976266747298419

Unique ID: US500 - MAPE: 0.007412931117247571, R2 Score: 0.3782229067061038




Toma de decisiones de trading utilizando N-BEATS en MetaTrader 5

Ahora que somos capaces de obtener predicciones a partir de este modelo, podemos integrarlo en un robot de trading basado en Python. 

Dentro del archivo NBEATS-tradingbot.py, empezamos por implementar la función para entrenar inicialmente el modelo completo:

def train_nbeats_model(forecast_horizon: int=30,
                       start_bar: int=1,
                       number_of_bars: int=1000, 
                       input_size: int=90, 
                       max_steps: int=100, 
                       mt5_timeframe: int=mt5.TIMEFRAME_D1,
                       symbol_01: str="NAS100",
                       symbol_02: str="US500",
                       test_size_percentage: float=0.2,
                       scaler_type: str='robust'):
    
    """    
        Train NBEATS model on NAS100 and US500 data from MetaTrader 5.
        
        Args:
            start_bar: starting bar to be used to in CopyRates from MT5
            number_of_bars: The number of bars to extract from MT5 for training the model
            forecast_horizon: the number of days to predict in the future
            input_size: number of previous days to consider for prediction
            max_steps: maximum number of training steps (epochs)
            mt5_timeframe: timeframe to be used for the data extraction from MT5
            symbol_01: unique identifier for the first symbol (default is NAS100)
            symbol_02: unique identifier for the second symbol (default is US500)
            test_size_percentage: percentage of the data to be used for testing (default is 0.2)
            scaler_type: type of scaler to be used for the time series data (default is 'robust')
        
        Returns:
            NBEATS: the n-beats model object
    """
        
    # Getting data from MetaTrader 5

    rates_nq = mt5.copy_rates_from_pos(symbol_01, mt5_timeframe, start_bar, number_of_bars)
    rates_df_nq = pd.DataFrame(rates_nq)

    rates_snp = mt5.copy_rates_from_pos(symbol_02, mt5_timeframe, start_bar, number_of_bars)
    rates_df_snp = pd.DataFrame(rates_snp)

    if rates_df_nq.empty or rates_df_snp.empty:
        print(f"Failed to retrieve data for {symbol_01} or {symbol_02}.")
        return None
    
    # Getting NAS100 data
    rates_df_nq["ds"] = pd.to_datetime(rates_df_nq["time"], unit="s")
    rates_df_nq["y"] = rates_df_nq["close"]
    rates_df_nq["unique_id"] = symbol_01
    df_nq = rates_df_nq[["unique_id", "ds", "y"]]

    # Getting US500 data
    rates_df_snp["ds"] = pd.to_datetime(rates_df_snp["time"], unit="s")
    rates_df_snp["y"] = rates_df_snp["close"]
    rates_df_snp["unique_id"] = symbol_02
    df_snp = rates_df_snp[["unique_id", "ds", "y"]]

    multivariate_df = pd.concat([df_nq, df_snp], ignore_index=True) # combine both dataframes
    multivariate_df = multivariate_df.sort_values(['unique_id', 'ds']).reset_index(drop=True) # sort by unique_id and date

    # Group by unique_id and split per group
    train_df_list = []
    test_df_list = []

    for _, group in multivariate_df.groupby('unique_id'):
        group = group.sort_values('ds')
        split_idx = int(len(group) * (1 - test_size_percentage))

        train_df_list.append(group.iloc[:split_idx])
        test_df_list.append(group.iloc[split_idx:])

    # Concatenate all series
    train_df = pd.concat(train_df_list).reset_index(drop=True)
    test_df = pd.concat(test_df_list).reset_index(drop=True)

    # Define model and horizon

    model = NeuralForecast(
        models=[NBEATS(h=forecast_horizon, # predictive horizon of the model
                    input_size=input_size, # considered autorregresive inputs (lags), y=[1,2,3,4] input_size=2 -> lags=[1,2].
                    max_steps=max_steps, # maximum number of training steps (epochs)
                    scaler_type=scaler_type, # scaler type for the time series data
                    )], 
        freq='D' # frequency of the time series data
    )

    # fit the model on the training data
    
    model.fit(df=train_df)

    test_forecast = model.predict() # predict future 30 days based on the training data

    df_test = pd.merge(test_df, test_forecast, on=['ds', 'unique_id'], how='outer') # merge the test data with the forecast
    df_test.dropna(inplace=True) # drop rows with NaN values


    unique_ids = df_test['unique_id'].unique()
    for unique_id in unique_ids:
        
        df_unique = df_test[df_test['unique_id'] == unique_id].copy()
        
        mape = mean_absolute_percentage_error(df_unique['y'], df_unique['NBEATS'])   
        print(f"Unique ID: {unique_id} - MAPE: {mape:.2f}")
    
    return model

Esta función combina todos los procedimientos de entrenamiento descritos anteriormente y devuelve el objeto del modelo N-BEATS para realizar previsiones directas.

La función para predecir los siguientes valores adopta un enfoque similar al utilizado en la función de entrenamiento.

def predict_next(model, 
                  symbol_unique_id: str, 
                  input_size: int=90):
    
    """
        Predict the next values for a given unique_id using the trained model.
        
        Args:
            model (NBEATS): the trained NBEATS model
            symbol_unique_id (str): unique identifier for the symbol to predict
            input_size (int): number of previous days to consider for prediction
        
        Returns:
            DataFrame: containing the predicted values for the next days
    """
    
    # Getting data from MetaTrader 5

    rates = mt5.copy_rates_from_pos(symbol_unique_id, mt5.TIMEFRAME_D1, 1, input_size * 2)  # Get enough data for prediction
    if rates is None or len(rates) == 0:
        print(f"Failed to retrieve data for {symbol_unique_id}.")
        return pd.DataFrame()
    
    rates_df = pd.DataFrame(rates)
    
    rates_df["ds"] = pd.to_datetime(rates_df["time"], unit="s")
    rates_df = rates_df[["ds", "close"]].rename(columns={"close": "y"})
    rates_df["unique_id"] = symbol_unique_id
    rates_df = rates_df.sort_values(by="ds").reset_index(drop=True)
    
    # Prepare the dataframe for reference & prediction    
    univariate_df = rates_df[["unique_id", "ds", "y"]]
    forecast = model.predict(df=univariate_df)
    
    return forecast

Le damos al modelo el doble de datos que el input_size utilizado durante el entrenamiento — solo para que tenga datos suficientes.

Llamemos a la función «predict» dos veces para cada símbolo por separado y observemos los marcos de datos resultantes.

trained_model = train_nbeats_model(max_steps=10)

print(predict_next(trained_model, "NAS100").head())
print(predict_next(trained_model, "US500").head())

Resultados.

Predicting DataLoader 0: 100%|████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 45.64it/s]
  unique_id         ds        NBEATS
0    NAS100 2025-07-16  22836.160156
1    NAS100 2025-07-17  22931.242188
2    NAS100 2025-07-18  22984.792969
3    NAS100 2025-07-19  23037.224609
4    NAS100 2025-07-20  23119.804688
GPU available: False, used: False
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs
Predicting DataLoader 0: 100%|████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 71.43it/s]
  unique_id         ds       NBEATS
0     US500 2025-07-16  6234.584961
1     US500 2025-07-17  6254.846680
2     US500 2025-07-18  6261.153320
3     US500 2025-07-19  6282.960449
4     US500 2025-07-20  6307.293945
GPU available: False, used: False
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs

Dado que los marcos de datos resultantes para ambos instrumentos contienen múltiples predicciones del precio de cierre diario para los próximos 30 días (la fecha de hoy es el 16 de julio de 2025), debemos seleccionar un valor previsto para el día de hoy (la fecha de hoy).

today = dt.datetime.now().date() # today's date

forecast_df = predict_next(trained_model, "NAS100") # Get the predicted values for NAS100, 30 days into the future
today_pred_close_nq = forecast_df[forecast_df['ds'].dt.date == today]['NBEATS'].values # extract today's predicted close value for NAS100

forecast_df = predict_next(trained_model, "US500") # Get the predicted values for US500, 30 days into the future
today_pred_close_snp = forecast_df[forecast_df['ds'].dt.date == today]['NBEATS'].values # extract today's predicted close value for US500

print(f"Today's predicted NAS100 values:", today_pred_close_nq)
print(f"Today's predicted US500 values:", today_pred_close_snp)

Resultados.

Today's predicted NAS100 values: [22836.16]
Today's predicted US500 values: [6234.585]

Por último, podemos utilizar estos valores previstos en una estrategia de negociación sencilla.

# Trading modules 

from Trade.Trade import CTrade
from Trade.PositionInfo import CPositionInfo
from Trade.SymbolInfo import CSymbolInfo

SLIPPAGE = 100 # points
MAGIC_NUMBER = 15072025 # unique identifier for the trades
TIMEFRAME = mt5.TIMEFRAME_D1 # timeframe for the trades

# Create trade objects for NAS100 and US500
m_trade_nq = CTrade(magic_number=MAGIC_NUMBER,
                 filling_type_symbol = "NAS100",
                 deviation_points=SLIPPAGE)

m_trade_snp = CTrade(magic_number=MAGIC_NUMBER,
                 filling_type_symbol = "US500",
                 deviation_points=SLIPPAGE)

# Training the NBEATS model INITIALLY
trained_model = train_nbeats_model(max_steps=10,
                                    input_size=90,
                                    forecast_horizon=30,
                                    start_bar=1,
                                    number_of_bars=1000,
                                    mt5_timeframe=TIMEFRAME,
                                    symbol_01="NAS100",
                                    symbol_02="US500"
                                   )

m_symbol_nq = CSymbolInfo("NAS100") # Create symbol info object for NAS100
m_symbol_snp = CSymbolInfo("US500") # Create symbol info object for US500

m_position = CPositionInfo() # Create position info object


def pos_exists(pos_type: int, magic: int, symbol: str) -> bool: 
    
    """ Checks whether a position exists given a magic number, symbol, and the position type

    Returns:
        bool: True if a position is found otherwise False
    """
    
    if mt5.positions_total() < 1: # no positions whatsoever
        return False
    
    positions = mt5.positions_get()
    
    for position in positions:
        if m_position.select_position(position):
            if m_position.magic() == magic and m_position.symbol() == symbol and m_position.position_type()==pos_type:
                return True
            
    return False


def RunStrategyandML(trained_model: NBEATS):

    today = dt.datetime.now().date() # today's date

    forecast_df = predict_next(trained_model, "NAS100") # Get the predicted values for NAS100, 30 days into the future
    today_pred_close_nq = forecast_df[forecast_df['ds'].dt.date == today]['NBEATS'].values # extract today's predicted close value for NAS100

    forecast_df = predict_next(trained_model, "US500") # Get the predicted values for US500, 30 days into the future
    today_pred_close_snp = forecast_df[forecast_df['ds'].dt.date == today]['NBEATS'].values # extract today's predicted close value for US500

    # convert numpy arrays to float values
    
    today_pred_close_nq = float(today_pred_close_nq[0]) if len(today_pred_close_nq) > 0 else None
    today_pred_close_snp = float(today_pred_close_snp[0]) if len(today_pred_close_snp) > 0 else None

    print(f"Today's predicted NAS100 values:", today_pred_close_nq)
    print(f"Today's predicted US500 values:", today_pred_close_snp)
    
    # Refreshing the rates for NAS100 and US500 symbols
    
    m_symbol_nq.refresh_rates()
    m_symbol_snp.refresh_rates()
    
    ask_price_nq = m_symbol_nq.ask() # get today's close price for NAS100
    ask_price_snp = m_symbol_snp.ask() # get today's close price for US500

    # Trading operations for the NAS100 symol
        
    if not pos_exists(pos_type=mt5.ORDER_TYPE_BUY, magic=MAGIC_NUMBER, symbol="NAS100"):
        if today_pred_close_nq > ask_price_nq: # if predicted close price for NAS100 is greater than the current ask price
            # Open a buy trade 
            m_trade_nq.buy(volume=m_symbol_nq.lots_min(), 
                            symbol="NAS100",
                            price=m_symbol_nq.ask(),
                            sl=0.0,
                            tp=today_pred_close_nq) # set take profit to the predicted close price
    
    print("ask: ", m_symbol_nq.ask(), "bid: ", m_symbol_nq.bid(), "last: ", ask_price_nq)
    print("tp: ", today_pred_close_nq, "lots: ", m_symbol_nq.lots_min())
    print("istp within range: ", (m_symbol_nq.ask() - today_pred_close_nq) > m_symbol_nq.stops_level())
    
    if not pos_exists(pos_type=mt5.ORDER_TYPE_SELL, magic=MAGIC_NUMBER, symbol="NAS100"):
        if today_pred_close_nq < ask_price_nq: # if predicted close price for NAS100 is less than the current bid price
            m_trade_nq.sell(volume=m_symbol_nq.lots_min(), 
                             symbol="NAS100",
                             price=m_symbol_nq.bid(),
                             sl=0.0,
                             tp=today_pred_close_nq) # set take profit to the predicted close price
    
    
    # Buy and sell operations for the US500 symbol
    
        
    if not pos_exists(pos_type=mt5.ORDER_TYPE_BUY, magic=MAGIC_NUMBER, symbol="US500"):
        if today_pred_close_snp > ask_price_snp: # if the predicted price for US500 is greater than the current ask price
            m_trade_snp.buy(volume=m_symbol_snp.lots_min(), 
                            symbol="US500",
                            price=m_symbol_snp.ask(),
                            sl=0.0,
                            tp=today_pred_close_snp)

    if not pos_exists(pos_type=mt5.ORDER_TYPE_SELL, magic=MAGIC_NUMBER, symbol="US500"):
        if today_pred_close_snp < ask_price_snp: # if the predicted price for US500 is less than the current bid price
            m_trade_snp.sell(volume=m_symbol_snp.lots_min(), 
                             symbol="US500",
                             price=m_symbol_snp.bid(),
                             sl=0.0,
                             tp=today_pred_close_snp)


RunStrategyandML(trained_model=trained_model) # Run the strategy and ML model once to initialize

Resultados.

Se han abierto dos nuevas operaciones.

Por último, podemos programar el proceso de entrenamiento y automatizar el modelo para que realice predicciones y abra operaciones al comienzo de cada día.

# Schedule the strategy to run every day at 00:00
schedule.every().day.at("00:00").do(RunStrategyandML, trained_model=trained_model)

while True:
    
    schedule.run_pending()
    time.sleep(10)


Conclusión

N-BEATS es un potente modelo para el análisis y la predicción de series temporales. Supera a modelos clásicos como ARIMA, VAR, PROPHET, etc., en la misma tarea, ya que se basa en redes neuronales, que destacan por su capacidad para captar patrones complejos.

N-BEATS es una alternativa perfecta para quienes deseen realizar previsiones de series temporales utilizando modelos no tradicionales para este fin.

Me encanta que incluya técnicas de normalización y herramientas de evaluación en su conjunto de herramientas, lo que hace que este modelo sea muy fácil de usar.

Aunque se trata de un modelo razonablemente bueno, al igual que cualquier otro modelo de aprendizaje automático del mundo, presenta algunos inconvenientes que conviene tener en cuenta, entre los que se incluyen:

  1. Están diseñados principalmente para la predicción univariante.
    Como se ha visto anteriormente, solo requieren dos características en el Dataframe de entrenamiento: ds (marca de tiempo) y la variable objetivo, indicada como y. Es similar al modelo PROPHET que se ha comentado anteriormente.

    En los datos financieros, estas dos características no bastan para reflejar la dinámica del mercado.
  2. Pueden sufrir un sobreajuste con datos ruidosos.
    Al igual que otras redes profundas, N-BEATS puede sufrir sobreajuste con datos ruidosos.
  3. Su interpretabilidad es limitada.
    Aunque N-BEATS incluye una descomposición en funciones básicas para facilitar su interpretabilidad, sigue siendo una red neuronal profunda; es menos interpretable que otros modelos de predicción de series temporales, como ARIMA y PROPHET.
  4. Su adopción es menos generalizada en la industria.
    Seguramente nunca hayas oído hablar de este modelo. 

    Aunque obtiene buenos resultados en las pruebas de rendimiento académico, este modelo no se ha generalizado en la comunidad del aprendizaje automático en comparación con otros modelos como ARIMA, XGBoost, LSTM, etc. No encontrarás muchas entradas en Internet que describan este modelo.

Un saludo.


Tabla de archivos adjuntos

Nombre del archivo Descripción y uso
Trade\PositionInfo.py Contiene la clase CPositionInfo de forma similar a la disponible en MQL5; esta clase proporciona información sobre todas las posiciones abiertas en MetaTrader 5.
Trade\SymbolInfo.py Contiene la clase CSymbolInfo de forma similar a la disponible en MQL5; esta clase proporciona toda la información sobre el símbolo seleccionado de MetaTrader 5.
Trade\Trade.py Contiene la clase CTrade, similar a la disponible en MQL5; esta clase proporciona funciones para abrir y cerrar operaciones en MetaTrader 5.
error_description.py Incluye funciones para convertir los códigos de error de MetaTrader 5 en información legible para el usuario.
NBEATS-Tradingbot.py Un script de Python que utiliza el modelo N-BEATS para tomar decisiones de trading.
test.ipynb Un cuaderno Jupyter para experimentar con el modelo N-BEATS.
requirements.txt  Contiene todas las dependencias de Python utilizadas en este proyecto. 


Fuentes y referencias

Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/18242

Archivos adjuntos |
Attachments.zip (125.7 KB)
nevar
nevar | 21 jul 2025 en 11:26
Muy buen artículo, gracias, Omega.
Dado que utiliza la transformada rápida de Fourier para la descomposición, lo que permite al modelo captar por separado tanto la estacionalidad a corto plazo como las tendencias a largo plazo, ¿es adecuado utilizar el propio precio de cierre como entrada o salida para el algoritmo N-BEATS?

Utilizando redes neuronales en MetaTrader Utilizando redes neuronales en MetaTrader
En el artículo se muestra la aplicación de las redes neuronales en los programas de MQL, usando la biblioteca de libre difusión FANN. Usando como ejemplo una estrategia que utiliza el indicador MACD se ha construido un experto que usa el filtrado con red neuronal de las operaciones. Dicho filtrado ha mejorado las características del sistema comercial.
Reimaginando las estrategias clásicas en MQL5 (Parte 14): Análisis de múltiples estrategias Reimaginando las estrategias clásicas en MQL5 (Parte 14): Análisis de múltiples estrategias
En este artículo, continuamos nuestro análisis sobre cómo crear un conjunto de estrategias de trading y utilizar el optimizador genético de MT5 para ajustar los parámetros de las estrategias. Hoy hemos analizado los datos en Python, demostrando que nuestro modelo podía predecir mejor qué estrategia obtendría mejores resultados, logrando una mayor precisión que al pronosticar directamente la rentabilidad del mercado. Sin embargo, cuando probamos nuestra aplicación con sus modelos estadísticos, nuestros niveles de rendimiento se desplomaron. Posteriormente descubrimos que, lamentablemente, el optimizador genético favorecía estrategias muy correlacionadas, lo que nos llevó a revisar nuestro método para mantener fijas las ponderaciones de los votos y centrar la optimización en los ajustes de los indicadores.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 32): Motor de reconocimiento de velas japonesas en Python (II) — Detección con TA-Lib Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 32): Motor de reconocimiento de velas japonesas en Python (II) — Detección con TA-Lib
En este artículo, hemos pasado de codificar manualmente la detección de patrones de velas japonesas en Python a aprovechar TA-Lib, una biblioteca que reconoce más de sesenta patrones distintos. Estas formaciones ofrecen información valiosa sobre posibles cambios de tendencia en el mercado y continuaciones de tendencias. Sigue leyendo para obtener más información.