preview
Нейросети в трейдинге: Гибридные модели последовательностей графов (Окончание)

Нейросети в трейдинге: Гибридные модели последовательностей графов (Окончание)

MetaTrader 5Торговые системы | 25 февраля 2025, 13:42
458 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Введение

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

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

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

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

GSM++

В практической часте предыдущей статьи была начата реализация собственного видения подходов, предложенных авторами фреймоврка GSM++, средствами MQL5. С учетом высокой изменчивости финансовых данных мы отказались от предложенной авторами фреймоврка иерархической кластеризации на основе сходства (HAC). Вместо неё используется модуль обучаемой смешанной токенизации, который значительно повышает гибкость и адаптивность модели при работе с реальными рыночными данными.

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

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

Объединение этих токенов осуществляется с использованием механизма Attention Pooling, который заимствован из фреймворка R-MAT. Этот метод позволяет модели фокусироваться на наиболее значимых признаках, отбрасывая менее важные данные, что существенно улучшает процесс принятия решений. Основное преимущество Attention Pooling заключается в способности эффективно обрабатывать сложные структуры данных, выделяя наиболее релевантные характеристики и минимизируя влияние шума.

Для реализации предложенного подхода был создан объект CNeuronMoT, который наследует базовую функциональность от CNeuronMHAttentionPooling, обеспечивая эффективное применение алгоритма Attention Pooling. Такой модульный подход повышает адаптивность модели, позволяет ей лучше обрабатывать рыночные данные, а также улучшает качество анализа и прогнозирования ценовых движений, что делает этот инструмент полезным для алгоритмического трейдинга.

Следующий этап обработки данных — локальное кодирование узлов. В своей реализации мы решили использовать ранее созданный модуль адаптивного сглаживания признаков. Метод адаптивного сглаживания признаков узлов (Node-Adaptive Feature Smoothing — NAFS) позволяет формировать более информативные эмбединги узлов, которые учитывают как структурные характеристики графа, так и особенности отдельных узлов. Данный метод основан на предположении, что разные узлы могут обладать различной степенью сглаживания. Это делает возможным адаптивную обработку каждого узла с учетом его окружения. В NAFS применяется комбинированный подход, использующий сглаживание низкого и высокого порядка, что позволяет эффективно учитывать локальные и глобальные взаимозависимости в графе.

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

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

Завершающим ключевым элементом GSM++ является гибридный энкодер. Авторы фреймворка предлагают в нем объединить модуль Mamba и Transformer. В своей реализации мы придерживаемся предложенной концепции. Однако идем дальше и заменяем Mamba на Chimera, а Transformer на Hidformer.

Chimera использует двумерные модели пространства состояний (2D-SSM), что позволяет ей эффективно моделировать зависимости по временной оси и дополнительному измерению, связанному с топологией графа. Данный подход существенно расширяет возможности анализа сложных рыночных взаимосвязей. Преимущества Chimera включают:

  • Двумерное кодирование зависимостей способствует улучшенному выявлению скрытых рыночных закономерностей и повышению точности прогнозирования;
  • Увеличенную экспрессивность модели, обеспечивающую более глубокий анализ сложных нелинейных взаимосвязей между активами;
  • Адаптивность к динамическим рыночным изменениям позволяет модели оперативно подстраиваться под изменяющиеся условия на рынке.

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

  • Разделение анализа на временную и частотную компоненты обеспечивает более точное прогнозирование динамики рынка;
  • Использование рекурсивного внимания во временном энкодере и линейного внимания в частотном позволяет сократить вычислительную сложность и повысить эффективность работы.

Таким образом, использование Chimera и Hidformer в рамках GSM++ потенциально позволит достичь высокой точности кодирования зависимостей, минимизировать влияние рыночного шума и повысить надежность аналитических прогнозов.


Корректировка SSM-модуля

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

class CNeuronChimeraPlus    :  public CNeuronChimera
  {
protected:
   CNeuron2DSSMOCL    cSSMPlus;
   CLayer             cDiscretizationPlus;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   //---
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronChimeraPlus(void) {};
                    ~CNeuronChimeraPlus(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window_in, uint window_out, uint units_in, uint units_out,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override  const   {  return defNeuronChimeraPlus; }
   //---
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau);
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   virtual bool      Clear(void) override;
  };

