English Deutsch 日本語
preview
Машинное обучение и Data Science (Часть 42): Прогнозирование фондовых рынков с использованием N-BEATS в Python

Машинное обучение и Data Science (Часть 42): Прогнозирование фондовых рынков с использованием N-BEATS в Python

MetaTrader 5Торговые системы |
345 1
Omega J Msigwa
Omega J Msigwa

Содержание


Что такое 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:

  1. Простота модели без ущерба для точности

    Поскольку более простые линейные модели прогнозирования временных рядов, такие как ARIMA, не способны улавливать сложные зависимости, которые лучше выявляются моделями глубокого обучения, разработчики решили использовать простые архитектуры нейронных сетей (MLP) для прогнозирования временных рядов. Они более интерпретируемы, быстрее работают и проще в отладке.

    Использование глубоких моделей, таких как RNN, LSTM или Transformers, добавляет дополнительный уровень сложности, делая модель труднее настраиваемой и более медленной при обучении.

  2. Интерпретируемость через структуру

    Поскольку MLP и другие нейронные модели не предоставляют интерпретируемых результатов, разработчики стремились создать модель, способную давать человеко-интерпретируемые прогнозы, разлагая выход на компоненты тренда и сезонности, аналогично классическим моделям временных рядов, таким как ETS.

    Модель N-BEATS позволяет явно объяснять изменения в данных, например: "Этот скачок в данных вызван трендом" или "Это падение носит сезонный характер". Это достигается через слои расширения базиса (например, полиномиальный или Фурье-базис).

  3. Конкурентная точность без специфических настроек

    Модель нацелена на создание универсальной модели, которая эффективно работает с широким спектром временных рядов и требует минимального ручного инженерного вмешательства.

    Модели вроде Prophet требуют от пользователя задания шаблонов тренда и сезонности.

    N-BEATS автоматически изучает эти закономерности напрямую из данных.

  4. Поддержка глобального моделирования множества временных рядов

    Многие модели прогнозирования временных рядов работают с одной серией данных за раз (панельные данные). N-BEATS разработан для прогнозирования нескольких временных рядов одновременно.
    Это особенно важно для финансовых данных, когда необходимо прогнозировать более одного показателя, например, цены закрытия NASDAQ и S&P 500 одновременно.
  5. Быстрое и масштабируемое обучение

    Модель N-BEATS спроектирована так, чтобы обучение было быстрым и легко параллелизируемым, в отличие от RNN или моделей на основе внимания.

  6. Высокая производительность относительно базовых методов

    N-BEATS стремится превзойти современные классические методы, такие как ARIMA и ETS, в честной бэктестинговой оценке.

  7. Модульная и расширяемая архитектура

    Классические модели прогнозирования временных рядов статичны и не поддаются модификации. 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 имеет свои ограничения, о которых следует помнить:

  1. Прежде всего предназначена для одномерного прогнозирования
    Как показано ранее, для обучения модели требуется всего два признака: ds (дата) и целевая переменная y, аналогично модели PROPHET, о которой мы говорил ранее.

    В финансовых данных этого может быть недостаточно, чтобы полноценно отразить рыночную динамику.
  2. Возможность переобучения на шумных данных
    Как и другие глубокие сети, N-BEATS может переобучаться при наличии шумных данных.
  3. Ограниченная интерпретируемость
    Хотя N-BEATS включает разложение через базисные функции для интерпретируемости, это все же глубокая нейронная сеть, и она менее интерпретируема по сравнению с моделями временных рядов, такими как ARIMA или PROPHET.
  4. Меньшее распространение
    Скорее всего вы еще не сталкивались с этой моделью. 

    Несмотря на высокую эффективность в академических исследованиях, она не получила широкого распространения в сообществе машинного обучения по сравнению с 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

Прикрепленные файлы |
Attachments.zip (125.7 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
nevar
nevar | 21 июл. 2025 в 11:26
Очень хорошая статья, спасибо Omega.
Поскольку для разложения используется быстрое преобразование Фурье, которое позволяет модели улавливать как краткосрочную сезонность, так и долгосрочные тренды по отдельности. А использование самой цены закрытия в качестве входных или выходных данных подходит для алгоритма N-BEATS?

Торговые инструменты на MQL5 (Часть 13): Создание ценовой панели на базе Canvas с панелями графика и статистики Торговые инструменты на MQL5 (Часть 13): Создание ценовой панели на базе Canvas с панелями графика и статистики
В этой статье мы разрабатываем ценовую панель на основе холста (canvas) в MQL5 с использованием класса CCanvas для создания интерактивных панелей для визуализации последних графиков цен и статистики счетов с поддержкой фоновых изображений, эффектов тумана и градиентной заливки. Система включает в себя функции перетаскивания и изменения размера с помощью обработки событий мыши, переключение тем оформления между темным и светлым режимами с динамической настройкой цветов, а также элементы управления сворачиванием/разворачиванием для эффективного управления пространством графика.
Внедрение в MQL5 практических модулей из других языков (Часть 03): Модуль schedule из Python — расширенные возможности OnTimer Внедрение в MQL5 практических модулей из других языков (Часть 03): Модуль schedule из Python — расширенные возможности OnTimer
Модуль schedule в Python предоставляет простой способ планирования повторяющихся задач. Хотя в MQL5 отсутствует встроенный аналог, в этой статье мы реализуем аналогичную библиотеку, чтобы упростить настройку событий по расписанию в MetaTrader 5.
Автоматизация торговых стратегий на MQL5 (Часть 24): Система торговли на пробое лондонской сессии с риск-менеджментом и трейлинг-стопами Автоматизация торговых стратегий на MQL5 (Часть 24): Система торговли на пробое лондонской сессии с риск-менеджментом и трейлинг-стопами
В этой статье мы разработаем систему анализа пробоев на Лондонской сессии, которая будет определять пробои диапазона перед открытием сессии и выставлять отложенные ордера с настройкой типа сделок и параметров риска. Мы реализуем в системе трейлинг-стоп, соотношение риска и прибыли, контроль максимальной просадки, а также панель управления для мониторинга в режиме реального времени.
Неопределенность как модель (Часть 3): Математическая статистика — как извлекать знания из данных Неопределенность как модель (Часть 3): Математическая статистика — как извлекать знания из данных
В данной части цикла разбираются механизмы Закона больших чисел (ЗБЧ) и Центральной предельной теоремы (ЦПТ) как теоретической основы для понимания рыночных закономерностей. Описывается инструментарий описательной статистики и методы нахождения точечных и интервальных оценок параметров распределений. Особое внимание уделено методологии проверки статистических гипотез, позволяющей объективно отделять истинные рыночные аномалии от случайного шума. Каждое теоретическое построение сопровождено практическим примером в приложении, что позволяет закрепить материал на конкретных данных.