English 中文 Deutsch 日本語
preview
Автоматизация торговых стратегий на MQL5 (Часть 21): Улучшение торговли на основе нейронных сетей с помощью адаптивных темпов обучения

Автоматизация торговых стратегий на MQL5 (Часть 21): Улучшение торговли на основе нейронных сетей с помощью адаптивных темпов обучения

MetaTrader 5Трейдинг |
139 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В нашей предыдущей статье (Часть 20) мы разработали мультисимвольную стратегию на основе Commodity Channel Index (CCI) и Awesome Oscillator (AO), что позволило автоматизировать сделки на развороте тренда по нескольким валютным парам с надежной генерацией сигналов и управлением рисками на MetaQuotes Language 5 (MQL5). В части 21 мы переходим к торговой стратегии на основе нейронной сети, усиленной механизмом адаптивного темпа обучения (adaptive learning rate) для оптимизации точности прогнозирования рыночных движений. Мы рассмотрим следующие темы:

  1. Стратегия адаптивного темпа обучения нейронных сетей
  2. Реализация в MetaQuotes Language 5 (MQL5)
  3. Тестирование и оптимизация настроек темпа обучения
  4. Заключение

В итоге у нас будет комплексная торговая система на языке MetaQuotes Language 5 (MQL5), использующая нейронные сети с динамической регулировкой темпа обучения, готовая к дальнейшему совершенствованию.


Стратегия адаптивного темпа обучения нейронных сетей

В Части 20 мы разработали мультисимвольную торговую систему, которая использует Commodity Channel Index и Awesome Oscillator, что позволило автоматизировать сделки на развороте тренда по нескольким валютным парам. Теперь мы займемся торговой стратегией на основе нейронной сети, используя вычислительные модели, имитирующие взаимосвязанные нейроны человеческого мозга, для более точного прогнозирования движений рыночных цен путем обработки разнообразных рыночных индикаторов и адаптации процесса обучения к волатильности рынка. Наша цель — построить гибкую, высокопроизводительную торговую систему, которая использует нейронные сети для анализа сложных рыночных паттернов и исполнения сделок с оптимизированной точностью благодаря механизму темпа обучения.

Нейронные сети работают через слои узлов, или нейронов, структурированных как входной слой (получающий рыночные данные), скрытые слои (выявляющие сложные паттерны) и выходной слой (генерирующий торговые сигналы, например, предсказывающий рост или падение цены). Прямое распространение (forward propagation) передает данные через эти слои, где нейроны применяют веса и смещения к входам, преобразуя их в прогнозы. Рассмотрите рисунок ниже.

НЕЙРОННАЯ СЕТЬ СО СЛОЯМИ И ВЕСАМИ

Критически важный компонент, функция активации, вносит нелинейность в эти преобразования, позволяя сети моделировать сложные взаимосвязи; например, сигмоидная функция активации отображает значения в диапазоне от 0 до 1, что делает ее идеальной для задач бинарной классификации, таких как принятие решений о покупке или продаже. Обратное распространение (backpropagation) совершенствует этот процесс, работая в обратном направлении от ошибок прогноза и корректируя веса и смещения для повышения точности со временем. Рассмотрим рисунок ниже.

РАСПРОСТРАНЕНИЯ

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

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

ФУНКЦИИ АКТИВАЦИИ

Также мы будем динамически корректировать скорость обучения на основе результатов тренировки и адаптировать количество нейронов в скрытом слое в соответствии с рыночной волатильностью, создавая отзывчивую систему, которая балансирует между сложностью и эффективностью. Стратегия закладывает основу для надежной реализации и тщательного тестирования. Мы будем использовать две скользящие средние - индекс относительной силы (Relative Strength Index, RSI) и средний истинный диапазон (Average True Range, ATR) для предоставления входных данных. Как только мы получим сигналы, мы откроем сделки. Ниже показано, чего мы стремимся достичь после внедрения.

ПЛАН


Реализация средствами MQL5

Чтобы создать программу на MQL5, откройте MetaEditor, перейдите в "Навигатор", выберите папку Indicators, кликните "Создать" и следуйте инструкциям для создания файла. После создания файла в среде программирования мы начнем с объявления некоторых входных переменных, структур и классов, которые будем использовать, поскольку хотим применить объектно-ориентированный (Object Oriented Programming, OOP) подход.

//+------------------------------------------------------------------+
//|                              Neural Networks Propagation EA.mq5  |
//|                          Copyright 2025, Allan Munene Mutiiria.  |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"

#include <Trade/Trade.mqh>
CTrade tradeObject; //--- Instantiate trade object for executing trades

// Input parameters with clear, meaningful names
input double LotSize            = 0.1;      // Lot Size
input int    StopLossPoints     = 100;      // Stop Loss (points)
input int    TakeProfitPoints   = 100;      // Take Profit (points)
input int    MinHiddenNeurons   = 10;       // Minimum Hidden Neurons
input int    MaxHiddenNeurons   = 50;       // Maximum Hidden Neurons
input int    TrainingBarCount   = 1000;     // Training Bars
input double MinPredictionAccuracy = 0.7;   // Minimum Prediction Accuracy
input double MinLearningRate    = 0.01;     // Minimum Learning Rate
input double MaxLearningRate    = 0.5;      // Maximum Learning Rate
input string InputToHiddenWeights = "0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1"; // Input-to-Hidden Weights
input string HiddenToOutputWeights = "0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1"; // Hidden-to-Output Weights
input string HiddenBiases       = "0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1,0.1,-0.1"; // Hidden Biases
input string OutputBiases       = "0.1,-0.1"; // Output Biases

// Neural Network Structure Constants
const int INPUT_NEURON_COUNT = 10; //--- Define number of input neurons
const int OUTPUT_NEURON_COUNT = 2; //--- Define number of output neurons
const int MAX_HISTORY_SIZE = 10;   //--- Define maximum history size for accuracy and error tracking

// Indicator handles
int ma20IndicatorHandle; //--- Handle for 20-period moving average
int ma50IndicatorHandle; //--- Handle for 50-period moving average
int rsiIndicatorHandle;  //--- Handle for RSI indicator
int atrIndicatorHandle;  //--- Handle for ATR indicator

// Training related structures
struct TrainingData {
    double inputValues[]; //--- Array to store input values for training
    double targetValues[]; //--- Array to store target values for training
};

Здесь мы закладываем основу для нашей торговой стратегии на основе нейронных сетей с адаптивным темпом обучения, инициализируя основные компоненты для исполнения сделок и обработки данных. Подключим библиотеку Trade.mqh и создадим экземпляр класса CTrade под названием tradeObject для управления торговыми операциями, что позволяет исполнять ордера на покупку и продажу на основе прогнозов нейронной сети.

Определим входные параметры для настройки стратегии: LotSize - для контроля объема сделок, StopLossPoints и TakeProfitPoints - для управления рисками, а также MinHiddenNeurons и MaxHiddenNeurons - для определения диапазона скрытых нейронов в нейронной сети. Кроме того, укажем TrainingBarCount для количества исторических баров, используемых при обучении, MinPredictionAccuracy - как порог точности, а также MinLearningRate и MaxLearningRate - для ограничения адаптивного темпа обучения. Также предоставим InputToHiddenWeights, HiddenToOutputWeights, HiddenBiases и OutputBiases в виде строковых входных данных для инициализации весов и смещений нейронной сети, позволяя использовать предустановленные значения или значения по умолчанию.

Далее определим константы для структуры нейронной сети, установив INPUT_NEURON_COUNT на 10 для входных рыночных данных, OUTPUT_NEURON_COUNT - на 2 для выходных сигналов покупки/продажи и MAX_HISTORY_SIZE - на 10 для отслеживания точности и ошибок обучения. Создадим хендлы индикаторов ma20IndicatorHandle, ma50IndicatorHandle, rsiIndicatorHandle и atrIndicatorHandle для ссылки на 20- и 50-периодные скользящие средние, RSI и Average True Range соответственно, для подачи рыночных данных в нейронную сеть. Наконец, определим структуру TrainingData с массивами inputValues и targetValues для хранения входных признаков и ожидаемых выходных данных для обучения, обеспечивая организованное управление данными для обучения нейронной сети. Будем хранить данные, как показано ниже, когда заполним их значениями.

СТРУКТУРА ДАННЫХ НА ОСНОВЕ ВХОДНЫХ ДАННЫХ

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

