Машинное обучение и Data Science (Часть 42): Прогнозирование фондовых рынков с использованием N-BEATS в Python
Содержание
- Что такое N-BEATS?
- Как работает N-BEATS
- Основные цели модели N-BEATS
- Построение модели N-BEATS
- Прогнозирование вне выборки с использованием модели N-BEATS
- Прогнозирование множественных серий
- Торговые сигналы на основе N-BEATS в MetaTrader 5
- Заключение
Что такое N-BEATS?
N-BEATS (Neural Basis Expansion Analysis for Time Series) — это модель глубокого обучения, специально разработанная для прогнозирования временных рядов. Она обеспечивает гибкую архитектуру для задач прогнозирования как одномерных, так и многомерных временных рядов.
Модель была представлена исследователями из Element AI (ныне часть ServiceNow) в 2019 году в статье N-BEATS: Neural basis expansion analysis for interpretable time series forecasting.
Разработчики Element AI создали эту модель, чтобы бросить вызов доминированию классических статистических моделей, таких как ARIMA и ETS, в задачах прогнозирования временных рядов, сохранив при этом возможности, предоставляемые классическими моделями машинного обучения.
Мы знаем, что прогнозирование временных рядов — это сложная задача, поэтому эксперты по машинному обучению и пользователи иногда обращаются к моделям глубокого обучения, таким как RNN, LSTM и др., которые часто:
- Сложны для простых задач
- Трудны для интерпретации
- Не всегда превосходят статистические базовые модели, несмотря на свою сложность
В то же время традиционные модели прогнозирования временных рядов, такие как ARIMA, зачастую слишком просты для многих задач.
Поэтому авторы решили создать модель глубокого обучения для прогнозирования временных рядов, которая хорошо работает, интерпретируема и не требует специфической настройки под конкретную область.

Основные цели модели N-BEATS
Разработчики преследовали конкретные цели при создании этой модели машинного обучения, стремясь устранить ограничения как классических, так и основанных на глубоких нейронных сетях моделей прогнозирования временных рядов.
Ниже приведено подробное описание ключевых целей N-BEATS:
- Простота модели без ущерба для точности
Поскольку более простые линейные модели прогнозирования временных рядов, такие как ARIMA, не способны улавливать сложные зависимости, которые лучше выявляются моделями глубокого обучения, разработчики решили использовать простые архитектуры нейронных сетей (MLP) для прогнозирования временных рядов. Они более интерпретируемы, быстрее работают и проще в отладке.
Использование глубоких моделей, таких как RNN, LSTM или Transformers, добавляет дополнительный уровень сложности, делая модель труднее настраиваемой и более медленной при обучении. - Интерпретируемость через структуру
Поскольку MLP и другие нейронные модели не предоставляют интерпретируемых результатов, разработчики стремились создать модель, способную давать человеко-интерпретируемые прогнозы, разлагая выход на компоненты тренда и сезонности, аналогично классическим моделям временных рядов, таким как ETS.
Модель N-BEATS позволяет явно объяснять изменения в данных, например: "Этот скачок в данных вызван трендом" или "Это падение носит сезонный характер". Это достигается через слои расширения базиса (например, полиномиальный или Фурье-базис). - Конкурентная точность без специфических настроек
Модель нацелена на создание универсальной модели, которая эффективно работает с широким спектром временных рядов и требует минимального ручного инженерного вмешательства.
Модели вроде Prophet требуют от пользователя задания шаблонов тренда и сезонности.
N-BEATS автоматически изучает эти закономерности напрямую из данных. - Поддержка глобального моделирования множества временных рядов
Многие модели прогнозирования временных рядов работают с одной серией данных за раз (панельные данные). N-BEATS разработан для прогнозирования нескольких временных рядов одновременно.Это особенно важно для финансовых данных, когда необходимо прогнозировать более одного показателя, например, цены закрытия NASDAQ и S&P 500 одновременно.
- Быстрое и масштабируемое обучение
Модель N-BEATS спроектирована так, чтобы обучение было быстрым и легко параллелизируемым, в отличие от RNN или моделей на основе внимания. - Высокая производительность относительно базовых методов
N-BEATS стремится превзойти современные классические методы, такие как ARIMA и ETS, в честной бэктестинговой оценке. - Модульная и расширяемая архитектура
Классические модели прогнозирования временных рядов статичны и не поддаются модификации. N-BEATS предлагает легко модифицируемую архитектуру, позволяя добавлять собственные блоки: блоки тренда, сезонные блоки или универсальные блоки.
Перед реализацией модели кратко разберем, как она работает.
Как работает модель N-BEATS (краткая математическая интуиция)
Рассмотрим архитектуру модели N-BEATS.

