English 中文 Español Deutsch 日本語 Português
preview
Нейросети — это просто (Часть 47): Непрерывное пространство действий

Нейросети — это просто (Часть 47): Непрерывное пространство действий

MetaTrader 5Торговые системы | 26 июня 2023, 12:24
1 005 3
Dmitriy Gizlyk
Dmitriy Gizlyk

Введение

В наших предыдущих работах мы обучали агента лишь определению направления торговли. Спектр действий Агента ограничивался лишь 4 вариантами:

  • купить, 
  • продать, 
  • удерживать/ожидать,
  • закрыть все позиции.

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

Кроме того, для создания стабильной торговой стратегии нам необходимо управлять рисками. Этот блок также отсутствует в наших разработках. Да, советник на каждой новой торговой свече оценивал рыночную ситуацию и принимал решение о торговой операции. Но каждый предстоящий бар несет риски для нашего счета. Движение цены внутри бара может быть губительным для нашего баланса. Именно поэтому всегда рекомендуется использовать стоп-лоссы. Такой простой подход позволяет нам ограничивать риски на одну сделку.


1. Особенности обучения непрерывного пространства действий

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

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

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

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

К примеру, если мы возьмем только 3 дискретных значения для объема сделки, 3 уровня стоп-лосса и 5 уровней тейк-профита, то нам потребуется 90 элементов только для определения пространства действий в 2 направлениях торговли (3 * 3 * 5 * 2 = 90). Добавьте сюда действия удержания и закрытия позиции. И уже 92 варианта в спектре возможных действий агента.

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

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

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

Одним из наиболее популярных алгоритмов для обучения агента в непрерывном пространстве действий является Deep Deterministic Policy Gradient (DDPG). В DDPG модель состоит из двух нейронных сетей: Актера и Критика. Актер прогнозирует оптимальное действие на основе текущего состояния, а Критик оценивает данное действие. Мы уже знакомились с подобным решением в статье "Алгоритм актер-критик с преимуществом". В указанных алгоритмах есть сходство подходов, но различие в алгоритме обучения Актера.

В DDPG Актер обучается по методу градиентного подъема для оптимизации детерминированной политики. Актер прямо прогнозирует оптимальное действие на основе текущего состояния, вместо моделирования вероятностного распределения действий, как в алгоритме актер-критик с преимуществом.

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

Важно отметить, что DDPG относится к off-policy алгоритмам. Модель обучается на данных, полученных из предыдущих взаимодействий с окружающей средой, независимо от текущей стратегии принятия решений. Это важное свойство алгоритма позволяет его использовать в сложной и стохастической окружающей среде, где прогнозирование динамики среды может быть сложными или неточными. С низким качеством прогнозирования финансовых рынков мы столкнулись при тестировании алгоритма EDL.

Алгоритм Deep Deterministic Policy Gradient основан на базовых принципах Deep Q-Network (DQN) и включает в себя многие из его подходов. В том числе буфер воспроизведения опыта и целевую модель. Рассмотрим алгоритм подробнее.

Как уже было сказано выше, модель состоит из 2 нейронных сетей: Актер и Критик. Актер получает на вход состояние окружающей среды. На выходе Актера мы получаем действие из непрерывного распределения значений. В нашем случае мы будем формировать объем сделки, уровни стоп-лосса и тейк-профита. В зависимости от архитектуры модели и постановки задачи мы можем использовать абсолютные или относительные значения. Для повышения уровня исследования окружающей среды к сформированному действию может добавляться некий шум.

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

Наборы данных "Состояние - Действие - Новое состояние - Вознаграждение" собираем в буфер воспроизведения опыта. Все по классике алгоритмов обучения с подкреплением.

Как и в DQN, из буфера воспроизведения опыта выбираем пакет для обучения модели. Состояния из данного пакета обучающих данных подаются на вход Актера. До изменения параметров мы с большой долей вероятности получим действие, аналогичное сохраненному в буфере воспроизведения опыта. Но в отличии от Актер-Критик с преимуществом, Актер возвращает не вероятностное распределение, а действие из непрерывного распределения.

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

Аналогично DQN, Критик обучается на минимизацию среднеквадратичного отклонения между прогнозным вознаграждением и фактическим из буфера воспроизведения опыта. Для выстраивания целостной политики используется целевая модель Target Net. Но так как для оценки последующего состояния Критику необходим набор данных из состояния и действия, то для формирования действия из последующего состояния мы будем использовать ещё и целевую модель Актера.

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