// Neural Network Class
class CNeuralNetwork {
private:
   int inputNeuronCount;          //--- Number of input neurons
   int hiddenNeuronCount;         //--- Number of hidden neurons
   int outputNeuronCount;         //--- Number of output neurons
   double inputLayer[];           //--- Array for input layer values
   double hiddenLayer[];          //--- Array for hidden layer values
   double outputLayer[];          //--- Array for output layer values
   double inputToHiddenWeights[]; //--- Weights between input and hidden layers
   double hiddenToOutputWeights[];//--- Weights between hidden and output layers
   double hiddenLayerBiases[];    //--- Biases for hidden layer
   double outputLayerBiases[];    //--- Biases for output layer
   double outputDeltas[];         //--- Delta values for output layer
   double hiddenDeltas[];         //--- Delta values for hidden layer
   double trainingError;          //--- Current training error
   double currentLearningRate;    //--- Current learning rate
   double accuracyHistory[];      //--- History of training accuracy
   double errorHistory[];         //--- History of training errors
   int historyRecordCount;        //--- Number of recorded history entries
};

Здесь мы реализуем основную структуру нашей нейронной сети, создавая класс CNeuralNetwork. Определим приватные переменные-члены для управления архитектурой сети и процессом обучения, начиная с inputNeuronCount, hiddenNeuronCount и outputNeuronCount для установки количества нейронов во входном, скрытом и выходном слоях соответственно, в соответствии с устройством стратегии для обработки рыночных данных и генерации торговых сигналов.

Создадим массивы для хранения значений слоев, включая inputLayer - для входных данных индикаторов, hiddenLayer - для обработки промежуточных паттернов и outputLayer - для формирования прогнозов на покупку/продажу. Для обработки вычислений нейронной сети создадим массивы inputToHiddenWeights и hiddenToOutputWeights для весовых связей между слоями, а также hiddenLayerBiases и outputLayerBiases - для корректировки смещений в скрытом и выходном слоях. Для обратного распространения определим outputDeltas и hiddenDeltas для хранения градиентов ошибок, что позволяет обновлять веса и смещения во время обучения.

Кроме того, включим trainingError для отслеживания текущей ошибки, currentLearningRate - для управления адаптивным темпом обучения, массивы accuracyHistory и errorHistory - для мониторинга результатов обучения с течением времени и historyRecordCount для подсчета зарегистрированных записей, гарантируя, что сеть может динамически корректировать свое обучение на основе тенденций производительности. Класс формирует основу для реализации прямого распространения, обратного распространения и корректировок адаптивного темпа обучения в последующих функциях. Внутри приватного модификатора доступа мы можем определить метод для преобразования строковых входных данных в массив для дальнейшего использования.

// Parse comma-separated string to array
bool ParseStringToArray(string inputString, double &output[], int expectedSize) {
   //--- Check if input string is empty
   if(inputString == "") return false;
   string values[];
   //--- Initialize array for parsed values
   ArrayResize(values, 0);
   //--- Split input string by comma
   int count = StringSplit(inputString, 44, values);
   //--- Check if string splitting failed
   if(count <= 0) {
      Print("Error: StringSplit failed for input: ", inputString, ". Error code: ", GetLastError());
      return false;
   }
   //--- Verify correct number of values
   if(count != expectedSize) {
      Print("Error: Invalid number of values in input string. Expected: ", expectedSize, ", Got: ", count);
      return false;
   }
   //--- Resize output array to expected size
   ArrayResize(output, expectedSize);
   //--- Convert string values to doubles and normalize
   for(int i = 0; i < count; i++) {
      output[i] = StringToDouble(values[i]);
      //--- Clamp values between -1.0 and 1.0
      if(MathAbs(output[i]) > 1.0) output[i] = MathMax(-1.0, MathMin(1.0, output[i]));
   }
   return true;
}

Реализуем функцию ParseStringToArray в классе CNeuralNetwork для обработки строк, разделенных запятыми, для весов и смещений нейронной сети. Проверим, пуст ли inputString, возвращая false в случае недопустимости, и используем StringSplit с разделителем-запятой для разделения на "значения". Если разделение не удается или значение count не соответствует значению expectedSize, регистрируем ошибки с помощью Print и возвращаем false. Изменяем размер "выходных" данных с помощью ArrayResize, конвертируем "значения" в формат double, используя StringToDouble, нормализуем их между -1.0 и 1.0 с MathMax и MathMin, и возвращаем true для успешного анализа. Остальные вспомогательные функции могут быть объявлены в части публичного модификатора доступа, как показано ниже. Мы можем определить их позже. 

public:
   CNeuralNetwork(int inputs, int hidden, int outputs); //--- Constructor
   void InitializeWeights();                            //--- Initialize network weights
   double Sigmoid(double x);                            //--- Apply sigmoid activation function
   void ForwardPropagate();                             //--- Perform forward propagation
   void Backpropagate(const double &targets[]);         //--- Perform backpropagation
   void SetInput(double &inputs[]);                     //--- Set input values
   void GetOutput(double &outputs[]);                   //--- Retrieve output values
   double TrainOnHistoricalData(TrainingData &data[]);  //--- Train on historical data
   void UpdateNetworkWithRecentData();                  //--- Update with recent data
   void InitializeTraining();                           //--- Initialize training arrays
   void ResizeNetwork(int newHiddenNeurons);            //--- Resize network
   void AdjustLearningRate();                           //--- Adjust learning rate dynamically
   double GetRecentAccuracy();                          //--- Get recent training accuracy
   double GetRecentError();                             //--- Get recent training error
   bool ShouldRetrain();                                //--- Check if retraining is needed
   double CalculateDynamicNeurons();                    //--- Calculate dynamic neuron count
   int GetHiddenNeurons() {
      return hiddenNeuronCount;                         //--- Get current hidden neuron count
   }

Здесь мы определяем публичный интерфейс класса CNeuralNetwork. Создадим конструктор CNeuralNetwork с параметрами inputs, hidden и outputs для настройки сети и InitializeWeights - для настройки весов и смещений. Реализуем Sigmoid для активации, ForwardPropagate и Backpropagate - для обработки и обновления на основе targets, а также SetInput и GetOutput - для обработки inputs и outputs.

Создадим TrainOnHistoricalData с использованием TrainingData, UpdateNetworkWithRecentData для недавних данных, InitializeTraining - для массивов, ResizeNetwork - для newHiddenNeurons, AdjustLearningRate - для динамического обучения, а также GetRecentAccuracy, GetRecentError, ShouldRetrain, CalculateDynamicNeurons и GetHiddenNeurons - для мониторинга и адаптации hiddenNeuronCount. Теперь мы можем приступить к реализации и определению функций, как показано ниже.

// Constructor
CNeuralNetwork::CNeuralNetwork(int inputs, int hidden, int outputs) {
   //--- Set input neuron count
   inputNeuronCount = inputs;
   //--- Set output neuron count
   outputNeuronCount = outputs;
   //--- Initialize learning rate to minimum
   currentLearningRate = MinLearningRate;
   //--- Set hidden neuron count
   hiddenNeuronCount = hidden;
   //--- Ensure hidden neurons within bounds
   if(hiddenNeuronCount < MinHiddenNeurons) hiddenNeuronCount = MinHiddenNeurons;
   if(hiddenNeuronCount > MaxHiddenNeurons) hiddenNeuronCount = MaxHiddenNeurons;
   //--- Resize input layer array
   ArrayResize(inputLayer, inputs);
   //--- Resize hidden layer array
   ArrayResize(hiddenLayer, hiddenNeuronCount);
   //--- Resize output layer array
   ArrayResize(outputLayer, outputs);
   //--- Resize input-to-hidden weights array
   ArrayResize(inputToHiddenWeights, inputs * hiddenNeuronCount);
   //--- Resize hidden-to-output weights array
   ArrayResize(hiddenToOutputWeights, hiddenNeuronCount * outputs);
   //--- Resize hidden biases array
   ArrayResize(hiddenLayerBiases, hiddenNeuronCount);
   //--- Resize output biases array
   ArrayResize(outputLayerBiases, outputs);
   //--- Resize accuracy history array
   ArrayResize(accuracyHistory, MAX_HISTORY_SIZE);
   //--- Resize error history array
   ArrayResize(errorHistory, MAX_HISTORY_SIZE);
   //--- Initialize history record count
   historyRecordCount = 0;
   //--- Initialize training error
   trainingError = 0.0;
   //--- Initialize network weights
   InitializeWeights();
   //--- Initialize training arrays
   InitializeTraining();
}

// Initialize training arrays
void CNeuralNetwork::InitializeTraining() {
   //--- Resize output deltas array
   ArrayResize(outputDeltas, outputNeuronCount);
   //--- Resize hidden deltas array
   ArrayResize(hiddenDeltas, hiddenNeuronCount);
}

