Aprendizaje automático y Data Science (Parte 46): Pronóstico de los mercados bursátiles con N-BEATS en Python
Contenido
- ¿Qué es N-BEATS?
- ¿Cómo funciona N-BEATS?
- Objetivos principales del modelo N-BEATS
- Construyendo el modelo N-BEATS
- Pronóstico fuera de muestra utilizando el modelo N-BEATS
- Pronóstico de series múltiples
- Toma de decisiones de trading utilizando N-BEATS en MetaTrader 5
- Conclusión
¿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:
- 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. - 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). - 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. - 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.
- 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. - 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. - 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:
- 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. - Pueden sufrir un sobreajuste con datos ruidosos.
Al igual que otras redes profundas, N-BEATS puede sufrir sobreajuste con datos ruidosos. - 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. - 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
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Utilizando redes neuronales en MetaTrader
Reimaginando las estrategias clásicas en MQL5 (Parte 14): Análisis de múltiples estrategias
Particularidades del trabajo con números del tipo double 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
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso
Echa un vistazo al nuevo artículo: Ciencia de datos y aprendizaje automático (Parte 46): Previsión de los mercados bursátiles mediante N-BEATS en Python.
Autor: Omega J Msigwa