Таким образом, в процессе обучения Q-функции Критика мы используем градиент ошибки по действию для оптимизации действий Агента. Можно сказать, что Актер выступает составной частью Q-функции. И обучение Q-функции ведет к оптимизации функции Актера.

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

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


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

После теоретического ознакомления с методом Deep Deterministic Policy Gradient (DDPG) переходим к его практической реализации средствами MQL5. И начнем мы с организации процесса мягкого обновления целевых моделей. Сама функция взвешенного суммирования 2 параметров не сложная, но есть 2 момента.

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

Во-вторых, все операции по обучению и эксплуатации моделей выполняются в контексте OpenCL. А операции копирования данных между памятью контекста и основной памятью довольно дорогие. И мы всегда стремились их минимизировать. Логично, пересчет параметров также проводить в контексте OpenCL.

2.1. Мягкое обновление целевых моделей

Вначале мы создадим кернел SoftUpdate для выполнения операций. Алгоритм кернела довольно прост. В параметрах кернела мы буем передавать указатели на 2 буфера данных (параметры целевой и обучаемой моделей) и коэффициент обновления в виде константы.

__kernel void SoftUpdate(__global float *target, 
                         __global const float *source, 
                         const float tau
                        )
  {
   const int i = get_global_id(0);
   target[i] = target[i] * tau + (1.0f - tau) * source[i];
  }

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

Далее нам предстоит организовать процесс на стороне основной программы.

Напомню, что параметры моделей у нас распределены по разным объектам в зависимости от типа нейронного слоя. А значит нам необходимо добавить метод обновления параметров в каждый класс организации работы нейронного слоя. Рассмотрим на примере базового класса нейронного слоя CNeuronBaseOCL.

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

bool CNeuronBaseOCL::WeightsUpdate(CNeuronBaseOCL *source, float tau)
  {
   if(!OpenCL || !Weights || !source || !source.Weights)
      return false;

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

Тут же мы проверяем соответствие типов двух нейронных слоев и размерностей матриц параметров.

   if(Type() != source.Type())
      return false;
   if(Weights.Total() != source.Weights.Total())
      return false;

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

   uint global_work_offset[1] = {0};
   uint global_work_size[1] = {Weights.Total()};
   ResetLastError();
   if(!OpenCL.SetArgumentBuffer(def_k_SoftUpdate, def_k_su_target, Weights.GetIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_SoftUpdate, def_k_su_source, source.getWeightsIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgument(def_k_SoftUpdate, def_k_su_tau, (float)tau))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }

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

   if(!OpenCL.Execute(def_k_SoftUpdate, 1, global_work_offset, global_work_size))
     {
      printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
      return false;
     }
//---
   return true;
  }

И завершаем работу метода.

Так как все объекты организации работы нейронных слоев различных архитектур в нашем классе наследуются от базового класса CNeuronBaseOCL, то и все классы унаследуют созданный метод. Но он позволяет обновить только матрицу весов базового класса. Во всех классах с добавлением дополнительных внутренних оптимизируемых объектов необходимо переопределить метод. К примеру, в сверточном слое CNeuronConvOCL мы добавляли матрицу параметров свертки. Для её обновления мы переопределим метод WeightsUpdate. С целью поддержки переопределения наследуемых методов мы сохраняем все параметры метода в неизменном состоянии.

bool CNeuronConvOCL::WeightsUpdate(CNeuronBaseOCL *source, float tau)
  {
   if(!CNeuronBaseOCL::WeightsUpdate(source, tau))
      return false;

Но в теле метода мы не повторяем весь блок контролей. Вместо этого мы вызываем метод родительского класса и проверяем результат выполнения операций.

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

Но вопрос в том, что мы в таком состоянии не можем обратиться к матрице весов свертки полученного в параметрах слоя. Такого объекта просто нет в родительском классе. Он появляется только в классе сверточного слоя. А в том, что в параметрах передан указатель именно на сверточный слой у нас нет сомнений. Ведь в методе родительского класса мы проверили соответствие типов текущего нейронного слоя и полученного в параметрах. Чтобы работать с данным объектом сверточного слоя нам достаточно присвоить полученный указатель динамическому объекту сверточного слоя. И тут же мы проверяем соответствие размеров матриц.

   CNeuronConvOCL *temp = source;
   if(WeightsConv.Total() != temp.WeightsConv.Total())
      return false;

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

   uint global_work_offset[1] = {0};
   uint global_work_size[1] = {WeightsConv.Total()};
   ResetLastError();
   if(!OpenCL.SetArgumentBuffer(def_k_SoftUpdate, def_k_su_target, WeightsConv.GetIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_SoftUpdate, def_k_su_source, temp.WeightsConv.GetIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgument(def_k_SoftUpdate, def_k_su_tau, (float)tau))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.Execute(def_k_SoftUpdate, 1, global_work_offset, global_work_size))
     {
      printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
      return false;
     }
//---
   return true;
  }

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

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