// Initialize weights
void CNeuralNetwork::InitializeWeights() {
   //--- Track if weights and biases are set
   bool isInputToHiddenWeightsSet = false;
   bool isHiddenToOutputWeightsSet = false;
   bool isHiddenBiasesSet = false;
   bool isOutputBiasesSet = false;
   double tempInputToHiddenWeights[];
   double tempHiddenToOutputWeights[];
   double tempHiddenBiases[];
   double tempOutputBiases[];

   //--- Parse and set input-to-hidden weights if provided
   if(InputToHiddenWeights != "" && ParseStringToArray(InputToHiddenWeights, tempInputToHiddenWeights, inputNeuronCount * hiddenNeuronCount)) {
      //--- Copy parsed weights to main array
      ArrayCopy(inputToHiddenWeights, tempInputToHiddenWeights);
      isInputToHiddenWeightsSet = true;
      //--- Log weight initialization
      Print("Initialized input-to-hidden weights from input: ", InputToHiddenWeights);
   }
   //--- Parse and set hidden-to-output weights if provided
   if(HiddenToOutputWeights != "" && ParseStringToArray(HiddenToOutputWeights, tempHiddenToOutputWeights, hiddenNeuronCount * outputNeuronCount)) {
      //--- Copy parsed weights to main array
      ArrayCopy(hiddenToOutputWeights, tempHiddenToOutputWeights);
      isHiddenToOutputWeightsSet = true;
      //--- Log weight initialization
      Print("Initialized hidden-to-output weights from input: ", HiddenToOutputWeights);
   }
   //--- Parse and set hidden biases if provided
   if(HiddenBiases != "" && ParseStringToArray(HiddenBiases, tempHiddenBiases, hiddenNeuronCount)) {
      //--- Copy parsed biases to main array
      ArrayCopy(hiddenLayerBiases, tempHiddenBiases);
      isHiddenBiasesSet = true;
      //--- Log bias initialization
      Print("Initialized hidden biases from input: ", HiddenBiases);
   }
   //--- Parse and set output biases if provided
   if(OutputBiases != "" && ParseStringToArray(OutputBiases, tempOutputBiases, outputNeuronCount)) {
      //--- Copy parsed biases to main array
      ArrayCopy(outputLayerBiases, tempOutputBiases);
      isOutputBiasesSet = true;
      //--- Log bias initialization
      Print("Initialized output biases from input: ", OutputBiases);
   }

   //--- Initialize input-to-hidden weights randomly if not set
   if(!isInputToHiddenWeightsSet) {
      for(int i = 0; i < ArraySize(inputToHiddenWeights); i++)
         inputToHiddenWeights[i] = (MathRand() / 32767.0) * 2 - 1;
   }
   //--- Initialize hidden-to-output weights randomly if not set
   if(!isHiddenToOutputWeightsSet) {
      for(int i = 0; i < ArraySize(hiddenToOutputWeights); i++)
         hiddenToOutputWeights[i] = (MathRand() / 32767.0) * 2 - 1;
   }
   //--- Initialize hidden biases randomly if not set
   if(!isHiddenBiasesSet) {
      for(int i = 0; i < ArraySize(hiddenLayerBiases); i++)
         hiddenLayerBiases[i] = (MathRand() / 32767.0) * 2 - 1;
   }
   //--- Initialize output biases randomly if not set
   if(!isOutputBiasesSet) {
      for(int i = 0; i < ArraySize(outputLayerBiases); i++)
         outputLayerBiases[i] = (MathRand() / 32767.0) * 2 - 1;
   }
}

// Sigmoid activation function
double CNeuralNetwork::Sigmoid(double x) {
   //--- Compute and return sigmoid value
   return 1.0 / (1.0 + MathExp(-x));
}

// Set input
void CNeuralNetwork::SetInput(double &inputs[]) {
   //--- Check for input array size mismatch
   if(ArraySize(inputs) != inputNeuronCount) {
      Print("Error: Input array size mismatch. Expected: ", inputNeuronCount, ", Got: ", ArraySize(inputs));
      return;
   }
   //--- Copy inputs to input layer
   ArrayCopy(inputLayer, inputs);
}

// Forward propagation
void CNeuralNetwork::ForwardPropagate() {
   //--- Compute hidden layer values
   for(int j = 0; j < hiddenNeuronCount; j++) {
      double sum = 0;
      //--- Calculate weighted sum for hidden neuron
      for(int i = 0; i < inputNeuronCount; i++)
         sum += inputLayer[i] * inputToHiddenWeights[i * hiddenNeuronCount + j];
      //--- Apply sigmoid activation
      hiddenLayer[j] = Sigmoid(sum + hiddenLayerBiases[j]);
   }
   //--- Compute output layer values
   for(int j = 0; j < outputNeuronCount; j++) {
      double sum = 0;
      //--- Calculate weighted sum for output neuron
      for(int i = 0; i < hiddenNeuronCount; i++)
         sum += hiddenLayer[i] * hiddenToOutputWeights[i * outputNeuronCount + j];
      //--- Apply sigmoid activation
      outputLayer[j] = Sigmoid(sum + outputLayerBiases[j]);
   }
}

// Get output
void CNeuralNetwork::GetOutput(double &outputs[]) {
   //--- Resize output array
   ArrayResize(outputs, outputNeuronCount);
   //--- Copy output layer to outputs
   ArrayCopy(outputs, outputLayer);
}

// Backpropagation
void CNeuralNetwork::Backpropagate(const double &targets[]) {
   //--- Calculate output layer deltas
   for(int i = 0; i < outputNeuronCount; i++) {
      double output = outputLayer[i];
      //--- Compute delta for output neuron
      outputDeltas[i] = output * (1 - output) * (targets[i] - output);
   }
   //--- Calculate hidden layer deltas
   for(int i = 0; i < hiddenNeuronCount; i++) {
      double error = 0;
      //--- Sum weighted errors from output layer
      for(int j = 0; j < outputNeuronCount; j++)
         error += outputDeltas[j] * hiddenToOutputWeights[i * outputNeuronCount + j];
      double output = hiddenLayer[i];
      //--- Compute delta for hidden neuron
      hiddenDeltas[i] = output * (1 - output) * error;
   }
   //--- Update hidden-to-output weights
   for(int i = 0; i < hiddenNeuronCount; i++) {
      for(int j = 0; j < outputNeuronCount; j++) {
         int idx = i * outputNeuronCount + j;
         //--- Adjust weight based on learning rate and delta
         hiddenToOutputWeights[idx] += currentLearningRate * outputDeltas[j] * hiddenLayer[i];
      }
   }
   //--- Update input-to-hidden weights
   for(int i = 0; i < inputNeuronCount; i++) {
      for(int j = 0; j < hiddenNeuronCount; j++) {
         int idx = i * hiddenNeuronCount + j;
         //--- Adjust weight based on learning rate and delta
         inputToHiddenWeights[idx] += currentLearningRate * hiddenDeltas[j] * inputLayer[i];
      }
   }
   //--- Update hidden biases
   for(int i = 0; i < hiddenNeuronCount; i++)
      //--- Adjust bias based on learning rate and delta
      hiddenLayerBiases[i] += currentLearningRate * hiddenDeltas[i];
   //--- Update output biases
   for(int i = 0; i < outputNeuronCount; i++)
      //--- Adjust bias based on learning rate and delta
      outputLayerBiases[i] += currentLearningRate * outputDeltas[i];
}

// Resize network (adjust hidden neurons)
void CNeuralNetwork::ResizeNetwork(int newHiddenNeurons) {
   //--- Clamp new neuron count within bounds
   newHiddenNeurons = MathMax(MinHiddenNeurons, MathMin(newHiddenNeurons, MaxHiddenNeurons));
   //--- Check if resizing is necessary
   if(newHiddenNeurons == hiddenNeuronCount)
      return;
   //--- Log resizing information
   Print("Resizing network. New hidden neurons: ", newHiddenNeurons, ", Previous: ", hiddenNeuronCount);
   //--- Update hidden neuron count
   hiddenNeuronCount = newHiddenNeurons;
   //--- Resize hidden layer array
   ArrayResize(hiddenLayer, hiddenNeuronCount);
   //--- Resize input-to-hidden weights array
   ArrayResize(inputToHiddenWeights, inputNeuronCount * hiddenNeuronCount);
   //--- Resize hidden-to-output weights array
   ArrayResize(hiddenToOutputWeights, hiddenNeuronCount * outputNeuronCount);
   //--- Resize hidden biases array
   ArrayResize(hiddenLayerBiases, hiddenNeuronCount);
   //--- Resize hidden deltas array
   ArrayResize(hiddenDeltas, hiddenNeuronCount);
   //--- Reinitialize weights
   InitializeWeights();
}

