preview
Нейросети в трейдинге: Адаптивное восприятие рыночной динамики (Энкодер)

Нейросети в трейдинге: Адаптивное восприятие рыночной динамики (Энкодер)

MetaTrader 5Торговые системы |
260 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Введение

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

В отличие от классических моделей, которые работают с дискретизированными свечами и усредняют динамику, event-driven подход рассматривает рынок как непрерывную реактивную систему. Каждое событие (тик, всплеск объёма, мгновенная ликвидность) имеет собственный вес и смысл. Это позволяет уйти от потери информации, неизбежной при грубом квантовании времени. И такой подход ближе к природе реальной рыночной динамики. Он помогает моделям учиться не только на значениях, но и на структуре временных отношений между ними.

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

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

Это дало фундамент для перехода к следующему шагу — к моделям, которые способны распознавать присутствие события и улавливать внутреннее направление потока. Умение различать как именно это происходит прямо сейчас — ключевое условие для построения систем, способных прогнозировать не по следам события, а по траектории его разворачивания. И именно здесь начинается территория фреймворка STE-FlowNetсобытийно-динамического взгляда на рынок.

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

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

Для финансовых рынков это имеет критическое значение. Цена не движется сама по себе — её толкают кластеры микроимпульсов ликвидности, и большинство из них остаются невидимыми в классических OHLC-представлениях. Даже высокочастотная свеча лишь фиксирует результат борьбы, но не раскрывает её внутреннюю динамику. Нам же важно понять — куда именно стремится система прямо сейчас, пока ещё не раскрыта вся сила потенциального движения. Именно это и отличает STE-FlowNet от статистических предсказательных моделей: он не гадает по данным прошлого, а воссоздаёт направление разворачивающегося действия.

Именно ради этой цели мы переносим STE-FlowNet в контекст анализа рыночных временных рядов — не как ещё одну нейросеть для предсказания цены, а как инструмент реконструкции внутреннего поля движения рынка, скрытого в микроструктуре событий. Фактически мы учим модель интерпретировать поток тиков так же, как человек интуитивно чувствует ускорение рыночного напряжения до явного движения цены — только не субъективно и не вероятностно, а с формализуемой вычислимой структурой.

Чтобы приблизить эту идею, представим рынок как силовое поле, в котором каждое событие — не точка, а возмущение, создающее локальное напряжение. Именно это напряжение — не само изменение цены, а направление его потенциала — и становится тем, что нам важно уловить. Spatio-Temporal Event embedding в STE-FlowNet работает как преобразователь этого поля. Он не фиксирует цену как значение, а кодирует вектор изменения состояния во времени и в пространстве событий. Каждое событие интерпретируется не как факт, а как перенос силы. Как микродавление, которое либо рассеивается, либо усиливается в сторону определённого направления.

Следующий шаг — Learnable Flow Reconstruction. Это ядро архитектуры. Модель не просто складывает события одно за другим, а пытается восстановить осмысленную траекторию потока — как если бы она восстанавливала невидимый ветер по колебанию травы. Она извлекает из последовательности событий не историю, а именно движущуюся форму — вектор эволюции. Поэтому STE-FlowNet не предсказывает состояние рынка, он учится читать его внутреннее стремление в реальном времени, пока это стремление ещё только рождается в микроструктуре торгов.

Именно здесь становится очевидным принципиальное отличие STE-FlowNet от классических подходов. Он работает как система, которая считывает намерение рынка ещё до его проявления в ценовых колебаниях. Это не статистический прогноз и не идентификация паттерна — это попытка охватить причинную архитектуру рыночного движения в момент её становления. И в этом состоит ключевой переход: от анализа следствия — к восприятию причины.

Авторская визуализация фреймворка STE-FlowNet представлена ниже.

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


Модуль корреляции

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

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

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

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

Объект стека

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

