English 中文 Español Deutsch 日本語
preview
Машинное обучение и Data Science (Часть 30): Тандем из сверточных (CNN) и рекуррентных (RNN) нейросетей для прогнозирования фондового рынка

Машинное обучение и Data Science (Часть 30): Тандем из сверточных (CNN) и рекуррентных (RNN) нейросетей для прогнозирования фондового рынка

MetaTrader 5Трейдинг |
590 1
Omega J Msigwa
Omega J Msigwa

Разделы


Введение

В предыдущих статьях мы говорили об эффективности сверточных нейронных сетей (CNN) и рекуррентных сетей (RNN). Мы поговорили о вариантах их использования применительно к рынку для получения качественных торговых сигналов.

В этой статье мы попытаемся объединить эти две мощные технологии. Оценим способность такого тандема предсказывать фондовый рынок. Но перед этим давайте вкратце вспомним, что такое CNN и RNN.


О двух типах нейросетей: рекуррентные (RNN) и сверточные (CNN)

Сверточные нейронные сети (CNN) были разработаны для распознавания паттернов и особенностей в данных. Хотя изначально они предназначались для задач распознавания изображений, они хорошо справляются с табличными данными, которые используются для прогнозирования временных рядов.

Как мы говорили в предыдущих статьях, они сначала применяют фильтры к входным данным, а затем извлекают высокоуровневые признаки, которые могут быть полезны для прогнозирования. В рыночных данных такими признаками являются тренды, сезонные эффекты и аномалии.

Сверточная нейронная сеть

Архитектура CNN

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


Рекуррентные нейронные сети (RNN) — это искусственные нейросети, разработанные для распознавания закономерностей в последовательностях данных, таких как временные ряды, текст или видео.

В отличие от традиционных нейронных сетей, которые предполагают, что входные данные независимы друг от друга, RNN могут улавливать и анализировать закономерности внутри последовательностей.

Поскольку рекуррентные нейронные сети специально разработаны для работы последовательными данными, их архитектура позволяет им сохранять информацию о предыдущих значениях и учитывают входные связи данные. Это делает их особенно подходящими для прогнозирования временных рядов — именно способность учитывать временные зависимости в данных имеет критическое значение для составления точных прогнозов на фондовом рынке.

Как я уже рассказывал в Части 25 этой серии статей, существует три наиболее часто используемых типа RNN: базовая (или "ванильная") рекуррентная нейронная сеть (RNN), сеть с долгосрочной и краткосрочной памятью (LSTM) и блок с управляемыми рекуррентными связями (GRU).

Если кратко: CNN отлично справляются с извлечением и обнаружением признаков в данных, а RNN умеют интерпретировать эти признаки с течением времени. Идея проста — объединить сильные стороны обеих моделей и попытаться создать более эффективную систему для прогнозирования фондового рынка.


Синергия CNN и RNN

Чтобы объединить эти два подхода, мы построим модель из трех этапов:

  1. Извлечение признаков с помощью CNN
  2. Моделирование временных зависимостей с помощью RNN
  3. Обучение модели и получение прогнозов

Давайте поэтапно разберем каждый шаг и соберем работающую модель, объединяющую в себе возможности RNN и LSTM.


01. Извлечение признаков с помощью CNN

На первом этапе мы передаем данные временного ряда в модель на основе CNN. Сверточная нейронная сеть обрабатывает данные, выявляя значимые паттерны и извлекая ключевые признаки, которые будут использоваться для дальнейшего прогнозирования. 

В качестве примера возьмем данные по акциям компании Tesla. В наборе будут цены открытия (Open), максимума (High), минимума (Low) и закрытия (Close). Начнем с подготовки данных в формате 3D, который необходим для подачи на вход как CNN, так и RNN.

Также сформируем целевую переменную для задачи классификации.

Код Python

target_var = []

open_price = new_df["Open"] 
close_price = new_df["Close"]

for i in range(len(open_price)): 
    if close_price[i] > open_price[i]: # Closing price is greater than opening price
        target_var.append(1) # buy signal
    else:
        target_var.append(0) # sell signal

Нормализуем данные с помощью Standard Scaler, чтобы сделать их подходящими для машинного обучения.

X = new_df.iloc[:, :-1]
y = target_var

# Scalling the data

scaler = StandardScaler()
X = scaler.fit_transform(X)

# Train-test split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, shuffle=False)

print(f"x_train = {X_train.shape} - x_test = {X_test.shape}\n\ny_train = {len(y_train)} - y_test = {len(y_test)}")

 Результаты

x_train = (799, 3) - x_test = (200, 3)

