English Español Deutsch 日本語 Português
preview
Советник на базе универсального аппроксиматора MLP

Советник на базе универсального аппроксиматора MLP

MetaTrader 5Примеры |
903 11
Andrey Dik
Andrey Dik

Содержание

  1. Введение
  2. Погружение в проблематику обучения
  3. Универсальный аппроксиматор
  4. Реализация MLP в составе торгового советника


Введение

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

F(x) = f2(f1(x))

где f1 — функция первого слоя, а f2 — функция второго слоя.

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

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

Интересно, что мы можем интегрировать этот аппроксиматор в любую торговую систему. Если рассматривать нейронную сеть без упоминания оптимизаторов, таких как SGD или ADAM, MLP можно использовать как преобразователь информации. Например, он может анализировать состояния рынка — будь то флет, тренд или переходное состояние — и на основе этого применять различные торговые стратегии. Мы также можем использовать нейросеть для преобразования данных индикаторов в торговые сигналы.

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


Погружение в проблематику обучения

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

Обучение с учителем. Модель обучается на размеченных данных, строя свои прогнозы на основе примеров. Целевая функция: минимизация ошибки соответствия прогноза целевому значению (например, ошибка MSE). Однако, у этого подхода есть ряд недостатков. Он требует значительного объема качественных размеченных данных, что представляет собой основную проблему в контексте временных рядов. Если у нас есть четкие и достоверные примеры для обучения, например, как в задачах распознавания рукописного текста или содержимого изображений, то процесс обучения проходит без затруднений. Нейронная сеть учится распознавать именно то, чему её обучали.

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

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

Обучение без учителя. Модель сама ищет скрытые структуры в данных без меток. Целевая функция: разные — в зависимости от методов. Сложно оценить качество полученных результатов, так как нет четких меток для проверки. Модель может не найти полезные закономерности, если данные не имеют четкой структуры, и вообще не известно, действительно ли были найдены структуры в данных, имеющих непосредственное отношение к "несущему процессу".

Методы, которые традиционно относят к обучению без учителя: K-means, Самоорганизующиеся карты (SOM), и другие. Все эти методы обучаются, используя специфичные для них целевые функции.

Приведем соотвествующие примеры:

  • K-средних (K-means). Минимизация внутрикластерной дисперсии, которая определяется как сумма квадратов расстояний между каждой точкой и центром её кластера.
  • Метод главных компонент (PCA). Максимизация дисперсии проекций данных на новые оси (главные компоненты).
  • Деревья решений (DT). Минимизация энтропии, индекса Джини, дисперсии и другие.

Обучение с подкреплением. Целевая функция: суммарная награда. Это метод машинного обучения, где агент (например, программа или робот) учится принимать решения, взаимодействуя с окружающей средой. Агент получает вознаграждение или наказание в зависимости от своих действий. Цель агента — максимизировать общее вознаграждение, обучаясь на основе опыта.