2.2. Обмен данными между Актером и Критиком

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

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

И первым мы рассмотрим организацию метода прямого прохода CNet::feedForward. В параметрах метода предусмотрена передача 2 указателей на нейронные сети (основные и дополнительные исходные данные) и 2 идентификаторв нейронных слоев в данных сетях.

bool CNet::feedForward(CNet *inputNet, int inputLayer=-1, CNet *secondNet = NULL, int secondLayer = -1)
  {
   if(!inputNet || !opencl)
      return false;

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

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

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

   if(inputLayer<0)
      inputLayer=inputNet.layers.Total()-1;

На следующем этапе мы организовываем работу по доступу к дополнительным данным. Мы создаем пустой указатель на объект буфера данных. И проверяем актуальность указателя на модель дополнительных исходных данных.

   CBufferFloat *second = NULL;
   bool del_second = false;
   if(!!secondNet)
     {
      if(secondLayer < 0)
         secondLayer = secondNet.layers.Total() - 1;
      if(secondNet.GetOpenCL() != opencl)
        {
         secondNet.GetLayerOutput(secondLayer, second);
         if(!!second)
           {
            if(!second.BufferCreate(opencl))
              {
               delete second;
               return false;
              }
            del_second = true;
           }
        }
      else
        {
         if(secondNet.layers.Total() <= secondLayer)
            return false;
         CLayer *layer = secondNet.layers.At(secondLayer);
         CNeuronBaseOCL *neuron = layer.At(0);
         second = neuron.getOutput();
        }
     }

При наличии действительного указателя на модель дополнительных исходных данных у нас есть 2 варианта развития событий:

  1. Если модель дополнительных исходных данных и текущая модель загружены в разные контексты OpenCL. Тогда нам в любом случае предстоит перезагружать данные. Мы копируем данные из соответствующего слоя модели данные в новый буфер и создаем буфер в необходимом контексте.
  2. Обе модели находятся в одном контексте OpenCL. Данные уже существуют в памяти контекста. Нам достаточно лишь скопировать указатель на буфер результатов нужного нейронного слоя.

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

   if(inputNet.opencl != opencl)
     {
      CBufferFloat *inputs;
      if(!inputNet.GetLayerOutput(inputLayer, inputs))
        {
         if(del_second)
            delete second;
         return false;
        }
      bool result = feedForward(inputs, 1, false, second);
      if(del_second)
         delete second;
      return result;
     }

Если же обе модели находятся в одном контексте OpenCL, то мы осуществляем подмену слоя исходных данных на указанный нейронный слоя из модели исходных данных.

   CLayer *layer = inputNet.layers.At(inputLayer);
   if(!layer)
     {
      if(del_second)
         delete second;
      return false;
     }
   CNeuronBaseOCL *neuron = layer.At(0);
   layer = layers.At(0);
   if(!layer)
     {
      if(del_second)
         delete second;
      return false;
     }
   if(layer.At(0) != neuron)
      if(!layer.Update(0, neuron))
        {
         if(del_second)
            delete second;
         return false;
        }

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

   for(int l = 1; l < layers.Total(); l++)
     {
      layer = layers.At(l);
      neuron = layer.At(0);
      layer = layers.At(l - 1);
      if(!neuron.FeedForward(layer.At(0), second))
        {
         if(del_second)
            delete second;
         return false;
        }
     }
//---
   if(del_second)
      delete second;
   return true;
  }

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

Аналогичным образом создадим метод обратного прохода CNet::backProp. С его кодом можно ознакомиться во вложении.

Оба эти метода мы будем использовать при обучении Критика. А вот для обучения Актера нам потребуется ещё один метод обратного прохода. Дело в том, что в методе обратного прохода до проведения градиента ошибки через нейронные слои мы сначала определяли отклонение результатов прямого прохода от целевых значений. Метод DDPG исключает данные процесс для Актера. И для практической реализации данного алгоритма был создан метод CNet::backPropGradient.

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