Здесь мы реализуем критические компоненты нейронной сети. Создадим конструктор CNeuralNetwork, принимающий inputs, hidden и outputs для установки inputNeuronCount, outputNeuronCount и hiddenNeuronCount, ограничивая последний значением от MinHiddenNeurons до MaxHiddenNeurons. Нам не нужно тратить много времени на объяснение прототипа класса, так как мы уже делали нечто подобное в предыдущей части.

Далее инициализируем currentLearningRate значением MinLearningRate, изменим размеры таких массивов, как inputLayer, hiddenLayer, outputLayer, inputToHiddenWeights, hiddenToOutputWeights, hiddenLayerBiases, outputLayerBiases, accuracyHistory и errorHistory, используя ArrayResize, а также вызовем InitializeWeights и InitializeTraining для настройки сети.

Введем функцию InitializeTraining для изменения размеров массивов outputDeltas и hiddenDeltas для обратного распространения, обеспечивая правильное хранение градиентов ошибок. Функция InitializeWeights задает веса и смещения, используя функцию ParseStringToArray для загрузки inputToHiddenWeights, hiddenToOutputWeights, hiddenLayerBiases и outputLayerBiases из входных данных, таких как InputToHiddenWeights, если они предоставлены, или рандомизирует их с помощью MathRand между -1 и 1, если нет, записывая действия с помощью Print. Используем функцию Sigmoid для вычисления значения активации сигмоиды для заданного x, используя функцию MathExp.

Функция SetInput копирует "входные данные" в inputLayer после проверки размера с помощью ArraySize, записывая ошибки с помощью Print в случае несоответствия. Создадим функцию ForwardPropagate для вычисления значений hiddenLayer и outputLayer путем применения взвешенных сумм и активации Sigmoid с hiddenLayerBiases и outputLayerBiases. Функция GetOutput изменяет размер выходных данных с помощью функции ArrayResize и копирует значения outputLayer с помощью ArrayCopy.

Реализуем функцию Backpropagate для вычисления outputDeltas и hiddenDeltas из targets, обновляя hiddenToOutputWeights, inputToHiddenWeights, hiddenLayerBiases и outputLayerBiases с использованием currentLearningRate. Наконец, функция ResizeNetwork корректирует hiddenNeuronCount в заданных пределах, изменяет размер массивов, таких как hiddenLayer и hiddenDeltas, и повторно инициализирует веса с помощью InitializeWeights, логируя изменения с помощью Print. Затем нам нужно скорректировать скорость обучения на основе тенденции ошибок.

// Adjust learning rate based on error trend
void CNeuralNetwork::AdjustLearningRate() {
   //--- Check if enough history exists
   if(historyRecordCount < 2) return;
   //--- Get last and previous errors
   double lastError = errorHistory[historyRecordCount - 1];
   double prevError = errorHistory[historyRecordCount - 2];
   //--- Calculate error difference
   double errorDiff = lastError - prevError;
   //--- Increase learning rate if error decreased
   if(lastError < prevError)
      currentLearningRate = MathMin(currentLearningRate * 1.05, MaxLearningRate);
   //--- Decrease learning rate if error increased significantly
   else if(lastError > prevError * 1.2)
      currentLearningRate = MathMax(currentLearningRate * 0.9, MinLearningRate);
   //--- Slightly decrease learning rate otherwise
   else
      currentLearningRate = MathMax(currentLearningRate * 0.99, MinLearningRate);
   //--- Log learning rate adjustment
   Print("Adjusted learning rate to: ", currentLearningRate, ", Last Error: ", lastError, ", Prev Error: ", prevError, ", Error Diff: ", errorDiff);
}

Реализуем механизм адаптивного темпа обучения, создав функцию AdjustLearningRate внутри класса CNeuralNetwork. Необходимо проверить, что historyRecordCount равен не менее 2, чтобы обеспечить достаточность данных в массиве errorHistory. Если это условие не выполняется, выходим, чтобы избежать невалидных корректировок. Извлекаем последнюю ошибку как lastError и предыдущую ошибку как prevError из errorHistory, затем вычисляем их разницу в errorDiff для оценки результатов обучения.

Используем функцию MathMin для увеличения currentLearningRate на 5%, если lastError меньше prevError, с ограничением значения MaxLearningRate, или используйте функцию MathMax для его уменьшения на 10%, если lastError превышает prevError на 20%, обеспечивая при этом, чтобы оно оставалось выше MinLearningRate. В противном случае немного уменьшим currentLearningRate на 1% с помощью MathMax. Наконец, используем функцию Print для логирования скорректированного currentLearningRate, lastError, prevError и errorDiff, обеспечивая динамическую оптимизацию обучения нейронной сети. Затем используем индикатор ATR для динамического расчета количества скрытых нейронов, чтобы можно было корректировать его в зависимости от волатильности.

// Calculate dynamic number of hidden neurons based on ATR
double CNeuralNetwork::CalculateDynamicNeurons() {
   double atrValues[];
   //--- Set ATR array as series
   ArraySetAsSeries(atrValues, true);
   //--- Copy ATR buffer
   if(CopyBuffer(atrIndicatorHandle, 0, 0, 10, atrValues) < 10) {
      Print("Error: Failed to copy ATR for dynamic neurons. Using default: ", hiddenNeuronCount);
      return hiddenNeuronCount;
   }
   //--- Calculate average ATR
   double avgATR = 0;
   for(int i = 0; i < 10; i++)
      avgATR += atrValues[i];
   avgATR /= 10;
   //--- Get current close price
   double closePrice = iClose(_Symbol, PERIOD_CURRENT, 0);
   //--- Check for valid close price
   if(MathAbs(closePrice) < 0.000001) {
      Print("Error: Invalid close price for ATR ratio. Using default: ", hiddenNeuronCount);
      return hiddenNeuronCount;
   }
   //--- Calculate ATR ratio
   double atrRatio = atrValues[0] / closePrice;
   //--- Compute new neuron count
   int newNeurons = MinHiddenNeurons + (int)((MaxHiddenNeurons - MinHiddenNeurons) * MathMin(atrRatio * 100, 1.0));
   //--- Return clamped neuron count
   return MathMax(MinHiddenNeurons, MathMin(newNeurons, MaxHiddenNeurons));
}

Для динамической корректировки количества скрытых нейронов в зависимости от волатильности рынка объявим массив atrValues и используем функцию ArraySetAsSeries для настройки его как массива временных рядов, а затем применим функцию CopyBuffer для извлечения 10 баров данных об ATR из atrIndicatorHandle в atrValues. Если функция CopyBuffer извлекает менее 10 значений, используем функцию Print для регистрации ошибки и возвращаем значение по умолчанию hiddenNeuronCount. Нам крайне необходимо представить массив в виде временного ряда, чтобы самые свежие полученные данные можно было сопоставить с начальными индексами и использовать в качестве первых.

ВРЕМЕННОЙ РЯД ДАННЫХ БАРОВ

Вычислим средний ATR, суммируя atrValues за 10 баров и разделив на 10. Сохраним результат в avgATR. Используем функцию iClose для получения текущей цены закрытия в closePrice для _Symbol и PERIOD_CURRENT. Если closePrice близка к нулю, используем функцию Print для логирования ошибки и вернем hiddenNeuronCount. Вычислим коэффициент ATR как atrValues[0], деленный на closePrice, затем вычислим newNeurons, масштабируя диапазон между MinHiddenNeurons и MaxHiddenNeurons с помощью функции MathMin на основе масштабированного atrRatio. Наконец используем функции MathMax и MathMin для ограничения и возврата скорректированного количества нейронов, обеспечивая динамическую адаптацию к рыночным условиям. Теперь мы можем определить остальные функции обучения и обновления, как показано ниже.