class CNeuronAddToStack :  public CNeuronBaseOCL
  {
protected:
   uint              iStackSize;
   uint              iDimension;
   uint              iVariables;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return true; }
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronAddToStack(void) : iStackSize(0), iDimension(0), iVariables(0) {};
                    ~CNeuronAddToStack(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint stack_size, uint dimension, uint variables,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   override const   {  return defNeuronAddToStack;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override { };
   //---
   virtual uint      GetStackSize(void) const { return iStackSize; }
   virtual uint      GetDimension(void) const { return iDimension; }
   virtual uint      GetVariables(void) const { return iVariables; }
  };

В структуре класса мы видим объявление лишь 3 переменных, определяющих параметры стека:

  • iStackSize — хранит количество состояний в стеке;
  • iVariables — количество унитарных последовательнойстей в каждом состоянии;
  • iDimension — определяет размерность описания одной унитарной последовательности каждого состояния.

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

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

bool CNeuronAddToStack::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                             uint stack_size, uint dimension, uint variables,
                             ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, stack_size * dimension * variables,
                                                                   optimization_type, batch))
      return false;
//---
   activation = None;
   iStackSize = stack_size;
   iDimension = dimension;
   iVariables = variables;
//---
   return true;
  }

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

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

__kernel void AddToStack(__global const float* inputs,
                         __global float* stack,
                         const int stack_size)
  {
   const size_t id = get_global_id(0);
   const size_t loc_id = get_local_id(1);
   const size_t var = get_global_id(2);
   const size_t dimension = get_global_size(0);
   const size_t total_loc = get_local_size(1);
   const size_t variables = get_global_size(2);

В параметрах кернел принимает указатели на 2 буфера данных: новых состояний (inputs) и текущий стек stack. Кроме того пользователь указывает размер стека stack_size.

В теле кернела сначала инициализируем идентификаторы и размеры пространства задач:

  • id — глобальный индекс элемента в векторе описания состояния одной унитарной последовательности;
  • loc_id — локальный индекс в рабочей группе,
  • var — индекс унитарной последовательности.

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

   const int total = (stack_size - 1) / total_loc;

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

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

   for(int i = total; i >= 0; i--)
     {
      int inp = 0;
      if(i == 0 && loc_id == 0)
         inp = IsNaNOrInf(inputs[RCtoFlat(var, id, variables, dimension, 1)], 0);
      else
         if((i * total_loc + loc_id) < stack_size)
           {
            int shift = RCtoFlat(i * total_loc + loc_id - 1, id, stack_size, dimension, var);
            inp = IsNaNOrInf(stack[shift], 0);
           }
      BarrierLoc

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

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

      if((i * total_loc + loc_id) < stack_size)
        {
         int shift = RCtoFlat(i * total_loc + loc_id, id, stack_size, dimension, var);
         stack[shift] = inp;
        }
     }
  }

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

Этот алгоритм обеспечивает динамическое обновление стека с параллельной обработкой, позволяя STE-FlowNet анализировать последовательность состояний в реальном времени без потери производительности и корректности данных.

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

На стороне нашего класса мы создадим метод-обертку данного кернела по уже знакомой схеме.

bool CNeuronAddToStack::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL || !OpenCL)
      return false;
//---
   if(NeuronOCL.Neurons() < int(iDimension * iVariables))
      return false;
//---
   uint global_work_offset[3] = {0};
   uint global_work_size[3] = { iDimension,
                                MathMin((uint)OpenCL.GetMaxLocalSize(1), iStackSize),
                                iVariables
                              };
   uint local_work_size[3] = { 1, global_work_size[1], 1 };
   uint kernel = def_k_AddToStack;
   setBuffer(kernel, def_k_ats_inputs, NeuronOCL.getOutputIndex())
   setBuffer(kernel, def_k_ats_stack, getOutputIndex())
   setArgument(kernel, def_k_ats_stack_size, (int)iStackSize)
   kernelExecuteLoc(kernel, global_work_offset, global_work_size, local_work_size)
//---
   return true;
  }

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

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

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

bool CNeuronAddToStack::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL || !OpenCL)
      return false;
   if(NeuronOCL.Neurons() < int(iDimension * iVariables))
      return false;
   if(!DeConcat(NeuronOCL.getGradient(), PrevOutput, Gradient, iDimension,
                               iDimension * (iStackSize - 1), iVariables))
      return false;
   Deactivation(NeuronOCL)