bool CNet::backPropGradient(CBufferFloat *SecondInput = NULL, CBufferFloat *SecondGradient = NULL)
  {
   if(
! layers || 
! opencl)
      return false;
   CLayer *currentLayer = layers.At(layers.Total() - 1);
   CNeuronBaseOCL *neuron = NULL;
   if(CheckPointer(currentLayer) == POINTER_INVALID)
      return false;

В теле метода мы сначала осуществляем проверку актуальности указателей на объекты динамического массива нейронных слоев и контекста OpenCL. И объявим необходимые локальные переменные.

Затем мы организуем цикл распределения градиента ошибки по всем нейронным слоям модели.

//--- Calc Hidden Gradients
   int total = layers.Total();
   for(int layerNum = total - 2; layerNum >= 0; layerNum--)
     {
      CLayer *nextLayer = currentLayer;
      currentLayer = layers.At(layerNum);
      if(CheckPointer(currentLayer) == POINTER_INVALID)
         return false;
      neuron = currentLayer.At(0);
      if(!neuron || !neuron.calcHiddenGradients(nextLayer.At(0), SecondInput, SecondGradient))
         return false;
     }

Обратите внимание, при организации процесса мы исходим из того, что градиент ошибки уже находится в буфере последнего нейронного слоя. Это предусмотрено алгоритмом DDPG (градиент ошибки Критика по действия Агента). Никакого контроля наличия градиента ошибки не предусмотрено. И применение метода находится в ответственности пользователя.

После распределения градиента ошибки проведем обновления матриц весовых коэффициентов.

   CLayer *prevLayer = layers.At(total - 1);
   for(int layerNum = total - 1; layerNum > 0; layerNum--)
     {
      currentLayer = prevLayer;
      prevLayer = layers.At(layerNum - 1);
      neuron = currentLayer.At(0);
      if(!neuron.UpdateInputWeights(prevLayer.At(0), SecondInput))
         return false;
     }

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

   bool result=false;
   for(int layerNum = 0; layerNum < total; layerNum++)
     {
      currentLayer = layers.At(layerNum);
      CNeuronBaseOCL *temp = currentLayer.At(0);
      if(!temp)
        continue; 
      if(!temp.TrainMode() || !temp.getWeights())
         continue;
      if(!temp.getWeights().BufferRead())
         continue;
      result=true;
      break;
     }
//---
   return result;
  }

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

2.3. Создание советника обучения модели

Далее мы перейдем к созданию и обучению модели с помощью алгоритма DDPG. Организация процесса обучения реализована в советнике "DDPG\Study.mq5".

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

В параметрах функция получает указатели на 2 динамических массива для записи объектов описания архитектуры нейронных слоев Актера и Критика. В теле функции мы проверяем актуальность полученных указателей и, при необходимости, создаем новые объекты массивов.

bool CreateDescriptions(CArrayObj *actor, CArrayObj *critic)
  {
//---
   CLayerDescription *descr;
//---
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         return false;
     }
   if(!critic)
     {
      critic = new CArrayObj();
      if(!critic)
         return false;
     }

Начинаем мы с описания архитектуры Актера. Здесь мы используем наработки GCRL и построим модель с 2 потоками исходных данных. В основе принятия решения Актера будет лежать текущее состояние окружающей среды (исторические данные). Для них мы создадим слой исходных данных соответствующего размера.

//--- Actor
   actor.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = (HistoryBars * BarDescr);
   descr.window = 0;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = 1000;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

"Сырые" данные обрабатываются слоем пакетной нормализации и проходят через блок сверточных слоёв.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = prev_count - 1;
   descr.window = 2;
   descr.step = 1;
   descr.window_out = 8;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = prev_count;
   descr.window = 8;
   descr.step = 8;
   descr.window_out = 8;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Далее мы сжимаем данные в 2 полносвязных слоях. Все это может напомнить используемый ранее Энкодер.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 256;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 128;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

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

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = 256;
   descr.window = prev_count;
   descr.step = AccountDescr;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

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

Далее идет блок принятия решения из полносвязных слоев.

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 256;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 256;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

На выходе актера у нас будет полносвязный слой из 6 элементов, которые представляют объем сделки, её стоп-лосс и тейк-профит (3 элемента для покупки и 3 для продажи).

//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 6;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

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