// Train network on historical data
double CNeuralNetwork::TrainOnHistoricalData(TrainingData &data[]) {
   const int maxEpochs = 100; //--- Maximum training epochs
   const double targetError = 0.01; //--- Target error threshold
   double accuracy = 0; //--- Training accuracy
   //--- Reset learning rate
   currentLearningRate = MinLearningRate;
   //--- Iterate through epochs
   for(int epoch = 0; epoch < maxEpochs; epoch++) {
      double totalError = 0; //--- Total error for epoch
      int correctPredictions = 0; //--- Count of correct predictions
      //--- Process each training sample
      for(int i = 0; i < ArraySize(data); i++) {
         //--- Check target array size
         if(ArraySize(data[i].targetValues) != outputNeuronCount) {
            Print("Error: Mismatch in targets size for training data at index ", i);
            continue;
         }
         //--- Set input values
         SetInput(data[i].inputValues);
         //--- Perform forward propagation
         ForwardPropagate();
         double error = 0;
         //--- Calculate error
         for(int j = 0; j < outputNeuronCount; j++)
            error += MathPow(data[i].targetValues[j] - outputLayer[j], 2);
         totalError += error;
         //--- Check prediction correctness
         if((outputLayer[0] > outputLayer[1] && data[i].targetValues[0] > data[i].targetValues[1]) ||
               (outputLayer[0] < outputLayer[1] && data[i].targetValues[0] < data[i].targetValues[1]))
            correctPredictions++;
         //--- Perform backpropagation
         Backpropagate(data[i].targetValues);
      }
      //--- Calculate accuracy
      accuracy = (double)correctPredictions / ArraySize(data);
      //--- Update training error
      trainingError = totalError / ArraySize(data);
      //--- Update history
      if(historyRecordCount < MAX_HISTORY_SIZE) {
         accuracyHistory[historyRecordCount] = accuracy;
         errorHistory[historyRecordCount] = trainingError;
         historyRecordCount++;
      } else {
         //--- Shift history arrays
         for(int i = 1; i < MAX_HISTORY_SIZE; i++) {
            accuracyHistory[i - 1] = accuracyHistory[i];
            errorHistory[i - 1] = errorHistory[i];
         }
         //--- Add new values
         accuracyHistory[MAX_HISTORY_SIZE - 1] = accuracy;
         errorHistory[MAX_HISTORY_SIZE - 1] = trainingError;
      }
      //--- Log error history update
      Print("Error history updated: ", errorHistory[historyRecordCount - 1]);
      //--- Adjust learning rate
      AdjustLearningRate();
      //--- Log progress every 10 epochs
      if(epoch % 10 == 0)
         Print("Epoch ", epoch, ": Error = ", trainingError, ", Accuracy = ", accuracy);
      //--- Check for early stopping
      if(trainingError < targetError && accuracy >= MinPredictionAccuracy)
         break;
   }
   //--- Return final accuracy
   return accuracy;
}

// Update network with recent data
void CNeuralNetwork::UpdateNetworkWithRecentData() {
   const int recentBarCount = 10; //--- Number of recent bars to process
   TrainingData recentData[];
   //--- Collect recent training data
   if(!CollectTrainingData(recentData, recentBarCount))
      return;
   //--- Process each recent data sample
   for(int i = 0; i < ArraySize(recentData); i++) {
      //--- Set input values
      SetInput(recentData[i].inputValues);
      //--- Perform forward propagation
      ForwardPropagate();
      //--- Perform backpropagation
      Backpropagate(recentData[i].targetValues);
   }
}

// Get recent accuracy
double CNeuralNetwork::GetRecentAccuracy() {
   //--- Check if history exists
   if(historyRecordCount == 0) return 0.0;
   //--- Return most recent accuracy
   return accuracyHistory[historyRecordCount - 1];
}

// Get recent error
double CNeuralNetwork::GetRecentError() {
   //--- Check if history exists
   if(historyRecordCount == 0) return 0.0;
   //--- Return most recent error
   return errorHistory[historyRecordCount - 1];
}

// Check if retraining is needed
bool CNeuralNetwork::ShouldRetrain() {
   //--- Check if enough history exists
   if(historyRecordCount < 2) return false;
   //--- Get recent metrics
   double recentAccuracy = GetRecentAccuracy();
   double recentError = GetRecentError();
   double prevError = errorHistory[historyRecordCount - 2];
   //--- Determine if retraining is needed
   return (recentAccuracy < MinPredictionAccuracy || recentError > prevError * 1.5);
}

Здесь мы реализуем ключевые механизмы обучения и обновления нейронной сети. Создадим функцию TrainOnHistoricalData для обучения сети с использованием массива структур TrainingData, устанавливая maxEpochs в 100 и targetError в 0.01 и сбрасывая currentLearningRate до MinLearningRate. Для каждой эпохи мы перебираем "данные", проверяем размер data[i].targetValues на соответствие outputNeuronCount и используем функцию SetInput для загрузки data[i].inputValues, после чего используем функцию ForwardPropagate для вычисления прогнозов. Вычисляем ошибку, используя MathPow для сравнения outputLayer и data[i].targetValues, отслеживания correctPredictions и применения функции Backpropagate с data[i].targetValues.

Обновим accuracy и trainingError, сохраним их в accuracyHistory и errorHistory с использованием historyRecordCount, сдвинем массивы, если они заполнены, зарегистрируем обновления с помощью функции Print и используем функцию AdjustLearningRate для оптимизации обучения, останавливаясь раньше, если trainingError и accuracy соответствуют targetError и MinPredictionAccuracy.

Разработаем функцию UpdateNetworkWithRecentData для уточнения работы сети, установив recentBarCount равным 10, используя функцию CollectTrainingData для заполнения recentData и итеративно применяя SetInput, ForwardPropagate и Backpropagate для каждого образца. Функция GetRecentAccuracy возвращает последнее значение из accuracyHistory или 0.0, если historyRecordCount равен нулю, а функция GetRecentError делает то же самое для errorHistory.

Создадим функцию ShouldRetrain, чтобы проверить, составляет ли значение historyRecordCount как минимум 2, используя GetRecentAccuracy и GetRecentError для сравнения recentAccuracy и recentError с MinPredictionAccuracy и значением, в 1.5 раза превышающим предыдущее значение из errorHistory, возвращая true, если требуется переобучение. Теперь мы можем создать экземпляр класса, который будем использовать для фактической реализации.

// Global neural network instance
CNeuralNetwork *neuralNetwork; //--- Global neural network object

Установим глобальный указатель для нейронной сети, создав указатель neuralNetwork на экземпляр класса CNeuralNetwork. Этот глобальный объект обеспечит централизованный доступ к функциональности нейронной сети, включая обучение, прямое распространение, обратное распространение и корректировки адаптивного темпа обучения, гарантируя бесшовную интеграцию во все операции Советника.

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

// Prepare inputs from market data
void PrepareInputs(double &inputs[]) {
   //--- Resize inputs array if necessary
   if(ArraySize(inputs) != INPUT_NEURON_COUNT)
      ArrayResize(inputs, INPUT_NEURON_COUNT);
   double ma20Values[], ma50Values[], rsiValues[], atrValues[];
   //--- Set arrays as series
   ArraySetAsSeries(ma20Values, true);
   ArraySetAsSeries(ma50Values, true);
   ArraySetAsSeries(rsiValues, true);
   ArraySetAsSeries(atrValues, true);
   //--- Copy MA20 buffer
   if(CopyBuffer(ma20IndicatorHandle, 0, 0, 2, ma20Values) <= 0) {
      Print("Error: Failed to copy MA20 buffer. Error code: ", GetLastError());
      return;
   }
   //--- Copy MA50 buffer
   if(CopyBuffer(ma50IndicatorHandle, 0, 0, 2, ma50Values) <= 0) {
      Print("Error: Failed to copy MA50 buffer. Error code: ", GetLastError());
      return;
   }
   //--- Copy RSI buffer
   if(CopyBuffer(rsiIndicatorHandle, 0, 0, 2, rsiValues) <= 0) {
      Print("Error: Failed to copy RSI buffer. Error code: ", GetLastError());
      return;
   }
   //--- Copy ATR buffer
   if(CopyBuffer(atrIndicatorHandle, 0, 0, 2, atrValues) <= 0) {
      Print("Error: Failed to copy ATR buffer. Error code: ", GetLastError());
      return;
   }
   //--- Check array sizes
   if(ArraySize(ma20Values) < 2 || ArraySize(ma50Values) < 2 || ArraySize(rsiValues) < 2 || ArraySize(atrValues) < 2) {
      Print("Error: Insufficient data in indicator arrays");
      return;
   }
   //--- Get current market prices
   double closePrice = iClose(_Symbol, PERIOD_CURRENT, 0);
   double openPrice = iOpen(_Symbol, PERIOD_CURRENT, 0);
   double highPrice = iHigh(_Symbol, PERIOD_CURRENT, 0);
   double lowPrice = iLow(_Symbol, PERIOD_CURRENT, 0);
   //--- Calculate input features
   inputs[0] = (MathAbs(openPrice) > 0.000001) ? (closePrice - openPrice) / openPrice : 0;
   inputs[1] = (MathAbs(lowPrice) > 0.000001) ? (highPrice - lowPrice) / lowPrice : 0;
   inputs[2] = (MathAbs(ma20Values[0]) > 0.000001) ? (closePrice - ma20Values[0]) / ma20Values[0] : 0;
   inputs[3] = (MathAbs(ma50Values[0]) > 0.000001) ? (ma20Values[0] - ma50Values[0]) / ma50Values[0] : 0;
   inputs[4] = rsiValues[0] / 100.0;
   double highLowRange = highPrice - lowPrice;
   if(MathAbs(highLowRange) > 0.000001) {
      inputs[5] = (closePrice - lowPrice) / highLowRange;
      inputs[7] = MathAbs(closePrice - openPrice) / highLowRange;
      inputs[8] = (highPrice - closePrice) / highLowRange;
      inputs[9] = (closePrice - lowPrice) / highLowRange;
   } else {
      inputs[5] = 0;
      inputs[7] = 0;
      inputs[8] = 0;
      inputs[9] = 0;
   }
   inputs[6] = (MathAbs(closePrice) > 0.000001) ? atrValues[0] / closePrice : 0;
   //--- Log input preparation
   Print("Prepared inputs. Size: ", ArraySize(inputs));
}