y_train = 799 - y_test = 200

Затем мы можем подготовить данные в формате временного ряда.

# creating the sequence 

X_train, y_train = create_sequences(X_train, y_train, time_step)
X_test, y_test = create_sequences(X_test, y_test, time_step)

Поскольку это проблема классификации, мы применяем one-hot кодирование к целевой переменной.

from tensorflow.keras.utils import to_categorical

y_train_encoded = to_categorical(y_train)
y_test_encoded = to_categorical(y_test)

print(f"One hot encoded\n\ny_train {y_train_encoded.shape}\ny_test {y_test_encoded.shape}")

Результаты

One hot encoded

y_train (794, 2)
y_test (195, 2)

Извлечение признаков выполняется непосредственно самой моделью CNN. Передадим ей сырые данные, которые мы только что подготовили.

model = Sequential()
model.add(Conv1D(filters=16, 
                 kernel_size=3, 
                 activation='relu', 
                 strides=2,
                 padding='causal',
                 input_shape=(time_step, X_train.shape[2])
                )
         )

model.add(MaxPooling1D(pool_size=2))


02. Моделирование временных зависимостей с помощью RNN

Извлеченные на предыдущем этапе признаки передадим на вход модели RNN. Она обрабатывает эти признаки, учитывая их временную последовательность и взаимосвязи между данными во времени.

В отличие от архитектуры модели CNN, с которой мы работали в Части 27 этой серии статей, где после слоя Flatten шли полносвязные слои, здесь мы поступим иначе: вместо обычных нейронных слоев мы используем рекуррентные — RNN, LSTM или GRU.

Важно также удалить слой Flatten из архитектуры CNN.

В классической CNN он используется для преобразования трёхмерного тензора (например, (batch, time, features)) в двумерный (например, (batch, features)), чтобы можно было передать данные в полносвязную сеть.

model.add(MaxPooling1D(pool_size=2))