Модель Критика для прогнозирования вознаграждения использует текущее состояния окружающей среды и действие Актера. В нашем случае оба потока информации идут от модели Актера, но разные нейронные слои. И, соответственно, разные буферы данных. Для объединения двух потоков данных мы воспользуемся нейронным слоем конкатенации данных. В архитектуре модели Критика это отразится следующим образом. Первый поток информации (латентное представление текущего состояния) мы передадим на слой исходных данных. Размер данного слоя должен соответствовать размеру нейронного слоя Актера, с которого планируем брать данные.

//--- Critic
   critic.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = 256;
   descr.window = 0;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!critic.Add(descr))
     {
      delete descr;
      return false;
     }

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

Далее мы используем слой конкатенации для объединения 2 потоков информации. Размер дополнительных данных равен размеру слоя результатов Актера.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = 128;
   descr.window = prev_count;
   descr.step = 6;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!critic.Add(descr))
     {
      delete descr;
      return false;
     }

Затем идет блок принятия решения из 2 полносвязных слоев.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 128;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!critic.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 128;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!critic.Add(descr))
     {
      delete descr;
      return false;
     }

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

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 1;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!critic.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

И чтобы в дальнейшем не путаться c идентификатором слоя латентного представления состояния окружающей среды мы определим константу в виде макроподстановки.

#define                    LatentLayer  6

Теперь, когда мы определились с архитектурой моделей мы переходим к работе над алгоритмом советника. Первым мы создадим метод инициализации советника OnInit. Вначале метода, мы как и ранее, инициализируем объекты индикаторов и торговых операций.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   if(!Symb.Name(_Symbol))
      return INIT_FAILED;
   Symb.Refresh();
//---
   if(!RSI.Create(Symb.Name(), TimeFrame, RSIPeriod, RSIPrice))
      return INIT_FAILED;
//---
   if(!CCI.Create(Symb.Name(), TimeFrame, CCIPeriod, CCIPrice))
      return INIT_FAILED;
//---
   if(!ATR.Create(Symb.Name(), TimeFrame, ATRPeriod))
      return INIT_FAILED;
//---
   if(!MACD.Create(Symb.Name(), TimeFrame, FastPeriod, SlowPeriod, SignalPeriod, MACDPrice))
      return INIT_FAILED;
   if(!RSI.BufferResize(HistoryBars) || !CCI.BufferResize(HistoryBars) ||
      !ATR.BufferResize(HistoryBars) || !MACD.BufferResize(HistoryBars))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      return INIT_FAILED;
     }
//---
   if(!Trade.SetTypeFillingBySymbol(Symb.Name()))
      return INIT_FAILED;

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

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

//--- load models
   float temp;
   if(!Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) ||
      !Critic.Load(FileName + "Crt.nnw", temp, temp, temp, dtStudied, true) ||
      !TargetActor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) ||
      !TargetCritic.Load(FileName + "Crt.nnw", temp, temp, temp, dtStudied, true))
     {
      CArrayObj *actor = new CArrayObj();
      CArrayObj *critic = new CArrayObj();
      if(!CreateDescriptions(actor, critic))
        {
         delete actor;
         delete critic;
         return INIT_FAILED;
        }
      if(!Actor.Create(actor) || !Critic.Create(critic) ||
         !TargetActor.Create(actor) || !TargetCritic.Create(critic))
        {
         delete actor;
         delete critic;
         return INIT_FAILED;
        }
      delete actor;
      delete critic;
      //---
     }

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

   COpenCLMy *opencl = Actor.GetOpenCL();
   Critic.SetOpenCL(opencl);
   TargetActor.SetOpenCL(opencl);
   TargetCritic.SetOpenCL(opencl);

Затем следует блок контроля соответствия архитектур моделей.

   Actor.getResults(Result);
   if(Result.Total() != 6)
     {
      PrintFormat("The scope of the actor does not match the actions count (%d <> %d)", 6, Result.Total());
      return INIT_FAILED;
     }
   ActorResult = vector<float>::Zeros(6);
//---
   Actor.GetLayerOutput(0, Result);
   if(Result.Total() != (HistoryBars * BarDescr))
     {
      PrintFormat("Input size of Actor doesn't match state description (%d <> %d)", Result.Total(), (HistoryBars * BarDescr));
      return INIT_FAILED;
     }