Рисунок 01
Вверху находится временной ряд, который фильтруется и обрабатывается в несколько стеков от 1 до M.
Каждый стек состоит из блоков от 1 до K. Каждый блок выдает прогнозное значение или остаток, который передается следующему стеку.

Рисунок 02
Каждый блок состоит из четырех полностью связанных слоев нейронной сети, которые генерируют либо backcast, либо forecast.
Поток данных в модели
01: В стеках
Сначала модель определяет период исторических данных (lookback period) и период прогнозирования (forecast period).
Lookback period — сколько времени в прошлом анализируется для прогнозирования будущего, Forecast period — на какой период нужно сделать прогноз.
После задания этих периодов начинается работа со стеком 01, который обрабатывает данные lookback period для первичных прогнозов. Например, если lookback period — это цены закрытия за последние часы, стек 01 использует эти данные для прогнозирования следующих 24 часов.
Первичный прогноз и его остатки (разница между фактическим и прогнозным значением) передаются в следующий стек (стек 02) для уточнения.
Процесс повторяется для всех последующих стеков до стека M, каждый стек улучшает прогнозы предыдущего.
В конце прогнозы всех стеков объединяются для глобального прогноза. Например, стек 01 прогнозирует всплеск, стек 02 корректирует тренд, а стек M уточняет долгосрочные закономерности. Глобальный прогноз интегрирует все эти данные для максимальной точности.
Стек можно рассматривать как слой анализа. Стек 01 отвечает за краткосрочные колебания (например, почасовые изменения цен закрытия), стек 02 — за долгосрочные паттерны (например, дневные тенденции).
Каждый стек вносит уникальный вклад в общий прогноз.
02: Внутри входных блоков
Блок 01 принимает в параметрах стек, который может быть исходными данными за предыдущий период или остатками предыдущего стека. Далее блок генерирует прогноз (forecast) и обратный прогноз (backcast). Например, если блок получил данные по потреблению электроэнергии за последние 24 часа, он выдает прогноз на следующие 24 часа и обратный прогноз для приближения исходных данных.
Обратный прогноз позволяет уточнить, как прогноз влияет на общие предсказания.
Каждый стек состоит из нескольких блоков, работающих последовательно: после обработки входа блока 1 и генерации прогноза и обратного прогноза, следующий блок принимает остатки предыдущего блока и исходные данные стека. Это позволяет текущему блоку делать более точные прогнозы, учитывая как предыдущие предсказания, так и оригинальные данные.
Итеративное уточнение внутри блоков каждого стека обеспечивает постепенное повышение точности. После обработки всех блоков стека остаток последнего блока (Block K) передается следующему стеку.
03: Разбор блока
Внутри каждого блока данные проходят через четыре слоя полностью связанных нейронных сетей, которые извлекают признаки, необходимые для генерации прогноза и обратного прогноза.
Полносвязный слой внутри каждого блока обрабатывает преобразования данных и извлекает признаки. После прохождения через эти слои данные разделяются на две части (см. Рисунок 02): одна для обратного прогноза, другая — для прогноза.
Обратный прогноз (backcast) приближает входные данные и уточняет остатки, передаваемые следующему блоку, а прогноз (forecast) выдает прогнозные значения для периода прогнозирования.
Построение модели N-BEATS на Python
Начнем с установки всех модулей, указанных в файле requirements.txt, который описан в таблице вложений и приложен в конце статьи.
pip install -r requirements.txt
В файле test.ipynb мы начинаем с импорта всех необходимых модулей.
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")
Затем выполняется инициализация MetaTrader 5.
if not mt5.initialize(): print("Metratrader5 initialization failed, Error code =", mt5.last_error()) mt5.shutdown()
Мы собираем 1000 баров из дневного таймфрейма по инструменту NASDAQ (символ NAS100).
rates = mt5.copy_rates_from_pos("NAS100", mt5.TIMEFRAME_D1, 1, 1000) rates_df = pd.DataFrame(rates)
Несмотря на то что эта модель использует подходы, характерные для классических методов машинного обучения, которые часто бывают многомерными, N-BEATS применяет одномерный подход, аналогичный традиционным моделям временных рядов, таким как ARIMA и VAR.
Ниже показан процесс построения одномерных данных.
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
Результаты.
| 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
Модуль neuralforecast, содержащий модель N-BEATS, предназначен для работы как с одномерными, так и с панельными (многосерийными) прогнозами. Параметр unique_id указывает модели, к какому временно́му ряду принадлежит каждая строка. Это особенно важно, когда:
- Вы прогнозируете несколько активов или символов (например, AAPL, TSLA, MSFT, EURUSD).
- Вы хотите обучить одну модель на множестве временных рядов пакетно.
Этот параметр обязателен (даже для одного ряда) из-за внутренних механизмов группировки и индексации.
Обучение модели занимает всего несколько строк кода.
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)
Результаты.
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.
Мы можем визуализировать прогнозные и фактические значения на одной оси.
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()
Результаты.