Как можно заметить из представленной структуры нового объекта, мы не стали полностью переписывать ранее созданный объект CNeuronChimera. Напротив, мы использовали его в качестве родительского класса, что позволило нам унаследовать весь ранее созданный функционал. Однако добавление нового третьего модуля 2D-SSM и соответствующего блока проекции данных требует переопределения привычного набора виртуальных методов. Инициализация вновь объявленных и унаследованных объектов осуществляется в методе Init, структура параметров которого полностью унаследованы от аналогичного метода родительского класса.

bool CNeuronChimeraPlus::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                              uint window_in, uint window_out, uint units_in, uint units_out,
                              ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronChimera::Init(numOutputs, myIndex, open_cl, window_in, window_out, units_in,
                                                      units_out, optimization_type, batch))
      return false;

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

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

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

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

   int index = 0;
   if(!cSSMPlus.Init(0, index, OpenCL, window_in, window_out, units_in, 2 * units_out, optimization, iBatch))
      return false;

Затем нам необходимо осуществить проекцию результатов в заданное подпространство. И здесь следует обратить внимание, что мы не  можем сразу осуществить проекции по временному измерению. Следовательно, нам необходимо будет собрать небольшую внутреннюю последовательность объектов.

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

   CNeuronTransposeOCL *transp = NULL;
   CNeuronConvOCL      *conv = NULL;
   cDiscretizationPlus.Clear();
   cDiscretizationPlus.SetOpenCL(OpenCL);

Первым мы создаем объект транспонирования данных, который позволит нам привести данные в необходимый вид.

   index++;
   transp = new CNeuronTransposeOCL();
   if(!transp ||
      !transp.Init(0, index, OpenCL, 2 * units_out, window_out, optimization, iBatch) ||
      !cDiscretizationPlus.Add(transp))
     {
      delete transp;
      return false;
     }

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

   index++;
   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, index, OpenCL, 2 * units_out, 2 * units_out, units_out, window_out, 1, optimization, iBatch) ||
      !cDiscretizationPlus.Add(conv))
     {
      delete conv;
      return false;
     }
   conv.SetActivationFunction(None);

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

   index++;
   transp = new CNeuronTransposeOCL();
   if(!transp ||
      !transp.Init(0, index, OpenCL, window_out, units_out, optimization, iBatch) ||
      !cDiscretizationPlus.Add(transp))
     {
      delete transp;
      return false;
     }
   transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation());
//---
   return true;
  }

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

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

bool CNeuronChimeraPlus::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   for(uint i = 0; i < caSSM.Size(); i++)
     {
      if(!caSSM[i].FeedForward(NeuronOCL))
         return false;
     }
   if(!cSSMPlus.FeedForward(NeuronOCL))
      return false;

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

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

   if(!cDiscretization.FeedForward(caSSM[1].AsObject()))
      return false;
   CNeuronBaseOCL *inp = NeuronOCL;
   CNeuronBaseOCL *current = NULL;
   for(int i = 0; i < cDiscretizationPlus.Total(); i++)
     {
      current = cDiscretizationPlus[i];
      if(!current ||
         !current.FeedForward(inp))
         return false;
      inp = current;
     }

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

   inp = NeuronOCL;
   for(int i = 0; i < cResidual.Total(); i++)
     {
      current = cResidual[i];
      if(!current ||
         !current.FeedForward(inp))
         return false;
      inp = current;
     }

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

   inp = cDiscretizationPlus[-1];
   if(!SumAndNormilize(caSSM[0].getOutput(), cDiscretization.getOutput(), Output, 1, false, 0, 0, 0, 1) ||
      !SumAndNormilize(Output, inp.getOutput(), Output, 1, false, 0, 0, 0, 1) ||
      !SumAndNormilize(Output, current.getOutput(), Output, cDiscretization.GetFilters(), true, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

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

Следующим этапом нашей работы является построение алгоритмов обратного прохода. Как обычно, они реализованы в двух методах: calcInputGradients и updateInputWeights. В рамках первого осуществляются операции распределения градиента ошибки между объектами — участниками процесса. Второй — предназначен для корректировки обучаемых параметров моделей. И на данном этапе, в отличие от метода прямого прохода, мы можем использовать функционал родительского класса.

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

bool CNeuronChimeraPlus::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!CNeuronChimera::calcInputGradients(NeuronOCL))
      return false;

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

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

   CNeuronBaseOCL *current = cDiscretizationPlus[-1];
   if(!current ||
      !DeActivation(current.getOutput(), current.getGradient(), Gradient, current.Activation()))
      return false;

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

   for(int i = cDiscretizationPlus.Total() - 2; i >= 0; i--)
     {
      current = cDiscretizationPlus[i];
      if(!current ||
         !current.calcHiddenGradients(cDiscretizationPlus[i + 1]))
         return false;
     }

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

   if(!cSSMPlus.calcHiddenGradients(current.AsObject()))
      return false;

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

   current = cResidual[0];
   CBufferFloat *temp = NeuronOCL.getGradient();
   if(!NeuronOCL.SetGradient(current.getGradient(), false) ||
      !NeuronOCL.calcHiddenGradients(cSSMPlus.AsObject()) ||
      !SumAndNormilize(temp, NeuronOCL.getGradient(), temp, 1, false, 0, 0, 0, 1) ||
      !NeuronOCL.SetGradient(temp, false))
      return false;
//---
   return true;
  }

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

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

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