//---
   Actor.GetLayerOutput(LatentLayer, Result);
   int latent_state = Result.Total();
   Critic.GetLayerOutput(0, Result);
   if(Result.Total() != latent_state)
     {
      PrintFormat("Input size of Critic doesn't match latent state Actor (%d <> %d)", Result.Total(), latent_state);
      return INIT_FAILED;
     }

Инициализируем глобальные переменные и завершаем работу метода.

   PrevBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   PrevEquity = AccountInfoDouble(ACCOUNT_EQUITY);
   FirstBar = true;
   Gradient.BufferInit(AccountDescr, 0);
   Gradient.BufferCreate(opencl);
//---
   return(INIT_SUCCEEDED);
  }

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

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   TargetActor.WeightsUpdate(GetPointer(Actor), Tau);
   TargetCritic.WeightsUpdate(GetPointer(Critic), Tau);
   TargetActor.Save(FileName + "Act.nnw", Actor.getRecentAverageError(), 0, 0, TimeCurrent(), true);
   TargetCritic.Save(FileName + "Crt.nnw", Critic.getRecentAverageError(), 0, 0, TimeCurrent(), true);
   delete Result;
  }

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

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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   if(!IsNewBar())
      return;
//---
   int bars = CopyRates(Symb.Name(), TimeFrame, iTime(Symb.Name(), TimeFrame, 1), HistoryBars, Rates);
   if(!ArraySetAsSeries(Rates, true))
      return;
//---
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();
   Symb.Refresh();
   Symb.RefreshRates();

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

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

   if(!FirstBar)
     {
      if(!TargetActor.feedForward(GetPointer(State), 1, false, GetPointer(Account)))
         return;
      if(!TargetCritic.feedForward(GetPointer(TargetActor), LatentLayer, GetPointer(TargetActor)))
         return;
      TargetCritic.getResults(Result);
      float reward = (float)(account[0] - PrevBalance + Result[0]);
      if(account[0] == PrevBalance)
         if((buy_value + sell_value) == 0)
            reward -= 1;
      Result.Update(0, reward);
      if(!Critic.backProp(Result, GetPointer(Actor)) || !Actor.backPropGradient(GetPointer(PrevAccount), GetPointer(Gradient)))
         return;
     }

Обратите внимание, что для обратного прохода Критика мы используем обновленный метод backProp с передачей буфера целевых значений и указателя на модель Актера. При этом мы не указываем идентификатор латентного слоя, так как ранее (при прямом проходе) осуществили подмену объектов.

А вот для обратного прохода Актера мы используем метод backPropGradient. В котором по модели распространяется градиент от обратного прохода Критика.

Выполнение обратного прохода Критика и Актера позволяет нам оптимизировать Q-функцию нашей модели.

Далее осуществим прямой проход обучаемой модели.

   if(!Actor.feedForward(GetPointer(State), 1, false, GetPointer(Account)))
      return;
   if(!Critic.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actor)))
      return;

Здесь стоит обратить внимание на такой аспект, что в процессе обучения Q-функцию мы лишь повышаем качество прогноза ожидаемого вознаграждения. Но не обучаем Актера к повышению доходности его действий. Для этой цели алгоритмом DDPG предусмотрено обновление параметров Актера в направлении повышения прогнозного вознаграждения. Стоит обратить внимание, что в этот момент мы пропускаем градиент ошибки через Критика, но не обновляем его параметры. Поэтому мы отключаем обновление матриц весов Критика путем установления флага TrainMode в значение false. И после обратного прохода Актера возвращаем положение флага в значение true.

   if(!FirstBar)
     {
      Critic.getResults(Result);
      Result.Update(0, Result.At(0) + MathAbs(Result.At(0) * 0.0001f));
      Critic.TrainMode(false);
      if(!Critic.backProp(Result, GetPointer(Actor)) || !Actor.backPropGradient(GetPointer(Account), GetPointer(Gradient)))
         return;
      Critic.TrainMode(true);
     }

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

   FirstBar = false;
   PrevAccount.AssignArray(GetPointer(Account));
   PrevAccount.BufferCreate(Actor.GetOpenCL());
   PrevBalance = account[0];
   PrevEquity = account[1];

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

   vector<float> temp;
   Actor.getResults(temp);
   float delta = MathAbs(ActorResult - temp).Sum();
   ActorResult = temp;