// Collect training data
bool CollectTrainingData(TrainingData &data[], int barCount) {
   //--- Check output neuron count
   if(OUTPUT_NEURON_COUNT != 2) {
      Print("Error: OUTPUT_NEURON_COUNT must be 2 for binary classification.");
      return false;
   }
   //--- Resize data array
   ArrayResize(data, barCount);
   double ma20Values[], ma50Values[], rsiValues[], atrValues[];
   //--- Set arrays as series
   ArraySetAsSeries(ma20Values, true);
   ArraySetAsSeries(ma50Values, true);
   ArraySetAsSeries(rsiValues, true);
   ArraySetAsSeries(atrValues, true);
   //--- Copy MA20 buffer
   if(CopyBuffer(ma20IndicatorHandle, 0, 0, barCount + 1, ma20Values) < barCount + 1) {
      Print("Error: Failed to copy MA20 buffer for training. Error code: ", GetLastError());
      return false;
   }
   //--- Copy MA50 buffer
   if(CopyBuffer(ma50IndicatorHandle, 0, 0, barCount + 1, ma50Values) < barCount + 1) {
      Print("Error: Failed to copy MA50 buffer for training. Error code: ", GetLastError());
      return false;
   }
   //--- Copy RSI buffer
   if(CopyBuffer(rsiIndicatorHandle, 0, 0, barCount + 1, rsiValues) < barCount + 1) {
      Print("Error: Failed to copy RSI buffer for training. Error code: ", GetLastError());
      return false;
   }
   //--- Copy ATR buffer
   if(CopyBuffer(atrIndicatorHandle, 0, 0, barCount + 1, atrValues) < barCount + 1) {
      Print("Error: Failed to copy ATR buffer for training. Error code: ", GetLastError());
      return false;
   }
   MqlRates priceData[];
   //--- Set rates array as series
   ArraySetAsSeries(priceData, true);
   //--- Copy price data
   if(CopyRates(_Symbol, PERIOD_CURRENT, 0, barCount + 1, priceData) < barCount + 1) {
      Print("Error: Failed to copy rates for training. Error code: ", GetLastError());
      return false;
   }
   //--- Process each bar
   for(int i = 0; i < barCount; i++) {
      //--- Resize input and target arrays
      ArrayResize(data[i].inputValues, INPUT_NEURON_COUNT);
      ArrayResize(data[i].targetValues, OUTPUT_NEURON_COUNT);
      //--- Get price data
      double closePrice = priceData[i].close;
      double openPrice = priceData[i].open;
      double highPrice = priceData[i].high;
      double lowPrice = priceData[i].low;
      double highLowRange = highPrice - lowPrice;
      //--- Calculate input features
      data[i].inputValues[0] = (MathAbs(openPrice) > 0.000001) ? (closePrice - openPrice) / openPrice : 0;
      data[i].inputValues[1] = (MathAbs(lowPrice) > 0.000001) ? (highPrice - lowPrice) / lowPrice : 0;
      data[i].inputValues[2] = (MathAbs(ma20Values[i]) > 0.000001) ? (closePrice - ma20Values[i]) / ma20Values[i] : 0;
      data[i].inputValues[3] = (MathAbs(ma50Values[i]) > 0.000001) ? (ma20Values[i] - ma50Values[i]) / ma50Values[i] : 0;
      data[i].inputValues[4] = rsiValues[i] / 100.0;
      if(MathAbs(highLowRange) > 0.000001) {
         data[i].inputValues[5] = (closePrice - lowPrice) / highLowRange;
         data[i].inputValues[7] = MathAbs(closePrice - openPrice) / highLowRange;
         data[i].inputValues[8] = (highPrice - closePrice) / highLowRange;
         data[i].inputValues[9] = (closePrice - lowPrice) / highLowRange;
      }
      data[i].inputValues[6] = (MathAbs(closePrice) > 0.000001) ? atrValues[i] / closePrice : 0;
      //--- Set target values based on price movement
      if(i < barCount - 1) {
         double futureClose = priceData[i + 1].close;
         double priceChange = futureClose - closePrice;
         if(priceChange > 0) {
            data[i].targetValues[0] = 1;
            data[i].targetValues[1] = 0;
         } else {
            data[i].targetValues[0] = 0;
            data[i].targetValues[1] = 1;
         }
      } else {
         data[i].targetValues[0] = 0;
         data[i].targetValues[1] = 0;
      }
   }
   //--- Return success
   return true;
}

Здесь мы реализуем подготовку данных, создавая функцию PrepareInputs для генерации входных признаков для нейронной сети. Используем функцию ArrayResize, чтобы убедиться в том, что массив inputs совпадает с INPUT_NEURON_COUNT, затем объявим массивы ma20Values, ma50Values, rsiValues и atrValues, установив их в качестве временных рядов с помощью функции ArraySetAsSeries. Используем функцию CopyBuffer для извлечения двух баров данных из ma20IndicatorHandle, ma50IndicatorHandle, rsiIndicatorHandle и atrIndicatorHandle, регистрируя ошибки с помощью функции Print и завершая работу в случае любой неудачи.

Проверим размеры массивов с помощью ArraySize, регистрируя ошибки с помощью Print, если данных недостаточно, и используем iClose, iOpen, iHigh и iLow для получения текущих цен в closePrice, openPrice, highPrice и lowPrice для _Symbol и PERIOD_CURRENT. Рассчитаем входные характеристики для inputs, включая нормализованные разности цен, отклонения скользящих средних, RSI, масштабированный до 0-1, и ATR относительно closePrice, обрабатывая деление на ноль с помощью проверок MathAbs, и вычислим характеристики на основе диапазона для highLowRange. Зарегистрируем успешные операции с помощью Print и ArraySize.

Создадим функцию CollectTrainingData для подготовки обучающих данных в массиве структур TrainingData для barCount баров. Убедимся, что OUTPUT_NEURON_COUNT равен 2, используем ArrayResize для задания размера data и используем CopyBuffer для получения данных из хендлов индикаторов и CopyRates для данных о ценах в priceData, регистрируя ошибки с помощью Print при любой неудаче. Для каждого бара используем ArrayResize, чтобы установить data[i].inputValues и data[i].targetValues, вычисляем входные признаки аналогично PrepareInputs и устанавливаем целевые значения в data[i].targetValues на основе движения цены в priceData, возвращая true при успехе. Затем нам нужно обучить сеть на основе полученных данных.

// Train the neural network
bool TrainNetwork() {
   //--- Log training start
   Print("Starting neural network training...");
   TrainingData trainingData[];
   //--- Collect training data
   if(!CollectTrainingData(trainingData, TrainingBarCount)) {
      Print("Failed to collect training data");
      return false;
   }
   //--- Train network
   double accuracy = neuralNetwork.TrainOnHistoricalData(trainingData);
   //--- Log training completion
   Print("Training completed. Final accuracy: ", accuracy);
   //--- Return training success
   return (accuracy >= MinPredictionAccuracy);
}

Создадим функцию TrainNetwork и используем функцию Print для записи начала обучения, затем объявим массив trainingData структуры TrainingData для хранения входных и целевых значений. Вызовем функцию CollectTrainingData, чтобы заполнить trainingData барами TrainingBarCount, запишем ошибку с помощью Print и возвращаем false, если сбор данных не удался.