Построение гибридного декодера

После построения модернизированного модуля Chimera мы переходим к работе над гибридным энкодером. Как было сказано выше, в нашей реализации он содержит модуль Chimera и блок Hidformer. И тут следует вспомнить, что объект Hidformer получает на вход данные анализируемого состояния системы, а на выходе генерирует тензор действий агента. И в таком контексте наш новый объект, наверное, корректнее будет назвать гибридным декодером. Структура объекта представлена ниже.

class CNeuronHypridDecoder :  public CNeuronHidformer
  {
protected:
   CNeuronChimeraPlus   cChimera;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronHypridDecoder(void){};
                    ~CNeuronHypridDecoder(void){};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint window_key, uint units_count,
                          uint heads, uint layers, uint stack_size, uint nactions,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override   const   {  return defNeuronHypridDecoder; }
   //---
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   virtual bool      Clear(void) override;
  };

Как можно заметить, в представленной структуре объявлен только один внутренний объект — модифицированного модуля Chimera, алгоритмы которого описаны выше. Объект CNeuronHidformer используется в качестве родительского класса, что позволяет избежать избыточного дублирования функциональности и эффективно применять уже реализованные методы и структуры без необходимости явного создания дополнительного экземпляра внутри объекта. Тем не менее нам предстоит переопределить привычный набор виртуальных методов.

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

bool CNeuronHypridDecoder::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                uint window, uint window_key, uint units_count,
                                uint heads, uint layers, uint stack_size, uint nactions,
                                ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronHidformer::Init(numOutputs, myIndex, open_cl, window_key, window_key, nactions,
                             heads, layers, stack_size, nactions, optimization_type, batch))
      return false;

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

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

   if(!cChimera.Init(0, 0, OpenCL, window, window_key, units_count, nactions, optimization, iBatch))
      return false;
//---
   return true;
  }

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

Думаю Вы заметили, что алгоритм метода инициализации объекта довольно прост. Такой же простой алгоритм имеют и остальные методы. К примеру, метод прямого прохода feedForward, как обычно, получает в параметрах указатель на объект исходных данных, который сразу передается в одноименный метод модуля Chimera.

bool CNeuronHypridDecoder::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cChimera.FeedForward(NeuronOCL))
      return false;
   return CNeuronHidformer::feedForward(cChimera.AsObject());
  }

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

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


Архитектура модели

Завершив работу по построению отдельных кирпичиков рассматриваемого фреймворка GSM++ мы переходим к созданию целостной архитектуры модели. В данном случае мы обучаем только одну модель — Актера. Описание архитектуры модели представлено в методе CreateDescriptions.

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

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

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

//--- Actor
   actor.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = (HistoryBars * BarDescr);
   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 = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

За ним следует модуль смешанной токенизации.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMoT;
   descr.window = BarDescr;
   descr.count = HistoryBars;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

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

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronS3;
   descr.count = HistoryBars;
   descr.window = BarDescr;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

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

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronNAFS;
   descr.count = HistoryBars;
   descr.window = BarDescr;
   descr.window_out = BarDescr;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

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

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronHypridDecoder;
//--- Windows
     {
      int temp[] = {BarDescr, 120, NActions};      //Window, Stack Size, N Actions
      if(ArrayCopy(descr.windows, temp) < int(temp.Size()))
         return false;
     }
   descr.count = HistoryBars;
   descr.window_out = 32;
   descr.step = 4;                                  // Heads
   descr.layers = 3;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

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

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

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMacroHFTvsRiskManager;
//--- Windows
     {
      int temp[] = {3, 15, NActions, AccountDescr}; //Window, Stack Size, N Actions, Account Description
      if(ArrayCopy(descr.windows, temp) < int(temp.Size()))
         return false;
     }
   descr.count = 10;
   descr.window_out = 16;
   descr.step = 4;                              // Heads
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = NActions / 3;
   descr.window = 3;
   descr.step = 3;
   descr.window_out = 3;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

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

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


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

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

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