Ниже показан вид объединенного датафрейма.
| 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 |
Отлично, модель спрогнозировала 30 дней в будущем.
Для оценки модели стандартно разделим данные: обучим на одном наборе данных и протестируем на другом.
Прогнозирование вне выборки с использованием модели N-BEATS
Сначала разделим данные на обучающую и тестовую выборку.
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]
Обучаем модель на обучающем наборе данных.
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)
Поскольку функция predict прогнозирует следующие N дней вперед в соответствии с горизонтом прогнозирования, для оценки модели на вневыборочных прогнозах необходимо объединить прогнозную выборку с датафреймом фактических данных.
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
Результаты.
| 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 |
Перейдем к оценке результата.
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_}")
Результаты.
mean_absolute_percentage_error (MAPE): 0.015779373328172166 R2 Score: 0.35350182943487285
Согласно метрике MAPE, прогнозы модели очень точны в процентном выражении; при этом значение R2 = 0.35 означает, что лишь 35% вариации целевой переменной объясняется моделью.
Ниже приведен график с фактическими и прогнозными значениями на одной оси.

Как и любая модель прогнозирования временных рядов, N-BEATS требует регулярного обновления новыми последовательными данными для поддержания актуальности и точности. В предыдущих примерах мы оценивали модель на основе прогнозов на 30 дней вперед по дневным данным, однако это не совсем корректно, так как модель пропускает много ежедневной информации между прогнозами.
Правильный подход — обновлять модель по мере поступления новых данных.
Модель N-BEATS предоставляет удобный способ обновления модели без полного переобучения, что экономит значительное время.
При выполнении:
NBEATS.predict(df=new_dataframe) Модель применяет обученные веса к новым данным, выполняя инференс, что обновляет модель и делает прогнозы актуальными для недавно поступивших данных из выборки.
Прогнозирование нескольких временных рядов
Как было описано ранее в разделе Основные цели N-BEATS, модель разработана для эффективного многосерийного прогнозирования.
Эта способность особенно впечатляет, так как модель использует закономерности, выученные на одном ряде, для улучшения общего прогноза по другим рядам.
Посмотрим, как можно использовать эту возможность.
Сначала собираем данные по каждому инструменту из 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)
Подготавливаем отдельный одномерный набор данных для каждого символа.
# 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"]]
Объединяем обе выборки и сортируем значения по столбцу с датой и unique_id.
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
Результаты.
| 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 |
Как и ранее, разделяем данные на обучающую и тестовую выборку.
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]
Далее обучаем модель.
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)
Делаем прогнозы на основе данных, не вошедших в обучающую выборку.
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
Результаты.
| 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 |
Наконец, оцениваем модель для обоих инструментов и визуализируем фактические и прогнозные значения на одной оси.
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()
Результаты.
Unique ID: NAS100 - MAPE: 0.0221775184381915, R2 Score: -0.16976266747298419

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