//---
   double min_lot = Symb.LotsMin();
   double stops = MathMax(Symb.StopsLevel(), 1) * Symb.Point();
   double buy_lot = MathRound((double)ActorResult[0] / min_lot) * min_lot;
   double sell_lot = MathRound((double)ActorResult[3] / min_lot) * min_lot;
   double buy_tp = NormalizeDouble(Symb.Ask() + ActorResult[1], Symb.Digits());
   double buy_sl = NormalizeDouble(Symb.Ask() - ActorResult[2], Symb.Digits());
   double sell_tp = NormalizeDouble(Symb.Bid() - ActorResult[4], Symb.Digits());
   double sell_sl = NormalizeDouble(Symb.Bid() + ActorResult[5], Symb.Digits());
//---
   if(ActorResult[0] > min_lot && ActorResult[1] > stops && ActorResult[2] > stops && buy_sl > 0)
      Trade.Buy(buy_lot, Symb.Name(), Symb.Ask(), buy_sl, buy_tp);
   if(ActorResult[3] > min_lot && ActorResult[4] > stops && ActorResult[5] > stops && sell_tp > 0)
      Trade.Sell(sell_lot, Symb.Name(), Symb.Bid(), sell_sl, sell_tp);

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

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

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

   if(temp.Min() < 0 || MathMax(temp[0], temp[3]) > 1.0f || MathMax(temp[1], temp[4]) > (Symb.Point() * 5000) ||
      MathMax(temp[2], temp[5]) > (Symb.Point() * 2000))
     {
      temp[0] = (float)(Symb.LotsMin() * (1 + MathRand() / 32767.0 * 5));
      temp[3] = (float)(Symb.LotsMin() * (1 + MathRand() / 32767.0 * 5));
      temp[1] = (float)(Symb.Point() * (MathRand() / 32767.0 * 500.0 + Symb.StopsLevel()));
      temp[4] = (float)(Symb.Point() * (MathRand() / 32767.0 * 500.0 + Symb.StopsLevel()));
      temp[2] = (float)(Symb.Point() * (MathRand() / 32767.0 * 200.0 + Symb.StopsLevel()));
      temp[5] = (float)(Symb.Point() * (MathRand() / 32767.0 * 200.0 + Symb.StopsLevel()));
      Result.AssignArray(temp);
      Actor.backProp(Result, GetPointer(PrevAccount), GetPointer(Gradient));
     }
  }

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

После завершения всех операций мы переходим в режим ожидания следующего тика.

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


3. Тестирование

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

Обучение модели

Обучение модели в режиме реального времени вносит свои коррективы и не позволяет использовать несколько параллельных агентов. Поэтому первые проверки корректности работы алгоритма советника проводились в режиме одиночного прогона. А дале был выбран режим медленной оптимизации и активирован только 1 локальный агент оптимизации.

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

Управление количеством проходов оптимизации

Примерно после 3000 проходов мне удалось получить модель, способную генерировать прибыль на обучающей выборке. За период обучения в 5 месяцев модель совершила 334 сделки. Более 84% из них были прибыльные. В результате была получена прибыль в размере 33% от первоначального капитала. При этом просадка по балансы составила менее 1%, а по Эквити — 7,6%. Профит фактор превысил значение 26, а фактор восстановления составил 3.16. На представленном ниже графике видна тенденция к росту баланса. И линия баланса практически постоянно ниже линии Эквити, что свидетельствует об открытии позиций в правильном направлении. В то же время нагрузка на депозит составляет около 20%. Это довольно высокий показатель, но не превышает накопленную прибыль.

Результаты обучения модели

Результаты обучения модели

К сожалению, вне обучающей выборки результаты работы советника были скромнее.


Заключение

В данной статье мы исследовали применение обучения с подкреплением в контексте непрерывного пространства действий и познакомились с методом Deep Deterministic Policy Gradient (DDPG). Этот подход открывает новые возможности для обучения агента управлять капиталом и рисками, что является важным аспектом успешной торговли.

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

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

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