//---
   return true;
  }

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

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

Объект корреляции

Теперь, когда у нас есть надёжный объект для организации стека состояний, логично перейти к следующему этапу — построению модуля корреляции. В духе оригинальной работы авторов, мы будем оценивать сходство между состояниями через скалярное произведение (dot‑product) соответствующих векторов их описания. Однако важно уточнить контекст. Вместо работы с фиксированными кадрами мы применяем эту меру в разрезе унитарных последовательностей — коротких, согласованных по смыслу блоков событий, которые удобно обрабатывать в режиме реального времени.

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

Предложенный подход мы реализуем в рамках нового класса CNeuronStackCorrelation.

class CNeuronStackCorrelation :  public CNeuronBaseOCL
  {
protected:
   CNeuronAddToStack cStack;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return true; }
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronStackCorrelation(void);
                    ~CNeuronStackCorrelation(void);
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint stack_size, uint dimension, uint variables,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   override const   {  return defNeuronStackCorrelation;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   virtual bool      Clear(void) override;
  };


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

bool CNeuronStackCorrelation::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                   uint stack_size, uint dimension, uint variables,
                                   ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, variables * stack_size, optimization_type, batch))
      return false;

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

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

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

   if(!cStack.Init(0, 0, OpenCL, stack_size, dimension, variables, optimization, iBatch))
      return false;
//---
   return true;
  }

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

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

bool CNeuronStackCorrelation::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cStack.FeedForward(NeuronOCL))
      return false;

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

Далее начинается ключевой этап — вычисление корреляции текущего состояния с историей. Для этого используется операция матричного умножения MatMul. В качестве левой матрицы берётся сформированный стек, а в качестве правой — текущее состояние. Результат записывается в буфер результатов Output.

   if(!MatMul(cStack.getOutput(), NeuronOCL.getOutput(), Output, cStack.GetStackSize(),
              cStack.GetDimension(), 1, cStack.GetVariables(), true))
      return false;

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

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

   if(activation != None)
      if(!Activation(Output, Output, activation))
         return false;
//---
   return true;
  }

Таким образом, прямой проход строится по принципу Память —> Сравнение —> Интерпретация. Обновляется стек. Вычисляются корреляции. Результат опционально преобразуется к нужному формату. Всё лаконично, строго последовательно и полностью совместимо с потоковой обработкой данных в реальном времени.

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

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

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

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

   if(!MatMulGrad(cStack.getOutput(), cStack.getGradient(),
                  NeuronOCL.getOutput(), cStack.getPrevOutput(),
                  Gradient, cStack.GetStackSize(),
                  cStack.GetDimension(), 1, cStack.GetVariables(), true))
      return false;

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

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

   if(NeuronOCL.Activation() != None)
      if(!DeActivation(NeuronOCL.getOutput(), cStack.getPrevOutput(),
                       cStack.getPrevOutput(), NeuronOCL.Activation()))
         return false;