Используем функцию TrainOnHistoricalData объекта neuralNetwork для обучения сети, сохраняя результат в переменной accuracy, регистрируя завершение с помощью функции Print, включающей accuracy, и возвращая true, если accuracy соответствует или превышает MinPredictionAccuracy, обеспечивая адекватное обучение сети для торговли. Наконец, мы можем создать функцию для проверки торговых сигналов, как показано ниже.

// Validate Stop Loss and Take Profit levels
bool CheckStopLossTakeprofit(ENUM_ORDER_TYPE orderType, double price, double stopLoss, double takeProfit) {
   //--- Get minimum stop level
   double stopLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * _Point;
   //--- Validate buy order
   if(orderType == ORDER_TYPE_BUY) {
      //--- Check stop loss distance
      if(MathAbs(price - stopLoss) < stopLevel) {
         Print("Buy Stop Loss too close. Minimum distance: ", stopLevel);
         return false;
      }
      //--- Check take profit distance
      if(MathAbs(takeProfit - price) < stopLevel) {
         Print("Buy Take Profit too close. Minimum distance: ", stopLevel);
         return false;
      }
   }
   //--- Validate sell order
   else if(orderType == ORDER_TYPE_SELL) {
      //--- Check stop loss distance
      if(MathAbs(stopLoss - price) < stopLevel) {
         Print("Sell Stop Loss too close. Minimum distance: ", stopLevel);
         return false;
      }
      //--- Check take profit distance
      if(MathAbs(price - takeProfit) < stopLevel) {
         Print("Sell Take Profit too close. Minimum distance: ", stopLevel);
         return false;
      }
   }
   //--- Return validation success
   return true;
}

Здесь мы просто создаем булеву функцию для проверки уровней Stop Loss и Take Profit, чтобы избежать потенциальных ошибок из-за ограничений брокера. Теперь мы можем инициализировать программу в обработчике событий OnInit, технически инициализируя индикаторы и экземпляры нейронного класса для выполнения наиболее трудоемкой работы, как показано ниже.

// Expert initialization function
int OnInit() {
   //--- Initialize 20-period MA indicator
   ma20IndicatorHandle = iMA(_Symbol, PERIOD_CURRENT, 20, 0, MODE_SMA, PRICE_CLOSE);
   //--- Initialize 50-period MA indicator
   ma50IndicatorHandle = iMA(_Symbol, PERIOD_CURRENT, 50, 0, MODE_SMA, PRICE_CLOSE);
   //--- Initialize RSI indicator
   rsiIndicatorHandle = iRSI(_Symbol, PERIOD_CURRENT, 14, PRICE_CLOSE);
   //--- Initialize ATR indicator
   atrIndicatorHandle = iATR(_Symbol, PERIOD_CURRENT, 14);
   //--- Check MA20 handle
   if(ma20IndicatorHandle == INVALID_HANDLE)
      Print("Error: Failed to initialize MA20 handle. Error code: ", GetLastError());
   //--- Check MA50 handle
   if(ma50IndicatorHandle == INVALID_HANDLE)
      Print("Error: Failed to initialize MA50 handle. Error code: ", GetLastError());
   //--- Check RSI handle
   if(rsiIndicatorHandle == INVALID_HANDLE)
      Print("Error: Failed to initialize RSI handle. Error code: ", GetLastError());
   //--- Check ATR handle
   if(atrIndicatorHandle == INVALID_HANDLE)
      Print("Error: Failed to initialize ATR handle. Error code: ", GetLastError());
   //--- Check for any invalid handles
   if(ma20IndicatorHandle == INVALID_HANDLE || ma50IndicatorHandle == INVALID_HANDLE ||
         rsiIndicatorHandle == INVALID_HANDLE || atrIndicatorHandle == INVALID_HANDLE) {
      Print("Error initializing indicators");
      return INIT_FAILED;
   }
   //--- Create neural network instance
   neuralNetwork = new CNeuralNetwork(INPUT_NEURON_COUNT, MinHiddenNeurons, OUTPUT_NEURON_COUNT);
   //--- Check neural network creation
   if(neuralNetwork == NULL) {
      Print("Failed to create neural network");
      return INIT_FAILED;
   }
   //--- Log initialization
   Print("Initializing neural network...");
   //--- Return success
   return(INIT_SUCCEEDED);
}

Реализуем логику инициализации в функции OnInit. Используем функцию iMA для инициализации ma20IndicatorHandle и ma50IndicatorHandle для 20-периодных и 50-периодных простых скользящих средних на _Symbol и PERIOD_CURRENT с PRICE_CLOSE, а также функции iRSI и iATR для установки rsiIndicatorHandle и atrIndicatorHandle для 14-периодных индикаторов RSI и ATR. Проверяем каждый хэндл на соответствие INVALID_HANDLE, регистрируем ошибки с помощью Print и GetLastError, если проблемы возникают с любым из них, и возвращаем INIT_FAILED, если инициализация какого-либо индикатора завершится неудачей.

Создадим новый экземпляр класса CNeuralNetwork для neuralNetwork, используя значения INPUT_NEURON_COUNT, MinHiddenNeurons и OUTPUT_NEURON_COUNT для настройки структуры сети. Если значение neuralNetwork равно NULL, используем функцию Print для регистрации ошибки и возвращаем INIT_FAILED. Наконец, мы регистрируем успешную инициализацию с помощью Print и возвращаем INIT_SUCCEEDED, обеспечивая правильную настройку советника для торговли. После запуска программы мы получим следующий результат.

ИНИЦИАЛИЗАЦИЯ

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

// Expert deinitialization function
void OnDeinit(const int reason) {
   //--- Release MA20 indicator handle
   if(ma20IndicatorHandle != INVALID_HANDLE) IndicatorRelease(ma20IndicatorHandle);
   //--- Release MA50 indicator handle
   if(ma50IndicatorHandle != INVALID_HANDLE) IndicatorRelease(ma50IndicatorHandle);
   //--- Release RSI indicator handle
   if(rsiIndicatorHandle != INVALID_HANDLE) IndicatorRelease(rsiIndicatorHandle);
   //--- Release ATR indicator handle
   if(atrIndicatorHandle != INVALID_HANDLE) IndicatorRelease(atrIndicatorHandle);
   //--- Delete neural network instance
   if(neuralNetwork != NULL) delete neuralNetwork;
   //--- Log deinitialization
   Print("Expert Advisor deinitialized - ", EnumToString((ENUM_INIT_RETCODE)reason));
}

Проведем очистку в функции OnDeinit, где мы используем функцию IndicatorRelease для освобождения ресурсов для ma20IndicatorHandle, ma50IndicatorHandle, rsiIndicatorHandle и atrIndicatorHandle, если каждый из них не равен INVALID_HANDLE, обеспечивая корректное освобождение хэндлов индикаторов скользящей средней, RSI и ATR. Проверяем, не равен ли neuralNetwork NULL, и используем оператор delete для освобождения экземпляра класса CNeuralNetwork, очищая нейронную сеть. Наконец, используем функцию EnumToString для регистрации причины деинициализации, подтверждающей надлежащее высвобождение ресурсов советника. Если не удалить экземпляр класса, это приведет к утечкам памяти, как показано ниже.

УТЕЧКА ПАМЯТИ

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