Ссылки

  • Continuous Control with Deep Reinforcement Learning
  • Нейросети — это просто (Часть 27): Глубокое Q-обучение (DQN)
  • Нейросети — это просто (Часть 29): Алгоритм актер-критик с преимуществом (Advantage actor-critic)
  • Нейросети — это просто (Часть 46): Обучение с подкреплением, направленное на достижение целей (GCRL)

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

    # Имя Тип Описание
    1 Study.mq5  Советник Советник обучения агента
    2 Test.mq5 Советник Советник для тестирования модели
    3 NeuroNet.mqh Библиотека класса Библиотека классов для создания нейронной сети
    4 NeuroNet.cl Библиотека Библиотека кода программы OpenCL


    Прикрепленные файлы |
    MQL5.zip (305.57 KB)
    Последние комментарии | Перейти к обсуждению на форуме трейдеров (3)
    Viktor Kudriavtsev
    Viktor Kudriavtsev | 29 июн. 2023 в 19:00
    Здравствуйте. Советник после 30-50 проходов перестаёт открывать сделки совсем. Это нормально или что-то надо править? Я сделал 5-7 попыток с новых файлах модели. Когда то чуть больше проходов продолжает открывать сделки, а когда то чуть меньше. Но всё равно перестаёт открывть сделки. Я пробовал одну из моделей тренировать в 4000 проходов. Результат один - прямая линия.
    Dmitriy Gizlyk
    Dmitriy Gizlyk | 3 июл. 2023 в 14:19
    Viktor Kudriavtsev #:
    Здравствуйте. Советник после 30-50 проходов перестаёт открывать сделки совсем. Это нормально или что-то надо править? Я сделал 5-7 попыток с новых файлах модели. Когда то чуть больше проходов продолжает открывать сделки, а когда то чуть меньше. Но всё равно перестаёт открывть сделки. Я пробовал одну из моделей тренировать в 4000 проходов. Результат один - прямая линия.

    Добрый день, Виктор.

    Обучение модели довольно длительный процесс. В библиотеки установлен коэффициент обучения на уровне 3.0e-4f. Т.е. если вы будете обучать модель только на 1 примере, она сможет его выучить примерно за 4000 итераций. Столь малый коэффициент обучения используется, чтобы модель могла усреднить веса для максимального соответствия обучающей выборки.

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

    star-ik
    star-ik | 8 янв. 2024 в 05:00

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

          float reward = (account[0] - PrevBalance) / PrevBalance;

          if(account[0] == PrevBalance)
             if((buy_value + sell_value) == 0)

                reward -= 1;

    Пробовал такие варианты

          float reward = (account[0] - PrevBalance) / PrevBalance;

          if(account[0] == PrevBalance)

             if((buy_value + sell_value) == 0)

                reward -= 1;

          if(buy_profit<10) 

             reward -= 1;

          if(buy_profit>10) 

             reward += 1;   

          if(sell_profit<10) 

             reward -= 1;

          if(sell_profit>10) 

             reward += 1;

    Не помогло. Подскажите, что делать, плиз.

    DoEasy. Элементы управления (Часть 32):  горизонтальный "ScrollBar", прокрутка колесиком мышки DoEasy. Элементы управления (Часть 32): горизонтальный "ScrollBar", прокрутка колесиком мышки
    В статье завершим разработку функционала объекта-горизонтальной полосы прокрутки. Сделаем возможность прокрутки содержимого контейнера перемещением ползунка полосы прокрутки и вращением колёсика мышки. Также внесём дополнения в библиотеку с учётом появившейся в терминале новой политики исполнения ордеров и новых кодов ошибок времени выполнения в MQL5.
    Возможности Мастера MQL5, которые вам нужно знать (Часть 6): Преобразование Фурье Возможности Мастера MQL5, которые вам нужно знать (Часть 6): Преобразование Фурье
    Преобразование Фурье, введенное Жозефом Фурье, является средством разложения сложных волновых точек данных на простые составляющие волны. Эта особенность может быть полезной для трейдеров, и именно ее мы и рассмотрим в этой статье.
    Возможности СhatGPT от OpenAI в контексте разработки на языках MQL4 и MQL5 Возможности СhatGPT от OpenAI в контексте разработки на языках MQL4 и MQL5
    В данной статье мы будем экспериментировать и разбираться с искусственным интеллектом ChatGPT от OpenAI, для того чтобы понять его возможности с целью уменьшения времени и трудоемкости разработки ваших советников, индикаторов и скриптов. Я быстро пройдусь по данной технологии и постараюсь показать вам, как правильно её использовать для программирования на языках MQL4 и MQL5.
    Реализация алгоритма обучения ARIMA на MQL5 Реализация алгоритма обучения ARIMA на MQL5
    В этой статье мы реализуем алгоритм, который применяет интегрированную модель авторегрессии скользящей средней (модель Бокса-Дженкинса) с использованием метода минимизации функции Пауэллса. Бокс и Дженкинс утверждали, что большинство временных рядов можно смоделировать с помощью одной или обеих из двух структур.