Торговые сигналы на основе N-BEATS в MetaTrader 5
Мы теперь можем получать прогнозы из модели N-BEATS. Поэтому интегрируем ее в торгового робота на Python.
В файле NBEATS-tradingbot.py начинаем с реализации функции для первоначального обучения всей модели:
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
Эта функция объединяет все процедуры обучения, описанные ранее, и возвращает объект модели N-BEATS для прямого прогнозирования.
Функция прогнозирования следующих значений использует подход, аналогичный функции обучения.
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
Мы подаем модели данные в размере вдвое большем, чем input_size, использованный при обучении, чтобы дать модели достаточно информации.
Вызовем функцию predict дважды для каждого отдельного символа и посмотрим на полученные данные.
trained_model = train_nbeats_model(max_steps=10) print(predict_next(trained_model, "NAS100").head()) print(predict_next(trained_model, "US500").head())
Результаты.
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
Полученные данные для обоих символов содержат прогнозы цены закрытия на 30 дней вперед (текущая дата — 16.07.2025), и нам необходимо выбрать значение, предсказанное именно для текущего дня.
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)
Результаты.
Today's predicted NAS100 values: [22836.16] Today's predicted US500 values: [6234.585]
Наконец, эти прогнозные значения можно использовать в простой торговой стратегии.
# 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
Результаты.

Были открыты две новые сделки.
Наконец, мы можем запланировать процесс обучения и автоматизировать модель, чтобы она делала прогнозы и открывала сделки в начале каждого дня.
# 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)
Заключение
N-BEATS — это эффективная модель для анализа и прогнозирования временных рядов. Она превосходит классические модели, такие как ARIMA, VAR, PROPHET и др., поскольку использует нейронные сети, которые отлично справляются с выявлением сложных закономерностей.
N-BEATS является хорошей альтернативой для тех, кто хочет прогнозировать временные ряды с использованием нетрадиционных подходов.
Мне нравится, что модель включает техники нормализации и инструменты для оценки, что делает ее удобной в использовании.
Однако, как и любая модель машинного обучения, N-BEATS имеет свои ограничения, о которых следует помнить:
- Прежде всего предназначена для одномерного прогнозирования
Как показано ранее, для обучения модели требуется всего два признака: ds (дата) и целевая переменная y, аналогично модели PROPHET, о которой мы говорил ранее.
В финансовых данных этого может быть недостаточно, чтобы полноценно отразить рыночную динамику. - Возможность переобучения на шумных данных
Как и другие глубокие сети, N-BEATS может переобучаться при наличии шумных данных. - Ограниченная интерпретируемость
Хотя N-BEATS включает разложение через базисные функции для интерпретируемости, это все же глубокая нейронная сеть, и она менее интерпретируема по сравнению с моделями временных рядов, такими как ARIMA или PROPHET. - Меньшее распространение
Скорее всего вы еще не сталкивались с этой моделью.
Несмотря на высокую эффективность в академических исследованиях, она не получила широкого распространения в сообществе машинного обучения по сравнению с ARIMA, XGBoost, LSTM и другими моделями. В интернете можно найти очень мало публикаций о ее применении.
Таблица вложений
| Имя файла | Описание и назначение |
|---|---|
| Trade\PositionInfo.py | Содержит класс CPositionInfo, аналог класса из MQL5; предоставляет информацию обо всех открытых позициях в MetaTrader 5. |
| Trade\SymbolInfo.py | Содержит класс CSymbolInfo, аналог класса из MQL5; предоставляет всю информацию о выбранном символе из MetaTrader 5. |
| Trade\Trade.py | Содержит класс CTrade, аналог класса из MQL5; предоставляет функции для открытия и закрытия сделок в MetaTrader 5. |
| error_description.py | Содержит функции для преобразования кодов ошибок MetaTrader 5 в удобочитаемый вид. |
| NBEATS-Tradingbot.py | Скрипт на Python, использующий модель N-BEATS для принятия торговых решений. |
| test.ipynb | Блокнот Jupyter для экспериментов с моделью N-BEATS. |
| requirements.txt | Содержит все зависимости Python, используемые в этом проекте. |
Источники и ссылки
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/18242
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Торговые инструменты на MQL5 (Часть 13): Создание ценовой панели на базе Canvas с панелями графика и статистики
Внедрение в MQL5 практических модулей из других языков (Часть 03): Модуль schedule из Python — расширенные возможности OnTimer
Автоматизация торговых стратегий на MQL5 (Часть 24): Система торговли на пробое лондонской сессии с риск-менеджментом и трейлинг-стопами
Неопределенность как модель (Часть 3): Математическая статистика — как извлекать знания из данных
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Ознакомьтесь с новой статьей: Наука о данных и ML (часть 46): Прогнозирование фондовых рынков с помощью N-BEATS в Python.
Автор: Omega J Msigwa