Для корректности сравнения обе модели обучены на одной и той же выборке, сформированной ранее для обучения Hidformer. Напомним, что:

  • Обучающая выборка включает исторические данные валютной пары EURUSD на таймфрейме M1 за весь 2024 год.
  • Параметры всех анализируемых индикаторов остаются по умолчанию, без дополнительной оптимизации, что исключает влияние сторонних факторов.
  • Тестирование обученной модели осуществляется на исторических данных Января 2025 года, сохраняя все прочие параметры неизменными, чтобы гарантировать объективность сравнения.

Результаты тестирования представлены ниже.

За период тестирования модель совершила 15 торговых операций, что довольно мало для высокочастотной торговли на таймфрейме M1. Данный показатель даже ниже продемонстрированного базовой моделью (Hidformer). Только 7 из них было закрыто с прибылью, что составило 46.67%. И этот показатель тоже ниже базового 62.07%. Здесь мы видим снижение точности коротких позиций. Однако замечено незначительное снижение размера убыточных позиций при относительном росте аналогичного показателя прибыльных торговых операций.

Если у базовой модели отношение средних прибыльной и убыточной позиций составляло 1.6, то в новой модели данный показатель превышает отметку 4. Это позволило почти в 2 раза увеличить общую прибыль за период тестирования с аналогичным ростом показателя профит-фактор. Данный факт может свидетельствовать, что стратегия, реализованная в новой архитектуре, делает акцент на минимизацию убытков и увеличение прибыли успешных позиций. В долгосрочной перспективе потенциально это позволит обеспечить более устойчивые финансовые результаты. Однако короткий период тестирования и малое количество совершенных торговых операций не позволяет нам судить об эффективности работы модели на длительном временном интервале.


Заключение

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

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

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


Ссылки


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

# Имя Тип Описание
1 Research.mq5 Советник Советник сбора примеров
2 ResearchRealORL.mq5
Советник
Советник сбора примеров методом Real-ORL
3 Study.mq5 Советник Советник обучения моделей
4 Test.mq5 Советник Советник для тестирования модели
5 Trajectory.mqh Библиотека класса Структура описания состояния системы и архитектуры моделей
6 NeuroNet.mqh Библиотека класса Библиотека классов для создания нейронной сети
7 NeuroNet.cl Библиотека Библиотека кода OpenCL-программы
Прикрепленные файлы |
MQL5.zip (2482.85 KB)
Интеграция MQL5 с пакетами обработки данных (Часть 2): Машинное обучение и предиктивная аналитика Интеграция MQL5 с пакетами обработки данных (Часть 2): Машинное обучение и предиктивная аналитика
В нашей серии статей об интеграции MQL5 с пакетами обработки данных мы подробно рассматриваем мощное сочетание машинного обучения и предиктивного анализа. Мы изучим, как беспрепятственно объединить MQL5 с популярными библиотеками машинного обучения, чтобы создавать сложные прогностические модели финансовых рынков.
Возможности Мастера MQL5, которые вам нужно знать (Часть 32): Регуляризация Возможности Мастера MQL5, которые вам нужно знать (Часть 32): Регуляризация
Регуляризация — это форма штрафования функции потерь пропорционально дискретному весу, применяемому ко всем слоям нейронной сети. Мы оценим значимость некоторых форм регуляризации, протестировав советник, собранный в Мастере.
Торговая стратегия SP500 на языке MQL5 для начинающих Торговая стратегия SP500 на языке MQL5 для начинающих
Узнайте, как использовать язык MQL5 для точного прогнозирования индекса S&P 500, добавляя классический технический анализ для обеспечения стабильности и объединяя алгоритмы с проверенными временем принципы для получения надежной информации о рынке.
Распознавание паттернов с использованием динамической трансформации временной шкалы в MQL5 Распознавание паттернов с использованием динамической трансформации временной шкалы в MQL5
В этой статье мы обсудим концепцию динамической трансформации временной шкалы (dynamic time warping) как средства выявления прогностических закономерностей в финансовых временных рядах. Мы рассмотрим, как она работает, а также представим ее реализацию на чистом MQL5.