Результаты могут быть нестабильными из-за случайного характера обучения, что затрудняет предсказание поведения модели и не всегда подходит для задач, где нет четкой системы наград и штрафов, что может сделать обучение менее эффективным. Обучение с подкреплением, как правило, связано с множеством практических проблем: трудность представления целевой функции подкрепления при использовании таких алгоритмов обучения нейронных сетей, как ADAM и подобные, так как необходимо нормировать значения целевой функции в диапазон, близкий к [-1;1]. Это связано с вычислением производных функции активации в нейронах и обратным прохождением ошибки через сеть для корректировки весов, чтобы избежать "взрыва весов" и подобных эффектов, приводящих к ступору нейронной сети.

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

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


        Универсальный аппроксиматор

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

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

        Для этой роли идеально подходит такой тип нейронных сетей, как MLP (multilayer perceptron), что подтверждается теоремой об универсальной аппроксимации. Эта теорема утверждает, что нейронные сети могут приближать любую непрерывную функцию. В нашем случае под "непрерывной функцией" мы понимаем процесс, происходящий в анализируемом временном ряде. В данном подходе нет необходимости прибегать к искусственным и субъективным понятиям, таким как "шум" и "переобучение", которые не имеют количественной оценки.

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

        Рисунок 1. Преобразование одного вида информации в другой


        Реализация MLP в составе торгового советника

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

        Объявим класс "C_MLP", который реализует многослойный персептрон (MLP). Основные функции:

        1. Init ()  — инициализация, настраивает сеть в зависимости от требуемого количества слоев и количества нейронов в каждом слое и возвращает общее количество весов.

        2. ANN ()  — прямой проход от первого входного слоя к последнему выходному, метод принимает входные данные и веса, вычисляет выходные значения сети (см. рисунок 1).

        3. GetWcount ()  — метод возвращает общее количество весов в сети.

        4. LayerCalc ()  — выполняет расчет слоя сети.

        Внутренние элементы:

        • слои — хранят значения нейронов
        • weightsCNT — общее количество весов
        • layersCNT — общее количество слоев

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

        //+----------------------------------------------------------------------------+
        //| Класс многослойного персептрона (MLP)                                      |
        //| Реализует прямой проход по полносвязной нейронной сети                     |
        //| Архитектура: Lin -> L1 -> L2 -> ... Ln -> Lout                             |
        //+----------------------------------------------------------------------------+
        class C_MLP
        {
          public: //--------------------------------------------------------------------
        
          // Инициализация сети заданной конфигурацией
          // Возвращает общее количество весов в сети или 0 в случае ошибки
          int Init (int &layerConfig []);
        
          // Последовательно вычисляет значения всех слоев от входа к выходу
          void ANN (double &inLayer  [],  // входные значения
                    double &weights  [],  // веса сети (включая смещения)
                    double &outLayer []); // значения выходного слоя
        
          // Получить общее количество весов в сети
          int GetWcount () { return weightsCNT; }
        
          int layerConf []; // Конфигурация сети - количество нейронов в каждом слое
        
          private: //-------------------------------------------------------------------
          // Структура для хранения слоя нейронной сети
          struct S_Layer
          {
              double l [];     // Значения нейронов
          };
        
          S_Layer layers [];    // Массив всех слоев сети
          int     weightsCNT;   // Общее количество весов в сети (включая смещения)
          int     layersCNT;    // Общее количество слоев (включая входной и выходной)
          int     cnt_W;        // Текущий индекс в массиве весов при проходе по сети
          double  temp;         // Временная переменная для хранения суммы взвешенных входов
        
          // Вычисление значений одного слоя сети
          void LayerCalc (double   &inLayer  [], // значения нейронов предыдущего слоя
                          double   &weights  [], // массив весов и смещений всей сети
                          double   &outLayer [], // массив для записи значений текущего слоя
                          const int inSize,      // количество нейронов во входном слое
                          const int outSize);    // outSize  - количество нейронов в выходном слое
        };

        Инициализируется многослойный персептрон (MLP) с заданной конфигурацией слоев. Основные шаги:

        1. Проверка конфигурации:

        • Проверка, что в сети минимум 2 слоя (входной и выходной).
        • Проверка, что в каждом слое есть хотя бы 1 нейрон. Если условия не выполнены, выводится сообщение об ошибке, и функция возвращает 0.

        2. Сохранение конфигурации каждого слоя для быстрого доступа в массив "layerConf".

        3. Создание массивов слоев: выделяется память для хранения нейронов в каждом слое.

        4. Подсчет весов: рассчитывается общее количество весов в сети, включая смещения для каждого нейрона.

        Функция возвращает общее количество весов или 0 в случае ошибки.

        //+----------------------------------------------------------------------------+
        //| Инициализация сети                                                         |
        //| layerConfig - массив с количеством нейронов в каждом слое                  |
        //| Возвращает общее количество необходимых весов или 0 при ошибке             |
        //+----------------------------------------------------------------------------+
        int C_MLP::Init (int &layerConfig [])
        {
          // Проверяем, что сеть имеет минимум 2 слоя (входной и выходной)
          layersCNT = ArraySize (layerConfig);
          if (layersCNT < 2)
          {
            Print ("Error Net config! Layers less than 2!");
            return 0;
          }
        
          // Проверяем, что в каждом слое есть хотя бы 1 нейрон
          for (int i = 0; i < layersCNT; i++)
          {
            if (layerConfig [i] <= 0)
            {
              Print ("Error Net config! Layer No." + string (i + 1) + " contains 0 neurons!");
              return 0;
            }
          }
        
          // Сохраняем конфигурацию сети
          ArrayCopy (layerConf, layerConfig, 0, 0, WHOLE_ARRAY);
        
          // Создаем массив слоев
          ArrayResize (layers, layersCNT);
        
          // Выделяем память под нейроны каждого слоя
          for (int i = 0; i < layersCNT; i++)
          {
            ArrayResize (layers [i].l, layerConfig [i]);
          }
        
          // Подсчитываем общее количество весов в сети
          weightsCNT = 0;
          for (int i = 0; i < layersCNT - 1; i++)
          {
            // Для каждого нейрона следующего слоя нужно:
            // - одно значение смещения (bias)
            // - веса для связей со всеми нейронами текущего слоя
            weightsCNT += layerConf [i] * layerConf [i + 1] + layerConf [i + 1];
          }
        
          return weightsCNT;
        }

        Метод "LayerCalc" выполняет вычисления для одного слоя нейронной сети, используя гиперболический тангенс в качестве функции активации. Основные шаги:

        1. Входные и выходные параметры:

        • inLayer []  — массив входных значений от предыдущего слоя
        • weights []  — массив весов содержит смещения и веса для связей
        • outLayer []  — массив для хранения выходных значений текущего слоя
        • inSize — количество нейронов во входном слое
        • outSize — количество нейронов в выходном слое

        2. Цикл по нейронам выходного слоя. Для каждого нейрона в выходном слое:

        • начинает с значения смещения (bias)
        • добавляет взвешенные входные значения (каждое входное значение умножается на соответствующий вес)
        • считается значение функции активации для нейрона

        3. Применение функции активации:

        • использует гиперболический тангенс для нелинейного преобразования значения в диапазон от -1 до 1
        • результат записывается в массив выходных значений "outLayer []"

        //+----------------------------------------------------------------------------+
        //| Вычисление значений одного слоя сети                                       |
        //| Реализует формулу: y = tanh(bias + w1*x1 + w2*x2 + ... + wn*xn)            |
        //+----------------------------------------------------------------------------+
        void C_MLP::LayerCalc (double    &inLayer  [],
                               double    &weights  [],
                               double    &outLayer [],
                               const int  inSize,
                               const int  outSize)
        {
          // Вычисляем значение для каждого нейрона в выходном слое
          for (int i = 0; i < outSize; i++)
          {
            // Начинаем со значения смещения (bias) для текущего нейрона
            temp = weights [cnt_W];
            cnt_W++;
        
            // Добавляем взвешенные входы от каждого нейрона предыдущего слоя
            for (int u = 0; u < inSize; u++)
            {
              temp += inLayer [u] * weights [cnt_W];
              cnt_W++;
            }
        
            // Применяем функцию активации "гиперболический тангенс"
            // f(x) = 2/(1 + e^(-x)) - 1
            // Диапазон значений f(x): [-1, 1]
            outLayer [i] = 2.0 / (1.0 + exp (-temp)) - 1.0;
          }
        }

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

        1. Входные и выходные параметры:

        • inLayer []  — массив входных значений, которые поступают в нейронную сеть
        • weights []  — массив весов, который включает в себя как веса для связей между нейронами, так и смещения
        • outLayer []  — массив, в который будут записаны выходные значения последнего слоя нейронной сети

        2. Сброс счетчика весов: переменная "cnt_W", которая отслеживает текущую позицию в массиве весов, сбрасывается в 0 перед началом вычислений.

        3. Копирование входных данных: входные данные из "inLayer" копируются в первый слой сети с помощью функции "ArrayCopy".

        4. Цикл по слоям:

        • цикл проходит по всем слоям нейронной сети.
        • для каждого слоя вызывается функция "LayerCalc", которая вычисляет значения для текущего слоя на основе выходных значений предыдущего слоя, весов и размеров слоев.

        5. После завершения вычислений для всех слоев, выходные значения последнего слоя копируются в массив "outLayer" с помощью функции "ArrayCopy".

        //+----------------------------------------------------------------------------+
        //| Последовательно вычисляет значения всех слоев от входа к выходу            |
        //+----------------------------------------------------------------------------+
        void C_MLP::ANN (double &inLayer  [],  // входные значения
                         double &weights  [],  // веса сети (включая смещения)
                         double &outLayer [])  // значения выходного слоя
        {
          // Сбрасываем счетчик весов перед началом прохода
          cnt_W = 0;
        
          // Копируем входные данные в первый слой сети
          ArrayCopy (layers [0].l, inLayer, 0, 0, WHOLE_ARRAY);
        
          // Последовательно вычисляем значения каждого слоя
          for (int i = 0; i < layersCNT - 1; i++)
          {
            LayerCalc (layers    [i].l,     // выход предыдущего слоя
                       weights,             // веса сети (включая смещения/bias)
                       layers    [i + 1].l, // следующий слой
                       layerConf [i],       // размер текущего слоя
                       layerConf [i + 1]);  // размер следующего слоя
          }
        
          // Копируем значения последнего слоя в выходной массив
          ArrayCopy (outLayer, layers [layersCNT - 1].l, 0, 0, WHOLE_ARRAY);
        }

        Настала очередь написать советник для автоматической торговой стратегии с использованием машинного обучения на базе нейронной сети MLP.

        1. Подключим библиотеки для торговых операций, обработки информации торгового символа, математических функций, многослойного перцептрона (MLP) и алгоритмов оптимизации.

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

        3. Объявление классов и переменных — объекты классов для утилит, нейронной сети, а также переменные для хранения входных данных, весов и времени последнего обучения.

        #include "#Symbol.mqh"
        #include <Math\AOs\Utilities.mqh>
        #include <Math\AOs\NeuroNets\MLP.mqh>
        #include <Math\AOs\PopulationAO\#C_AO_enum.mqh>
        
        //------------------------------------------------------------------------------
        input group    "---Trade parameters-------------------";
        input double   Lot_P              = 0.01;   // Объем позиции
        input int      StartTradeH_P      = 3;      // Час начала торговли
        input int      EndTradeH_P        = 12;     // Час окончания торговли
        
        input group    "---Training parameters----------------";
        input E_AO     OptimizerSelect_P  = AO_CLA; // Выбрать оптимизатор
        input int      NumbTestFuncRuns_P = 5000;   // Общее количество запусков тестовой функции
        input string   MLPstructure_P     = "1|1";  // Скрытые слои, <4|6|2> - три скрытых слоя
        input int      BarsAnalysis_P     = 3;      // Кол-во баров для анализа
        input int      DepthHistoryBars_P = 10000;  // Глубина истории для обучения в барах
        input int      RetrainingPeriod_P = 12;     // Длительность в часах актуальности модели
        input double   SigThr_P           = 0.5;    // Порог сигнала
        
        //------------------------------------------------------------------------------
        C_AO_Utilities U;
        C_MLP          NN;
        int            InpSigNumber;
        int            WeightsNumber;
        double         Inputs  [];
        double         Weights [];
        double         Outs    [1];
        datetime       LastTrainingTime = 0;
        
        C_Symbol       S;
        C_NewBar       B;
        int            HandleS;
        int            HandleR;

        В качестве информации, поступающей на обработку в нейронную сеть, я выбрал первое, что пришло в голову: OHLC — цены баров (по умолчанию в настройках 3 предыдущих бара перед текущим) и значения индикаторов RSI и Stochastic на этих барах. Функция "OnInit ()" инициализирует торговую стратегию с использованием нейронной сети. 

        1. Инициализация индикаторов — создаются объекты для RSI и Stochastic.

        2. Расчет количества входных сигналов для сети на основе входного параметра "BarsAnalysis_P".

        3. Настройка структуры нейронной сети — разбивается строка входного параметра с конфигурацией сети, проверяется корректность количества слоев и нейронов. Входной строковый параметр задает количество скрытых слоев сети и нейронов в них, по умолчанию параметр равен "1|1", что означает 2 скрытых слоя в сети по одному нейрону в каждом.

        4. Инициализация нейронной сети — вызывается метод для инициализации сети, создаются массивы для весов и входных данных.

        5. Вывод информации — печатаются данные о количестве слоев и параметров сети.

        6. Возвращается статус успешной инициализации.

        Функция обеспечивает подготовку всех необходимых компонентов для работы торговой стратегии.

        //——————————————————————————————————————————————————————————————————————————————
        int OnInit ()
        {
          //----------------------------------------------------------------------------
          // Инициализация индикаторов: Stochastic и RSI
          HandleS = iStochastic (_Symbol, PERIOD_CURRENT, 5, 3, 3, MODE_EMA, STO_LOWHIGH);
          HandleR = iRSI        (_Symbol, PERIOD_CURRENT, 14, PRICE_TYPICAL);
        
          // Расчет количества входов в нейронную сеть на основе количества баров для анализа
          InpSigNumber = BarsAnalysis_P * 2 + BarsAnalysis_P * 4;
        
          // Вывод информации о количестве входов
          Print ("Количество входов в сеть  : ", InpSigNumber);
        
          //----------------------------------------------------------------------------
          // Инициализация структуры многослойного MLP
          string sepResult [];
          int layersNumb = StringSplit (MLPstructure_P, StringGetCharacter ("|", 0), sepResult);
        
          // Проверка, что количество скрытых слоев больше 0
          if (layersNumb < 1)
          {
            Print ("Ошибка конфигурации сети, скрытых слоев < 1...");
            return INIT_FAILED; // Возвращаем ошибку инициализации
          }
        
          // Увеличиваем количество слоев на 2 (входной и выходной)
          layersNumb += 2;
        
          // Инициализация массива для конфигурации нейронной сети
          int nnConf [];
          ArrayResize (nnConf, layersNumb);
        
          // Установка количества входов и выходов в конфигурацию сети
          nnConf [0] = InpSigNumber;   // Входной слой
          nnConf [layersNumb - 1] = 1; // Выходной слой
        
          // Заполнение конфигурации скрытых слоев
          for (int i = 1; i < layersNumb - 1; i++)
          {
            nnConf [i] = (int)StringToInteger (sepResult [i - 1]); // Преобразование строкового значения в целое число
        
            // Проверка, что количество нейронов в слое больше 0
            if (nnConf [i] < 1)
            {
              Print ("Ошибка конфигурации сети, в слое ", i, " <= 0 нейронов...");
              return INIT_FAILED; // Возвращаем ошибку инициализации
            }
          }
        
          // Инициализация нейронной сети и получение количества весов
          WeightsNumber = NN.Init (nnConf);
          if (WeightsNumber <= 0)
          {
            Print ("Ошибка инициализации сети MLP...");
            return INIT_FAILED; // Возвращаем ошибку инициализации
          }
        
          // Изменение размера массива входных данных и весов
          ArrayResize (Inputs,  InpSigNumber);
          ArrayResize (Weights, WeightsNumber);
        
          // Инициализация весов случайными значениями в диапазоне [-1, 1] (для отладки)
          for (int i = 0; i < WeightsNumber; i++)
              Weights [i] = 2 * (rand () / 32767.0) - 1;
        
          // Вывод информации о конфигурации сети
          Print ("Количество всех слоев     : ", layersNumb);
          Print ("Количество параметров сети: ", WeightsNumber);
        
          //----------------------------------------------------------------------------
          // Инициализация торгового и барного классов
          S.Init (_Symbol);
          B.Init (_Symbol, PERIOD_CURRENT);
        
          return (INIT_SUCCEEDED); // Возвращаем успешный результат инициализации
        }
        //——————————————————————————————————————————————————————————————————————————————

        Основная логика торговой стратегии реализована в функции "OnTick ()". Стратегия простая: если сигнал нейрона выходного слоя превышает порог, заданный в параметрах, то сигнал интерпретируется как соответствующий направлению купить/продать, и если нет открытых позиций, и текущее время разрешено для торговли, то открываем позицию. Позиция закрывается, в случае получения от нейронной сети противоположного сигнала, или принудительно, в случае завершения разрешенного для торговли времени. Проговорим основные шаги стратегии:

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

        2. Проверка нового бара. Если текущий тик не является началом нового бара, выполнение функции прекращается.

        3. Получение данных. Код запрашивает данные о ценах (открытие, закрытие, максимум, минимум) и значения индикаторов (RSI и Stochastic).

        4. Нормализация данных. Находятся максимумы и минимумы среди полученных данных цен символа, после чего, все данные нормализуются в диапазоне от -1 до 1.

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

        6. Генерация торгового сигнала. На основе выходных данных формируется сигнал для покупки (1) или продажи (-1).

        7. Управление позициями. Если текущая позиция противоречит сигналу, она закрывается. Если сигнал на открытие новой позиции совпадает с разрешённым временем, позиция открывается. В противном случае, если есть открытая позиция, она закрывается.

        Таким образом, логика в OnTick () реализует полный цикл автоматической торговли, включая обучение, получение данных, нормализацию, прогнозирование и управление позициями.

        //——————————————————————————————————————————————————————————————————————————————
        void OnTick ()
        {
          // Проверка, нужно ли переобучить нейронную сеть
          if (TimeCurrent () - LastTrainingTime >= RetrainingPeriod_P * 3600)
          {
            // Запуск процесса обучения нейронной сети
            if (Training ()) LastTrainingTime = TimeCurrent (); // Обновление времени последнего обучения
            else             Print ("Ошибка обучения...");      // Вывод сообщения об ошибке
        
            return; // Завершение выполнения функции
          }
        
          //----------------------------------------------------------------------------
          // Проверка, является ли текущий тик началом нового бара
          if (!B.IsNewBar ()) return;
        
          //----------------------------------------------------------------------------
          // Объявление массивов для хранения данных о ценах и индикаторах
          MqlRates rates [];
          double   rsi   [];
          double   sto   [];
        
          // Получение данных о ценах
          if (CopyRates (_Symbol, PERIOD_CURRENT, 1, BarsAnalysis_P, rates) != BarsAnalysis_P) return;
        
          // Получение значений Stochastic
          if (CopyBuffer (HandleS, 0, 1, BarsAnalysis_P, sto) != BarsAnalysis_P) return;
          // Получение значений RSI
          if (CopyBuffer (HandleR, 0, 1, BarsAnalysis_P, rsi) != BarsAnalysis_P) return;
        
          // Инициализация переменных для нормализации данных
          int wCNT   = 0;
          double max = -DBL_MAX; // Начальное значение для максимума
          double min =  DBL_MAX; // Начальное значение для минимума
        
          // Поиск максимума и минимума среди high и low
          for (int b = 0; b < BarsAnalysis_P; b++)
          {
            if (rates [b].high > max) max = rates [b].high; // Обновление максимума
            if (rates [b].low  < min) min = rates [b].low;  // Обновление минимума
          }
        
          // Нормализация входных данных для нейронной сети
          for (int b = 0; b < BarsAnalysis_P; b++)
          {
            Inputs [wCNT] = U.Scale (rates [b].high,  min, max, -1, 1); wCNT++; // Нормализация high
            Inputs [wCNT] = U.Scale (rates [b].low,   min, max, -1, 1); wCNT++; // Нормализация low
            Inputs [wCNT] = U.Scale (rates [b].open,  min, max, -1, 1); wCNT++; // Нормализация open
            Inputs [wCNT] = U.Scale (rates [b].close, min, max, -1, 1); wCNT++; // Нормализация close
        
            Inputs [wCNT] = U.Scale (sto   [b],       0,   100, -1, 1); wCNT++; // Нормализация Stochastic
            Inputs [wCNT] = U.Scale (rsi   [b],       0,   100, -1, 1); wCNT++; // Нормализация RSI
          }
        
          // Преобразование данных из Inputs в Outs
          NN.ANN (Inputs, Weights, Outs);
        
          //----------------------------------------------------------------------------
          // Генерация торгового сигнала на основе выходных данных нейронной сети
          int signal = 0;
          if (Outs [0] >  SigThr_P) signal =  1; // Сигнал на покупку
          if (Outs [0] < -SigThr_P) signal = -1; // Сигнал на продажу
        
          // Получение типа открытой позиции
          int posType = S.GetPosType ();
          S.GetTick ();
        
          if ((posType == 1 && signal == -1) || (posType == -1 && signal == 1))
          {
            if (!S.PosClose ("", ORDER_FILLING_FOK) != 0) posType = 0;
            else return;
          }
        
          MqlDateTime time;
          TimeToStruct (TimeCurrent (), time);
        
          // Проверка разрешенного времени для торговли
          if (time.hour >= StartTradeH_P && time.hour < EndTradeH_P)
          {
            // Открытие новой позиции в зависимости от сигнала
            if (posType == 0 && signal != 0) S.PosOpen (signal, Lot_P, "", ORDER_FILLING_FOK, 0, 0.0, 0.0, 1);
          }
          else
          {
            if (posType != 0) S.PosClose ("", ORDER_FILLING_FOK);
          }
        }
        //——————————————————————————————————————————————————————————————————————————————

        Далее рассмотрим обучение нейронной сети на исторических данных:

        1. Получение данных. Загружаются исторические данные о ценах, а также значения индикаторов RSI и Stochastic.

        2. Определение торгового времени. Создается массив, который отмечает, какие бары попадают в разрешенное время для торговли.

        3. Настройка параметров оптимизации. Инициализируются границы и шаги параметров для оптимизации.

        4. Выбор алгоритма оптимизации. Определяется алгоритм оптимизации и задается размер популяции.

        5. Основной цикл оптимизации весов нейронной сети: 

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

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

        7. Освобождается память, занятая объектом алгоритма оптимизации.

        Функция осуществляет процесс обучения нейронной сети для нахождения лучших параметров на исторических данных.

        //——————————————————————————————————————————————————————————————————————————————
        bool Training ()
        {
          MqlRates rates [];
          double   rsi   [];
          double   sto   [];
        
          int bars = CopyRates (_Symbol, PERIOD_CURRENT, 1, DepthHistoryBars_P, rates);
          Print ("Обучение на истории ", bars, " баров");
          if (CopyBuffer (HandleS, 0, 1, DepthHistoryBars_P, sto) != bars) return false;
          if (CopyBuffer (HandleR, 0, 1, DepthHistoryBars_P, rsi) != bars) return false;
        
          MqlDateTime time;
          bool truTradeTime []; ArrayResize (truTradeTime, bars); ArrayInitialize (truTradeTime, false);
          for (int i = 0; i < bars; i++)
          {
            TimeToStruct (rates [i].time, time);
            if (time.hour >= StartTradeH_P && time.hour < EndTradeH_P) truTradeTime [i] = true;
          }
        
          //----------------------------------------------------------------------------
          int popSize          = 50;                           // Размер популяции для алгоритма оптимизации
          int epochCount       = NumbTestFuncRuns_P / popSize; // Общее количество эпох (итераций) для оптимизации
        
          double rangeMin [], rangeMax [], rangeStep [];       // Массивы для хранения границ и шагов параметров
        
          ArrayResize (rangeMin,  WeightsNumber);              // Изменение размера массива min границ
          ArrayResize (rangeMax,  WeightsNumber);              // Изменение размера массива max границ
          ArrayResize (rangeStep, WeightsNumber);              // Изменение размера массива шагов
        
          for (int i = 0; i < WeightsNumber; i++)
          {
            rangeMax  [i] =  5.0;
            rangeMin  [i] = -5.0;
            rangeStep [i] = 0.01;
          }
        
          //----------------------------------------------------------------------------
          C_AO *ao = SelectAO (OptimizerSelect_P);             // Выбор алгоритма оптимизации
        
          ao.params [0].val = popSize;                         // Назначение размера популяции....
          ao.SetParams ();                                     //... (необязательно, тогда будет использован размер популяции по умолчанию)
        
          ao.Init (rangeMin, rangeMax, rangeStep, epochCount); // Инициализация алгоритма с заданными границами и количеством эпох
        
          // Основной цикл по количеству эпох
          for (int epochCNT = 1; epochCNT <= epochCount; epochCNT++)
          {
            ao.Moving ();                                      // Выполнение одной эпохи алгоритма оптимизации
        
            // Вычисление значения целевой функции для каждого решения в популяции
            for (int set = 0; set < ArraySize (ao.a); set++)
            {
              ao.a [set].f = TargetFunction (ao.a [set].c, rates, rsi, sto, truTradeTime); //FF.CalcFunc (ao.a [set].c); //ObjectiveFunction (ao.a [set].c); // Применение целевой функции к каждому решению
            }
        
            ao.Revision ();                                    // Обновление популяции на основе результатов целевой функции
          }
        
          //----------------------------------------------------------------------------
          // Вывод имени алгоритма, лучшего результата и количества запусков функции
          Print (ao.GetName (), ", лучший результат: ", ao.fB);
          ArrayCopy (Weights, ao.cB);
          delete ao;                                           // Освобождение памяти, занятой объектом алгоритма
        
          return true;
        }
        //——————————————————————————————————————————————————————————————————————————————

        Реализуем целевую функцию для оценки эффективности торговой стратегии, использующей нейронную сеть.

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

        2. Обработка исторических данных. Цикл проходит по историческим данным и проверяет, разрешено ли открытие позиций на текущем баре.

        3. Нормализация данных. Для каждого бара нормализуются значения цен (high, low, open, close) и индикаторов (RSI и Stochastic) для последующей передачи в нейронную сеть.

        4. Прогнозирование сигналов. Нормализованные данные подаются в нейронную сеть, которая генерирует торговые сигналы (покупка или продажа).

        5. Управление виртуальными позициями осуществляется согласно торговой стратегии в OnTick ().

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

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

        //——————————————————————————————————————————————————————————————————————————————
        double TargetFunction (double &weights [], MqlRates &rates [], double &rsi [], double &sto [], bool &truTradeTime [])
        {
          int bars = ArraySize (rates);
        
          // Инициализация переменных для нормализации данных
          int    wCNT       = 0;
          double max        = 0.0;
          double min        = 0.0;
          int    signal     = 0;
          double profit     = 0.0;
          double allProfit  = 0.0;
          double allLoss    = 0.0;
          int    dealsNumb  = 0;
          int    sells      = 0;
          int    buys       = 0;
          int    posType    = 0;
          double posOpPrice = 0.0;
          double posClPrice = 0.0;
        
          // Прогон по истории
          for (int h = BarsAnalysis_P; h < bars - 1; h++)
          {
            if (!truTradeTime [h])
            {
              if (posType != 0)
              {
                posClPrice = rates [h].open;
                profit = (posClPrice - posOpPrice) * signal - 0.00003;
        
                if (profit > 0.0) allProfit += profit;
                else              allLoss   += -profit;
        
                if (posType == 1) buys++;
                else              sells++;
        
                allProfit += profit;
                posType = 0;
              }
        
              continue;
            }
        
            max  = -DBL_MAX; // Начальное значение для максимума
            min  =  DBL_MAX; // Начальное значение для минимума
        
            // Поиск максимума и минимума среди high и low
            for (int b = 1; b <= BarsAnalysis_P; b++)
            {
              if (rates [h - b].high > max) max = rates [h - b].high; // Обновление максимума
              if (rates [h - b].low  < min) min = rates [h - b].low;  // Обновление минимума
            }
        
            // Нормализация входных данных для нейронной сети
            wCNT = 0;
            for (int b = BarsAnalysis_P; b >= 1; b--)
            {
              Inputs [wCNT] = U.Scale (rates [h - b].high,  min, max, -1, 1); wCNT++; // Нормализация high
              Inputs [wCNT] = U.Scale (rates [h - b].low,   min, max, -1, 1); wCNT++; // Нормализация low
              Inputs [wCNT] = U.Scale (rates [h - b].open,  min, max, -1, 1); wCNT++; // Нормализация open
              Inputs [wCNT] = U.Scale (rates [h - b].close, min, max, -1, 1); wCNT++; // Нормализация close
        
              Inputs [wCNT] = U.Scale (sto   [h - b],       0,   100, -1, 1); wCNT++; // Нормализация Stochastic
              Inputs [wCNT] = U.Scale (rsi   [h - b],       0,   100, -1, 1); wCNT++; // Нормализация RSI
            }
        
            // Преобразование данных из Inputs в Outs
            NN.ANN (Inputs, weights, Outs);
        
            //----------------------------------------------------------------------------
            // Генерация торгового сигнала на основе выходных данных нейронной сети
            signal = 0;
            if (Outs [0] >  SigThr_P) signal =  1; // Сигнал на покупку
            if (Outs [0] < -SigThr_P) signal = -1; // Сигнал на продажу
        
            if ((posType == 1 && signal == -1) || (posType == -1 && signal == 1))
            {
              posClPrice = rates [h].open;
              profit = (posClPrice - posOpPrice) * signal - 0.00003;
        
              if (profit > 0.0) allProfit += profit;
              else              allLoss   += -profit;
        
              if (posType == 1) buys++;
              else              sells++;
        
              allProfit += profit;
              posType = 0;
            }
        
            if (posType == 0 && signal != 0)
            {
              posType = signal;
              posOpPrice = rates [h].open;
            }
          }
        
          dealsNumb = buys + sells;
        
          double ko = 1.0;
          if (sells == 0 || buys == 0) return -DBL_MAX;
          if (sells / buys > 1.5 || buys / sells > 1.5) ko = 0.001;
        
          return (allProfit / (allLoss + DBL_EPSILON)) * dealsNumb;
        }
        //——————————————————————————————————————————————————————————————————————————————

        На рисунке 2 представлен график баланса торговых результатов, полученных с использованием советника на основе MLP на новых, незнакомых для нейронной сети данных. На вход подаются нормированные значения цен OHLC, а также индикаторы RSI и Stochastic, рассчитанные на основе указанного количества баров. Советник осуществляет торговлю, пока нейронная сеть остается актуальной; в противном случае, он проводит обучение сети и затем продолжает торговлю. Таким образом, результаты, показанные на рисунке 2, отражают эффективность работы на OOS (out of sample).

        Рисунок 2. Результат работы советника на незнакомых для MLP данных


        Выводы

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


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

        Программы, используемые в статье

        # Имя Тип Описание
        1 #C_AO.mqh
        Включаемый файл
        Родительский класс популяционных алгоритмов оптимизации
        2 #C_AO_enum.mqh
        Включаемый файл
        Перечисление популяционных алгоритмов оптимизации
        3
        Utilities.mqh
        Включаемый файл
        Библиотека вспомогательных функций
        4
        #Symbol.mqh
        Включаемый файл Библиотека торговых и вспомогательных функций
        5
        ANN EA.mq5
        Советник
        Советник на базе нейронной сети MLP
        Прикрепленные файлы |
        ANN_EA.ZIP (140.73 KB)
        Последние комментарии | Перейти к обсуждению на форуме трейдеров (11)
        Andrey Dik
        Andrey Dik | 3 авг. 2025 в 13:49
        CapeCoddah #:

        Здравствуйте, Cape.

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

        Обучал на EURUSD M15, если мне не изменяет память.

        Andrey Dik
        Andrey Dik | 3 авг. 2025 в 13:51
        SYAHRIRICH01 #:
        Рядом с противоположным сигналом не работает

        Попробуйте на счете типа неттинг. Статья даёт только идею, вы должны адаптировать советник под торговые условия вашего брокера.
        CapeCoddah
        CapeCoddah | 4 авг. 2025 в 08:32

        Привет, Андрей,

        Понял, спасибо за быстрый ответ.

        CapeCoddah

        Eric Ruvalcaba
        Eric Ruvalcaba | 5 авг. 2025 в 20:49
        Andrey Dik #:
        Попробуйте его на счете типа неттинг. Статья дает только представление, вы должны адаптировать советник к торговым условиям вашего брокера.

        Большое спасибо за статью и понимание. Отличная идея. Я реализовал некоторые независимые операции с позициями и заставил их работать на хеджирующем счете (мой брокер).



        Вы лучшие.

        Andrey Dik
        Andrey Dik | 6 авг. 2025 в 19:47
        Eric Ruvalcaba #:

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

        Вы лучшие.

        Супер!

        Автоматическая оптимизация параметров для торговых стратегий с Python и MQL5 Автоматическая оптимизация параметров для торговых стратегий с Python и MQL5
        Существует несколько типов алгоритмов самостоятельной оптимизации торговых стратегий и параметров. Эти алгоритмы используются для автоматического улучшения торговых стратегий на основе исторических и текущих рыночных данных. В этой статье мы рассмотрим один из них на примерах реализаций на Python и MQL5.
        Добавляем пользовательскую LLM в торгового робота (Часть 4): Обучение собственной LLM с помощью GPU Добавляем пользовательскую LLM в торгового робота (Часть 4): Обучение собственной LLM с помощью GPU
        Языковые модели (LLM) являются важной частью быстро развивающегося искусственного интеллекта, поэтому нам следует подумать о том, как интегрировать мощные LLM в нашу алгоритмическую торговлю. Большинству людей сложно настроить эти модели в соответствии со своими потребностями, развернуть их локально, а затем применить к алгоритмической торговле. В этой серии статей будет рассмотрен пошаговый подход к достижению этой цели.
        Возможности Мастера MQL5, которые вам нужно знать (Часть 24): Скользящие средние Возможности Мастера MQL5, которые вам нужно знать (Часть 24): Скользящие средние
        Скользящие средние — очень распространенный индикатор, который используют и понимают большинство трейдеров. Мы рассмотрим возможные варианты их использования, которые относительно редко используются в советниках, собранных с помощью Мастера MQL5.
        Построение модели для ограничения диапазона сигналов по тренду (Часть 5): Система уведомлений (Часть II) Построение модели для ограничения диапазона сигналов по тренду (Часть 5): Система уведомлений (Часть II)
        В статье подробно рассматривается интеграция уведомлений индикаторов MetaTrader 5 в Telegram с использованием возможностей MQL5, Python и API Telegram Bot. Вы сможете применить полученную информацию в своих проектах.