// Expert tick function
void OnTick() {
   static datetime lastBarTime = 0; //--- Track last processed bar time
   //--- Get current bar time
   datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);
   //--- Skip if same bar
   if(lastBarTime == currentBarTime)
      return;
   //--- Update last bar time
   lastBarTime = currentBarTime;

   //--- Calculate dynamic neuron count
   int newNeuronCount = (int)neuralNetwork.CalculateDynamicNeurons();
   //--- Resize network if necessary
   if(newNeuronCount != neuralNetwork.GetHiddenNeurons())
      neuralNetwork.ResizeNetwork(newNeuronCount);

   //--- Check if retraining is needed
   if(TimeCurrent() - iTime(_Symbol, PERIOD_CURRENT, TrainingBarCount) >= 12 * 3600 || neuralNetwork.ShouldRetrain()) {
      //--- Log training start
      Print("Starting network training...");
      //--- Train network
      if(!TrainNetwork()) {
         Print("Training failed or insufficient accuracy");
         return;
      }
   }

   //--- Update network with recent data
   neuralNetwork.UpdateNetworkWithRecentData();

   //--- Check for open positions
   if(PositionsTotal() > 0) {
      //--- Iterate through positions
      for(int i = PositionsTotal() - 1; i >= 0; i--) {
         //--- Skip if position is for current symbol
         if(PositionGetSymbol(i) == _Symbol)
            return;
      }
   }

   //--- Prepare input data
   double currentInputs[];
   ArrayResize(currentInputs, INPUT_NEURON_COUNT);
   PrepareInputs(currentInputs);
   //--- Verify input array size
   if(ArraySize(currentInputs) != INPUT_NEURON_COUNT) {
      Print("Error: Inputs array not properly initialized. Size: ", ArraySize(currentInputs));
      return;
   }
   //--- Set network inputs
   neuralNetwork.SetInput(currentInputs);
   //--- Perform forward propagation
   neuralNetwork.ForwardPropagate();
   double outputValues[];
   //--- Resize output array
   ArrayResize(outputValues, OUTPUT_NEURON_COUNT);
   //--- Get network outputs
   neuralNetwork.GetOutput(outputValues);
   //--- Verify output array size
   if(ArraySize(outputValues) != OUTPUT_NEURON_COUNT) {
      Print("Error: Outputs array not properly initialized. Size: ", ArraySize(outputValues));
      return;
   }

   //--- Get market prices
   double askPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bidPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   //--- Calculate stop loss and take profit levels
   double buyStopLoss = NormalizeDouble(askPrice - StopLossPoints * _Point, _Digits);
   double buyTakeProfit = NormalizeDouble(askPrice + TakeProfitPoints * _Point, _Digits);
   double sellStopLoss = NormalizeDouble(bidPrice + StopLossPoints * _Point, _Digits);
   double sellTakeProfit = NormalizeDouble(bidPrice - TakeProfitPoints * _Point, _Digits);

   //--- Validate stop loss and take profit
   if(!CheckStopLossTakeprofit(ORDER_TYPE_BUY, askPrice, buyStopLoss, buyTakeProfit) ||
         !CheckStopLossTakeprofit(ORDER_TYPE_SELL, bidPrice, sellStopLoss, sellTakeProfit)) {
      return;
   }

   // Trading logic
   const double CONFIDENCE_THRESHOLD = 0.8; //--- Confidence threshold for trading
   //--- Check for buy signal
   if(outputValues[0] > CONFIDENCE_THRESHOLD && outputValues[1] < (1 - CONFIDENCE_THRESHOLD)) {
      //--- Set trade magic number
      tradeObject.SetExpertMagicNumber(123456);
      //--- Place buy order
      if(tradeObject.Buy(LotSize, _Symbol, askPrice, buyStopLoss, buyTakeProfit, "Neural Buy")) {
         //--- Log successful buy order
         Print("Buy order placed - Signal Strength: ", outputValues[0]);
      } else {
         //--- Log buy order failure
         Print("Buy order failed. Error: ", GetLastError());
      }
   }
   //--- Check for sell signal
   else if(outputValues[0] < (1 - CONFIDENCE_THRESHOLD) && outputValues[1] > CONFIDENCE_THRESHOLD) {
      //--- Set trade magic number
      tradeObject.SetExpertMagicNumber(123456);
      //--- Place sell order
      if(tradeObject.Sell(LotSize, _Symbol, bidPrice, sellStopLoss, sellTakeProfit, "Neural Sell")) {
         //--- Log successful sell order
         Print("Sell order placed - Signal Strength: ", outputValues[1]);
      } else {
         //--- Log sell order failure
         Print("Sell order failed. Error: ", GetLastError());
      }
   }
}
//+------------------------------------------------------------------+

Здесь мы реализуем основную торговую логику для стратегии нейронной сети внутри функции OnTick. Объявляем переменную lastBarTime, чтобы отслеживать последний обработанный бар, и используем функцию iTime для получения значения currentBarTime для _Symbol и PERIOD_CURRENT, выходя из программы, если статус не изменился, для обработки только новых баров. Обновим lastBarTime и используем функцию CalculateDynamicNeurons для вычисления newNeuronCount, вызывая функцию ResizeNetwork, если результат отличается от результата функции GetHiddenNeurons, чтобы скорректировать сеть.

Проверим, требуется ли переобучение, путем сравнения TimeCurrent минус iTime для TrainingBarCount с 12 часами или с помощью функции ShouldRetrain, затем используем функцию TrainNetwork для переобучения, завершив процесс в случае неудачи. Вызываем функцию UpdateNetworkWithRecentData для уточнения работы сети. Если PositionsTotal обозначает открытые позиции, используем функцию PositionGetSymbol, чтобы пропустить сделку, если таковая имеется для _Symbol. Объявим переменную currentInputs, изменим ее размер с помощью ArrayResize до INPUT_NEURON_COUNT и используем функцию PrepareInputs для ее заполнения, проверяя размер с помощью ArraySize и регистрируя ошибки с помощью Print.

Используем функцию SetInput для загрузки currentInputs, вызываем функцию ForwardPropagate для генерации прогнозов и используем функцию GetOutput для получения outputValues после изменения размера с помощью ArrayResize до OUTPUT_NEURON_COUNT, записывая ошибки с помощью Print, если данные недействительны. Получим askPrice и bidPrice с SymbolInfoDouble, рассчитаем buyStopLoss, buyTakeProfit, sellStopLoss и sellTakeProfit, используя NormalizeDouble с StopLossPoints, TakeProfitPoints, _Point и _Digits, а также проверим их с помощью функции CheckStopLossTakeprofit.

Для торговли установим CONFIDENCE_THRESHOLD равным 0.8; если outputValues[0] превысит его, а outputValues[1] ниже его дополнения, используем функцию tradeObject.Buy после установки магического номера с помощью SetExpertMagicNumber, регистрируя успех или неудачу с помощью Print и GetLastError. Аналогично, используем функцию tradeObject.Sell для сигналов на продажу, обеспечивая надежное исполнение сделок. После компиляции получаем следующий результат.

ИТОГОВЫЙ РЕЗУЛЬТАТ

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


Тестирование и оптимизация настроек темпа обучения

После тщательного тестирования на истории мы получили следующие результаты.

График тестирования на истории:

График

Отчет о тестировании на истории:

ОТЧЕТ


Заключение

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

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

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

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

Прикрепленные файлы |

Другие статьи автора

Нейросети в трейдинге: Оптимизация Cross-Attention для анализа длинных последовательностей рынка (Основные компоненты) Нейросети в трейдинге: Оптимизация Cross-Attention для анализа длинных последовательностей рынка (Основные компоненты)
В статье продолжается реализация фреймворка STCA средствами MQL5. Оригинальные оптимизации Self-Attention перенесены в архитектуру FlashAttention-2 и адаптированы под финансовые данные. Особое внимание уделено аккумулированию и распределению градиентов между потоками рабочей группы для анализа длинных временных рядов и многоголового внимания.
Преодоление ограничений машинного обучения (Часть 6): Эффективная кросс-валидация исторической памяти рынка Преодоление ограничений машинного обучения (Часть 6): Эффективная кросс-валидация исторической памяти рынка
В этом обсуждении мы противопоставим классический подход к кросс-валидации временных рядов современным альтернативам, бросающим вызов его основным допущениям. Мы выявляем ключевые «слепые зоны» традиционной кросс-валидации, особенно её неспособность учитывать меняющиеся рыночные условия. Для устранения этих пробелов мы внедряем эффективную кросс-валидацию исторической памяти рынка (Effective Memory Cross-Validation, EMCV) - подход, ориентированный на предметную область, ставящий под сомнение устоявшееся мнение о том, что увеличение объема исторических данных всегда повышает показатели результатов.
Торговые инструменты на языке MQL5 (Часть 10): Создание системы отслеживания стратегии с визуальными уровнями и и метриками эффективности Торговые инструменты на языке MQL5 (Часть 10): Создание системы отслеживания стратегии с визуальными уровнями и и метриками эффективности
В данной статье мы разрабатываем систему отслеживания стратегий на языке MQL5, которая обнаруживает сигналы пересечения скользящих средних, отфильтрованные долгосрочной скользящей средней, моделирует или исполняет сделки с настраиваемыми уровнями TP и SL в пунктах, а также отслеживает результаты, такие как попадание в TP/SL, для анализа эффективности.
Знакомство с языком MQL5 (Часть 41): Руководство для начинающих по работе с файлами в MQL5 (III) Знакомство с языком MQL5 (Часть 41): Руководство для начинающих по работе с файлами в MQL5 (III)
Узнайте, как читать CSV-файл в MQL5 и упорядочивать содержащиеся в нем торговые данные в динамических массивах. В этой статье пошагово показано, как подсчитать элементы файла, сохранить все данные в едином массиве и разнести каждый столбец по отдельным массивам, заложив основу для более продвинутого анализа и визуализации торговой эффективности.