Следом передаем градиенты ошибки текущего состояния по магистрали нашего стека.

   if(!NeuronOCL.CalcHiddenGradients(cStack.AsObject()))
      return false;

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

   if(!SumAndNormilize(NeuronOCL.getGradient(), cStack.getPrevOutput(),
                       NeuronOCL.getGradient(), cStack.GetDimension(), false, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

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


Энкодер

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

class CNeuronSTEFlowNetEncoder   :  public CNeuronSpikeConvGRU
  {
protected:
   CLayer            cRecurrentLine;
   CLayer            cFlow;
   CLayer            cCorrelation;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronSTEFlowNetEncoder(void);
                    ~CNeuronSTEFlowNetEncoder(void);
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint &chanels[], uint &units[], uint group_size,
                          uint groups, uint heads, uint dimension_k, uint stack_size,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual CNeuronBaseOCL* GetLayer(uint layer);
   virtual uint      Layers(void)   const { return (cRecurrentLine.Total() + 1) / 3; }
   //--
   virtual int       Type(void)   override const   {  return defNeuronSTEFlowNetEncoder;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      TrainMode(bool flag) override;
   virtual bool      Clear(void) override;
  };

Внутри класса объявлены три внутренних компонента: cRecurrentLin, cFlow и cCorrelation. Это динамические массивы указателей на объекты, которые отвечают соответственно за обработку временной последовательности, извлечение потоковой информации и оценку локальных зависимостей между состояниями.

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

bool CNeuronSTEFlowNetEncoder::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                    uint &chanels[], uint &units[], uint group_size,
                                    uint groups, uint heads, uint dimension_k, uint stack_size,
                                    ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(chanels.Size() != units.Size())
      return false;

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

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

   const uint layers = chanels.Size() - 1;
   if(!CNeuronSpikeConvGRU::Init(numOutputs, myIndex, open_cl, units[layers], chanels[layers] + stack_size,
                                 chanels[layers], optimization_type, batch))
      return false;

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

   CNeuronSSAMResNeXtBlock*   conv_res = NULL;
   CNeuronStackCorrelation*   correl = NULL;
   CNeuronSpikeConvGRU*       gru = NULL;
   CNeuronBaseOCL*            neuron = NULL;
//---
   cRecurrentLine.Clear();
   cFlow.Clear();
   cCorrelation.Clear();
   cRecurrentLine.SetOpenCL(OpenCL);
   cFlow.SetOpenCL(OpenCL);
   cCorrelation.SetOpenCL(OpenCL);

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

   uint index = 0;
   for(uint i = 0; i <= layers; i++)
     {
      conv_res = new CNeuronSSAMResNeXtBlock();
      if(!conv_res ||
         !conv_res.Init(0, index, OpenCL, chanels[i], chanels[i + 1],
                        units[i], units[i + 1], group_size, groups, heads,
                        dimension_k, optimization, iBatch) ||
         !cRecurrentLine.Add(conv_res))
        {
         DeleteObj(conv_res)
         return false;
        }

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

      index++;
      neuron = new CNeuronBaseOCL();
      if(!neuron ||
         !neuron.Init(0, index, OpenCL, units[i + 1] * (chanels[i + 1]*stack_size), optimization, iBatch) ||
         !cRecurrentLine.Add(neuron))
        {
         DeleteObj(neuron)
         return false;
        }

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

      if(i == layers)
         break;
      //---
      if(i < layers - 1)
        {
         index++;
         gru = new CNeuronSpikeConvGRU();
         if(!gru ||
            !gru.Init(0, index, OpenCL, units[i + 1], chanels[i + 1] + stack_size, chanels[i + 1],
                                                                          optimization, iBatch) ||
            !cRecurrentLine.Add(gru))
           {
            DeleteObj(gru)
            return false;
           }
        }

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

      index++;
      correl = new CNeuronStackCorrelation();
      if(!correl ||
         !correl.Init(0, index, OpenCL, stack_size, chanels[i + 1], units[i + 1], optimization, iBatch) ||
         !cCorrelation.Add(correl))
        {
         DeleteObj(correl)
         return false;
        }

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

      index++;
      conv_res = new CNeuronSSAMResNeXtBlock();
      if(!conv_res ||
         !conv_res.Init(0, index, OpenCL, chanels[i + 1], chanels[i + 2],
                        units[i + 1], units[i + 2], group_size, groups, heads,
                        dimension_k, optimization, iBatch) ||
         !cFlow.Add(conv_res))
        {
         DeleteObj(conv_res)
         return false;
        }
     }
//---
   return true;
  }

Таким образом, метод постепенно выстраивает разветвленную архитектуру:

  • рекуррентную последовательность из ResNeXt‑блоков, базовых нейронов и GRU;
  • корреляционную линию для анализа связей между каналами;
  • потоковую ветку, которая усиливает пространственные зависимости.

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

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

bool CNeuronSTEFlowNetEncoder::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   CNeuronBaseOCL *recur = NeuronOCL;
   CNeuronBaseOCL *flow = cRecurrentLine[0];
   CNeuronStackCorrelation *correl = NULL;
   CNeuronSSAMResNeXtBlock* resnext = NULL;
   int layers = (cRecurrentLine.Total() + 1) / 3;
//---
   for(int i = 0; i < layers; i++)
     {
      //--- ResNeXt
      if(!cRecurrentLine[i * 3] || !cRecurrentLine[i * 3].FeedForward(recur))
         return false;
      recur = cRecurrentLine[i * 3];

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

      //--- Correlation
      correl = cCorrelation[i];
      if(!correl ||
         !correl.FeedForward(flow))
         return false;

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

      //--- Concatenate
      resnext = recur;
      recur = cRecurrentLine[i * 3 + 1];
      if(!recur ||
         !Concat(resnext.getOutput(), correl.getOutput(), recur.getOutput(), resnext.GetChannelsOut(),
                                                        correl.GetStackSize(), resnext.GetUnitsOut()))
         return false;

Если текущий слой не является последним, в ход идёт GRU‑ячейка, которая добавляет глобальную временную динамику и обеспечивает рекуррентную обработку.

      if((i * 3 + 2) == cRecurrentLine.Total())
         break;
      //--- GRU
      if(!cRecurrentLine[i * 3 + 1] ||
         !cRecurrentLine[i * 3 + 2].FeedForward(recur))
         return false;
      recur = cRecurrentLine[i * 3 + 1];

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

      if(!cFlow[i] ||
         !cFlow[i].FeedForward(flow))
         return false;
      flow = cFlow[i];
     }
//---
   return CNeuronSpikeConvGRU::feedforward(recur);
  }

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

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

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


Заключение

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

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

Ссылки


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

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

Прикрепленные файлы |
MQL5.zip (3222.94 KB)
Самоорганизующиеся карты Кохонена в советнике MQL5 Самоорганизующиеся карты Кохонена в советнике MQL5
Самоорганизующиеся карты Кохонена превращают хаос рыночных данных в упорядоченную двумерную карту, где похожие паттерны группируются вместе. Эта статья показывает полную реализацию SOM в торговом советнике MQL5 с четырехстами нейронами и непрерывным обучением. Разбираем алгоритм поиска Best Matching Unit, обновление весов с гауссовой функцией соседства, интеграцию с квантовыми эффектами и создание торговых сигналов. Код открыт, математика понятна, результаты проверяемы.
От новичка до эксперта: Создание подробных торговых отчетов с помощью советника Reporting EA От новичка до эксперта: Создание подробных торговых отчетов с помощью советника Reporting EA
В настоящей статье мы подробно рассмотрим усовершенствование деталей торговых отчетов и отправку окончательного документа по электронной почте в формате PDF. Это знаменует собой прогресс по сравнению с нашей предыдущей работой, поскольку мы продолжаем изучать, каким образом использовать возможности MQL5 и Python для создания и планирования торговых отчетов в наиболее удобных и профессиональных форматах. Присоединяйтесь к нам в этой дискуссии, чтобы узнать больше об оптимизации формирования торговых отчетов в экосистеме MQL5.
Осваиваем JSON: Разработка пользовательского JSON-ридера с нуля на MQL5 Осваиваем JSON: Разработка пользовательского JSON-ридера с нуля на MQL5
В статье приведено пошаговое руководство по созданию пользовательского парсера JSON на языке MQL5, включающего обработку объектов и массивов, проверку ошибок и сериализацию. Вы сможет объединить торговую логику и структурированные данные с помощью гибкого решения для обработки JSON в MetaTrader 5.
Разработка инструментария для анализа движения цен (Часть 12): Внешние библиотеки (III) TrendMap Разработка инструментария для анализа движения цен (Часть 12): Внешние библиотеки (III) TrendMap
Движение рынка определяется силами быков и медведей. Существуют определенные уровни, которые рынок соблюдает из-за действующих на них сил. Уровни Фибоначчи и VWAP особенно сильно влияют на поведение рынка. В этой статье мы рассмотрим стратегию, основанную на VWAP и уровнях Фибоначчи для генерации сигналов.