model.add(SimpleRNN(50, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(50, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(units=len(np.unique(y)), activation='softmax'))  # Softmax for binary classification (1 buy, 0 sell signal)


03. Обучение модели и получение прогнозов

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

Код Python

model.summary()

# Compile the model
optimizer = Adam(learning_rate=0.0001)
model.compile(optimizer=optimizer, loss='binary_crossentropy', metrics=['accuracy'])

# Train the model
early_stopping = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True)

history = model.fit(X_train, y_train_encoded, epochs=1000, batch_size=16, validation_split=0.2, callbacks=[early_stopping])

plt.figure(figsize=(7.5, 6))
plt.plot(history.history['loss'], label='Training Loss')
plt.plot(history.history['val_loss'], label='Validation Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.title('Training Loss Curve')
plt.legend()
plt.savefig("training loss curve-rnn-cnn-clf.png")
plt.show()

# Evaluating the Trained Model

y_pred = model.predict(X_test) 


classes_in_y = np.unique(y)
y_pred_binary = classes_in_y[np.argmax(y_pred, axis=1)]

# Confusion Matrix
cm = confusion_matrix(y_test, y_pred_binary)
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.xlabel("Predicted Label")
plt.ylabel("True Label")
plt.title("Confusion Matrix")
plt.savefig("confusion-matrix RNN + CNN.png")  # Display the heatmap


print("Classification Report\n",
      classification_report(y_test, y_pred_binary))

Результаты

В результате оценки модели после 14 эпох точность модели на тестовых данных составила 54%.

Кривая потерь обучения RNN + LSTM

7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step
Classification Report
               precision    recall  f1-score   support

           0       0.70      0.40      0.51       117
           1       0.45      0.74      0.56        78

    accuracy                           0.54       195
   macro avg       0.58      0.57      0.54       195
weighted avg       0.60      0.54      0.53       195

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

После завершения обучения сохраняем финальную модель в формате ONNX.

Код Python

onnx_file_name = "rnn+cnn.TSLA.D1.onnx"

spec = (tf.TensorSpec((None, time_step, X_train.shape[2]), tf.float16, name="input"),)
model.output_names = ['outputs']

onnx_model, _ = tf2onnx.convert.from_keras(model, input_signature=spec, opset=13)

# Save the ONNX model to a file
with open(onnx_file_name, "wb") as f:
    f.write(onnx_model.SerializeToString())

Также не забываем сохранить параметра масштабатора.

# Save the mean and scale parameters to binary files

scaler.mean_.tofile(f"{onnx_file_name.replace('.onnx','')}.standard_scaler_mean.bin")
scaler.scale_.tofile(f"{onnx_file_name.replace('.onnx','')}.standard_scaler_scale.bin")

Сохраненную ONNX-модель я открыл в Netron, и она действительно оказалась очень объемной.

Модель CNN + RNN


Аналогично тому, как мы ранее разворачивали сверточную нейрсеть (CNN), здесь мы можем воспользоваться той же библиотекой, чтобы без особых усилий загрузить и использовать эту громоздкую модель в MQL5.

#include <MALE5\Convolutional Neural Networks(CNNs)\ConvNet.mqh>
#include <MALE5\preprocessing.mqh>

CConvNet cnn;
StandardizationScaler *scaler; //from preprocessing.mqh

Но прежде чем приступить к работе, необходимо добавить ONNX-модель и параметры стандартизации (StandardScaler) в наш советник в качестве ресурсов.

#resource "\\Files\\rnn+cnn.TSLA.D1.onnx" as uchar onnx_model[]
#resource "\\Files\\rnn+cnn.TSLA.D1.standard_scaler_mean.bin" as double standardization_mean[]
#resource "\\Files\\rnn+cnn.TSLA.D1.standard_scaler_scale.bin" as double standardization_std[]

Первое, что нам нужно сделать внутри функции OnInit, — это инициализировать масштабатор и саму модель CNN).

int OnInit()
  {
//---
   
   if (!cnn.Init(onnx_model)) //Initialize the Convolutional neural network 
     return INIT_FAILED;
   
   scaler = new StandardizationScaler(standardization_mean, standardization_std); //Initialize the saved scaler by populating it with values 
  
   ...
   ...
        
  return (INIT_SUCCEEDED);       
 }

Для получения прогнозов необходимо сначала нормализовать входные данные с использованием заранее загруженного масштабатора, затем передать нормализованные данные в модель CNN и на выходе получить прогнозный сигнал и вероятности.

   if (NewBar()) //Trade at the opening of a new candle
    {
      CopyRates(Symbol(), PERIOD_D1, 1, time_step, rates);
      
      for (ulong i=0;  i<x_data.Rows(); i++)
        {
          x_data[i][0] = rates[i].open;
          x_data[i][1] = rates[i].high;
          x_data[i][2] = rates[i].low;
        }
   
   //---
            
      x_data = scaler.transform(x_data); //Normalize the data
       
      int signal = cnn.predict_bin(x_data, classes_in_data_); //getting a trading signal from the RNN model
      vector probabilities = cnn.predict_proba(x_data);  //probability for each class
     
      Comment("Probability = ",probabilities,"\nSignal = ",signal);

На скриншоте ниже показано, как комментарий выглядит на графике.

Комментарии по сигналам и вероятностям на графике

Вектор вероятностей зависит от классов, которые присутствовали в целевой переменной обучающего набора данных. Напомним, что при подготовке данных для обучения мы обозначили целевую переменную следующим образом: 0 — сигнал на продажу, 1 — сигнал на покупку. При этом идентификаторы классов (номера) должны быть в порядке возрастания.

input int time_step = 5; 
input int magic_number = 24092024;
input int slippage = 100;

MqlRates rates[];
matrix x_data(time_step, 3); //3 columns for open, high and low
vector classes_in_data_ = {0, 1}; //unique target variables as they are in the target variable in your training data
int OldNumBars = 0;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---

Матрица x_data отвечает за временное хранение независимых переменных (признаков), полученных с рынка. Эта матрица масштабируется под 3 столбца, так как модель обучалась на трех признаках: Open, High, Low, а число строк соответствует значению временного окна (time step), использованному при подготовке обучающей выборки.

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

На основе сигналов, генерируемых нашей моделью, разработаем простую стратегию.

   double min_lot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN);
     
      MqlTick ticks;
      SymbolInfoTick(Symbol(), ticks);
      
      if (signal==1) //if the signal is bullish
       {    
          ClosePos(POSITION_TYPE_SELL); //close sell trades when the signal is buy
          
          if (!PosExists(POSITION_TYPE_BUY)) //There are no buy positions
           {
             if (!m_trade.Buy(min_lot, Symbol(), ticks.ask, 0 , 0)) //Open a buy trade
               printf("Failed to open a buy position err=%d",GetLastError());
           }
       }
      else if (signal==0) //Bearish signal
        {
          ClosePos(POSITION_TYPE_BUY); //close all buy trades when the signal is sell
          
          if (!PosExists(POSITION_TYPE_SELL)) //There are no Sell positions
           {
            if (!m_trade.Sell(min_lot, Symbol(), ticks.bid, 0 , 0)) //open a sell trade
               printf("Failed to open a sell position err=%d",GetLastError());
           }
        }
      else //There was an error
        return;

Теперь, когда модель загружена и готова к работе, протестируем ее на периоде с 01.01.2020 по 01.09.2024. Ниже представлено изображение с полной конфигурацией тестера.

Обратите внимание: советник запущен на 4-часовом графике, хотя данные по акциям Tesla были собраны с дневного таймфрейма. Это сделано не случайно. Стратегия и модель были запрограммированы на активацию сразу после открытия новой свечи, однако дневная свеча обычно открывается в момент, когда рынок закрыт, что может привести к тому, что советник пропустит сделку и "заснет" до следующего дня. 

Запуская советника на младшем таймфрейме (в данном случае H4), мы обеспечиваем непрерывный мониторинг рынка каждые 4 часа и возможность оперативно реагировать на сигналы модели. 

При этом источник данных для принятия решений не меняется, так как мы применяем функцию CopyRates к дневному таймфрейму — торговые сигналы по-прежнему формируются на основе дневных данных.

Ниже представлен результат тестера.

Впечатляюще! Советник показал 90% прибыльных сделок. Причем использовалась простая модель на основе RNN.

Теперь давайте посмотрим, насколько эффективны LSTM и GRU в тех же рыночных условиях.


Комбинация CNN и LSTM

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

На практике LSTM чаще дает более точные и стабильные результаты, чем классическая RNN. Давайте построим модель CNN с LSTM, протестируем ее на данных акций Tesla и посмотрим, как она себя проявит.

Код Python

from tensorflow.keras.layers import LSTM

# Define the CNN model

model = Sequential()
model.add(Conv1D(filters=16, 
                 kernel_size=3, 
                 activation='relu', 
                 strides=2,
                 padding='causal',
                 input_shape=(time_step, X_train.shape[2])
                )
         )

model.add(MaxPooling1D(pool_size=2))


model.add(LSTM(50, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(50, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(units=len(np.unique(y)), activation='softmax'))  # For binary classification (e.g., buy/sell signal)

model.summary()

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

После обучения и валидации модели ее точность составила 53% на тестовых данных.

7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 36ms/step
Classification Report
               precision    recall  f1-score   support

           0       0.67      0.44      0.53       117
           1       0.45      0.68      0.54        78

    accuracy                           0.53       195
   macro avg       0.56      0.56      0.53       195
weighted avg       0.58      0.53      0.53       195

В MQL5 мы можем использовать ту же библиотеку, что и для советника с простой RNN.

#resource "\\Files\\lstm+cnn.TSLA.D1.onnx" as uchar onnx_model[]
#resource "\\Files\\lstm+cnn.TSLA.D1.standard_scaler_mean.bin" as double standardization_mean[]
#resource "\\Files\\lstm+cnn.TSLA.D1.standard_scaler_scale.bin" as double standardization_std[]

#include <MALE5\Convolutional Neural Networks(CNNs)\ConvNet.mqh>
#include <MALE5\preprocessing.mqh>

CConvNet cnn;
StandardizationScaler *scaler;

Остальная часть кода осталась такой же, как и в советнике с CNN + RNN.

Я использовал те же настройки тестера, что и раньше, и вот что получилось.

На этот раз общая точность сделок составила около 74%. Это меньше, чем в предыдущей модели, но все равно впечатляющий результат!


Комбинация сверточной нейронной сети (CNN) и Gated Recurrent Unit (GRU)

Как и LSTM, модели GRU тоже способны распознавать зависимости в длинных последовательностях данных. При этом они обладают более минималистичной архитектурой, чем LSTM.

Мы можем реализовать модель GRU так же, как и другие RNN, изменив только тип модели в коде при построении архитектуры CNN.

from tensorflow.keras.layers import GRU

# Define the CNN model

model = Sequential()
model.add(Conv1D(filters=16, 
                 kernel_size=3, 
                 activation='relu', 
                 strides=2,
                 padding='causal',
                 input_shape=(time_step, X_train.shape[2])
                )
         )

model.add(MaxPooling1D(pool_size=2))


model.add(GRU(50, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(50, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(units=len(np.unique(y)), activation='softmax'))  # For binary classification (e.g., buy/sell signal)

model.summary()

После обучения и проверки модели она показала ту же точность, что и модель LSTM, а именно 53% точности на тестовых данных.

7/7 ━━━━━━━━━━━━━━━━━━━━ 1s 41ms/step
Classification Report
               precision    recall  f1-score   support

           0       0.69      0.39      0.50       117
           1       0.45      0.73      0.55        78

    accuracy                           0.53       195
   macro avg       0.57      0.56      0.53       195
weighted avg       0.59      0.53      0.52       195

Загружаем модель GRU в формате ONNX и ее параметры масштабирования в двоичных файлах.

#resource "\\Files\\gru+cnn.TSLA.D1.onnx" as uchar onnx_model[]
#resource "\\Files\\gru+cnn.TSLA.D1.standard_scaler_mean.bin" as double standardization_mean[]
#resource "\\Files\\gru+cnn.TSLA.D1.standard_scaler_scale.bin" as double standardization_std[]

#include <MALE5\Convolutional Neural Networks(CNNs)\ConvNet.mqh>
#include <MALE5\preprocessing.mqh>

CConvNet cnn;
StandardizationScaler *scaler;

Снова остальная часть кода остаётся такой же, как и в советнике с простой RNN.

После тестирования модели с использованием тех же настроек тестера, вот что мы получили.

Модель GRU показала точность около 61%. Это не так хорошо, как в двух предыдущих моделях, но все равно достойный результат.


Заключительные мысли

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

Кроме того, сложность сочетания CNN и RNN ведет к значительным вычислительным затратам, особенно если вы решите масштабировать модель, добавив больше плотных слоев или увеличив количество нейронов. Важно тщательно сбалансировать сложность модели с доступными вычислительными ресурсами и задачей, которую необходимо решить.

Всем добра.


За развитием этой модели машинного обучения и многого другого из этой серии статей можно следить в моем репозитории на GitHub.

Таблица вложений


Наименование файла

Тип файла Описание и использование

Experts\CNN + GRU EA.mq5
Experts\CNN + LSTM EA.mq5
Experts\CNN + RNN EA.mq5

 Советники  Торговый робот для загрузки моделей ONNX и тестирования торговой стратегии в MetaTrader 5.

ConvNet.mqh
preprocessing.mqh

 Include files


- Этот файл содержит код для загрузки моделей CNN, сохраненных в формате ONNX.
- Стандартный масштабатор


Files\ *.onnx


 ONNX-модели


 Модели машинного обучения, обсуждаемые в этой статье, в формате ONNX

Files\*.bin  Бинарные файлы
 Бинарные файлы для загрузки параметров масштабатора для каждой модели


Jupyter Notebook\cnns-rnns.ipynb  

 python/Jupyter notebook   Этот блокнот содержит весь код Python, обсуждаемый в этой статье.


Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/15585

Прикрепленные файлы |
Attachments.zip (342.55 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Juan Guirao
Juan Guirao | 13 окт. 2024 в 20:30
Эти результаты удивительно хороши. Добавлено в список TODO.
Алгоритм верблюда — Camel Algorithm (CA) Алгоритм верблюда — Camel Algorithm (CA)
Алгоритм верблюда, разработанный в 2016 году, моделирует поведение верблюдов в пустыне для решения оптимизационных задач, учитывая факторы температуры, запасов и выносливости. В данной работе представлена еще его модифицированная версия (CAm) с ключевыми улучшениями: применение гауссова распределения при генерации решений и оптимизация параметров эффекта оазиса.
Критерий независимости Гильберта-Шмидта (HSIC) Критерий независимости Гильберта-Шмидта (HSIC)
В статье рассматривается непараметрический статистический тест HSIC (Hilbert-Schmidt Independence Criterion) предназначенный для выявления линейных и нелинейных зависимостей в данных. Предложены реализации двух алгоритмов вычисления HSIC на языке MQL5: точного перестановочного теста и гамма-аппроксимации. Эффективность метода демонстрируется на синтетических данных, моделирующих нелинейную связь признаков и целевой переменной.
Как создать торговый журнал с помощью MetaTrader и Google Sheets Как создать торговый журнал с помощью MetaTrader и Google Sheets
Создайте торговый журнал с помощью MetaTrader и Google Sheets! Вы узнаете, как синхронизировать свои торговые данные с помощью HTTP POST и извлекать их с помощью HTTP-запросов. Наконец, у вас будет торговый журнал, который поможет эффективно отслеживать ваши сделки.
От начального до среднего уровня: Массив (III) От начального до среднего уровня: Массив (III)
В этой статье мы рассмотрим, как работать с массивами в MQL5, в том числе, как передавать информацию между функциями и процедурами с помощью массивов. Цель — подготовить вас к тому, что будет демонстрироваться и разъясняться в будущих материалах серии. Поэтому настоятельно рекомендую внимательно изучить то, что будет показано в этой статье.