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

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

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

Введение

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

Архитектура HimNet выстроена просто и логично: на вход подаются отрезки временных рядов длиной T по N локациям. Сначала модель формирует два типа обучаемых эмбеддингов — временные и пространственные. Временные эмбединги (время суток, день недели) ловят циклы и режимы — утренние всплески при открытии, дневные провалы и ночную тишину. Пространственные эмбединги — отдельный вектор на каждую локацию или тикер — кодируют профиль временного ряда. Все эти векторы хранятся в словарях и в процессе обучения уточняются. Они выступают в качестве Запросов к пулам метапараметров и позволяют модели подбирать разные веса для разных рыночных контекстов, которые определяет поведение графовой свёртки. Такая генерация на лету даёт гибкость метаобучения без лавинного потребления памяти и вычислительных ресурсов.

Сердце HimNet — это графовые рекуррентные блоки (GCRU), усиленные базисом Чебышева. Этот архитектурный приём делает модель одновременно глубокой и прагматичной. GCRU читает сеть торговых площадок как граф: каждый узел — это тикер или площадка, рёбра отражают корреляцию, кросс-спред или эмпирическую связь. Полиномы Чебышева дают компактную реализацию K-hop агрегации: модель учитывает влияние соседей на расстоянии до K рёбер, но делает это без тяжеловесной спектральной диагонализации — быстро, локально и численно устойчиво. Проще говоря, вместо вычисления полного спектра графа, авторы фреймворка шаг за шагом строят набор матриц, которые захватывают влияние ближайших соседей, их соседей и так далее, а затем комбинируют эти эффекты с учётом контекста.

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

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

С точки зрения алгебры и чисел, для каждого состояния моделируем K уровней Чебышева, конкатенируем результаты и прогоняем через матрицу, генерируемую метапулом по запросу эмбеддинга. Градиенты обратно поступают как в параметры пулов, так и в эмбеддинги. Поэтому модель учится не только прогнозировать, но и выделять полезные пространственно-временные контексты. На практике это даёт адаптацию, позволяющую модели выбирать режим работы, релевантный именно в данный момент.

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

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

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

На практике это превращается в конкретный инструмент контроля — от автоматических триггеров переключения в консервативный режим до прозрачных отчётов для риск-менеджмента, что делает HimNet управляемым и понятным для трейдера и инженера.

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

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

Практическая часть первой статьи подтвердила, что идея работает в реальной инженерной среде. Мы перенесли тяжёлые операции построения полиномов Чебышева и обратного распространения ошибки на GPU, реализовав на OpenCL кернелы ChebStep и ChebStepGrad.

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

В данной статье мы продолжаем работу по реализации подходов фреймворка HimNet средствами MQL5.



Объект графовой свертки

На следующем этапе нашей работы переходим к созданию объекта графовой свёртки, который будет использовать базис Чебышева в качестве вычислительной оптики для K-hop агрегации. Этот модуль — ключевой исполнитель: он принимает подготовленные окна признаков, запрашивает у CChebPolinom готовые матрицы 𝑇𝑘 и, опираясь на текущие метапараметры, конструирует выходные последовательности, готовые пойти дальше по модели. Важно, чтобы он был одновременно гибким (поддерживал разные режимы метапараметризации), быстрым и предсказуемым (строгое управление памятью и проверка чисел).

Архитектура нового объекта CNeuronHimNetGrapConv задумана как прагматичный и модульный переходник между временными окнами признаков и графовой свёрткой — он объединяет подготовку данных, вызов матричных операций и интеграцию с OpenCL-кернелами, не захламляя высокоуровневую логику модели деталями реализации.

class CNeuronHimNetGrapConv   :  public CNeuronTransposeRCDOCL
  {
protected:
   CNeuronBaseOCL    cX_G;

public:
                     CNeuronHimNetGrapConv(void) {};
                    ~CNeuronHimNetGrapConv(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint count, uint window, uint cheb_k,
                          ENUM_OPTIMIZATION optimization_type, uint batch) override;
   //---
   virtual bool      FeedForward(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support);
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support);
   //---
   virtual bool      Load(const int file_handle) override;
   //---
   virtual int       Type(void)        const                      {  return defNeuronHimNetGrapConv; }
   virtual void      SetOpenCL(COpenCLMy *obj);
   virtual uint      GetCount(void) override const { return iWindow; }
   virtual uint      GetWindow(void) override const { return GetDimension(); }
   virtual uint      GetChebK(void) const { return iCount; }
  };

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

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

bool CNeuronHimNetGrapConv::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                 uint count, uint window, uint cheb_k,
                                 ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronTransposeRCDOCL::Init(numOutputs, myIndex, open_cl, cheb_k,
                                    count, window, optimization_type, batch))
      return false;
   if(!cX_G.Init(0, 0, OpenCL, Neurons(), optimization, iBatch))
      return false;
//---
   return true;
  }

Затем инициализируется вспомогательный буфер cX_G, который служит рабочей площадкой для уже склеенных данных и хранит результат умножения матриц. Если хоть одна из этих операций не прошла — Init возвращает false, и объект остаётся в безопасном состоянии.

Метод прямого прохода FeedForward действует как дирижёр. Сначала метод строго проверяет совместимость полученных в параметрах объектов: переданный NeuronOCL и Support должны существовать. Кроме того у Support должны совпадать ожидаемые Dimension и Steps с настройками модуля.

bool CNeuronHimNetGrapConv::FeedForward(CNeuronBaseOCL *NeuronOCL,
                                        CChebPolinom *Support)
  {
   if(!NeuronOCL || !Support)
      return false;
   if(Support.GetDimension() != iWindow ||
      Support.GetSteps() != iCount)
      return false;
   if(NeuronOCL.Neurons() != (Neurons() / iCount))
      return false;

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

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

if(!MatMul(Support.getOutput(), NeuronOCL.getOutput(), cX_G.getOutput(),
           iWindow, iWindow, GetDimension(), iCount, false))
   return false;

По смыслу это та самая операция, которая строит X_G конкатенацию образов T0 X, T1 X, …, Tk X. В трейдинговой аналогии: Support — это карта взаимосвязей между унитарными последовательностями (полиномы Чебышева), NeuronOCL — это окно признаков мультимодальной временной последовательности, а cX_G — это уже проанализированная версия исходных данных, где для каждой унитарной последовательности учтено влияние соседей. Именно эту скорректированную матрицу нам и предстоит передать дальше.

Но обратите внимание, что в результате операции матричного умножения мы получаем последовательность из нескольких наборов скорректированных исходных данных разной детализации {K-hop, N, C}. И это не то представление данных, которое ожидается на выходе объекта. Поэтому последним шагом метода прямого прохода мы передаём cX_G в одноименный метод родительского класса, который транспонирует тензор в ожидаемое представление. Такая последовательность делает код модульным, что упрощает тестирование и оптимизацию.

Если на каком-то этапе что-то идёт не так, метод аккуратно возвращает false, не оставляя побочных эффектов.

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

Метод calcInputGradients начинает с блока проверки полученных параметров: валидны ли переданные указатели на предыдущий слой и матрицу полиномов Чебышева. Эта проверка — своего рода страховочный трос, чтобы избежать некорректных вычислений в случае рассинхронизации структуры модели.

bool CNeuronHimNetGrapConv::calcInputGradients(CNeuronBaseOCL *NeuronOCL,
      CChebPolinom *Support)
  {
   if(!NeuronOCL || !Support)
      return false;
   if(Support.GetDimension() != iWindow ||
      Support.GetSteps() != iCount)
      return false;
   if(NeuronOCL.Neurons() != (Neurons() / iCount))
      return false;

Далее метод сверяет размерности: число шагов (K-hop) и размер окна должны соответствовать ожиданиям. Этот этап важен, поскольку графовые свёртки чувствительны к несоответствию размерностей — ошибка здесь может потянуть за собой цепочку неверных градиентов и обрушить весь процесс обучения.

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

   if(!CNeuronTransposeRCDOCL::calcInputGradients(cX_G.AsObject()))
      return false;
   if(!MatMulGrad(Support.getOutput(), Support.getGradient(),
                  NeuronOCL.getOutput(), NeuronOCL.getGradient(),
                  cX_G.getGradient(), iWindow, iWindow,
                  GetDimension(), iCount, false))
      return false;
//---
   return true;
  }

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

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

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


Рекуррентный блок

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

class CNeuronHimNetGCRU    :  public CNeuronBaseOCL
  {
protected:
   CNeuronBaseOCL          cInpAndHidden;
   CNeuronHimNetGrapConv   cZ_R;
   CNeuronConvOCL          cZ_R_emb;
   CNeuronBaseOCL          cZe_Re;
   CNeuronBaseOCL          cZ;
   CNeuronBaseOCL          cR;
   CNeuronBaseOCL          cCandidate;
   CNeuronHimNetGrapConv   cHC;
   CNeuronConvOCL          cHC_emb;
   CNeuronBaseOCL          cHCe;

public:
                     CNeuronHimNetGCRU(void) {};
                    ~CNeuronHimNetGCRU(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units, uint window, uint window_out,
                          uint cheb_k, uint embed_dim,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL,
                                 CChebPolinom *Support,
                                 CNeuronBaseOCL *Embedding);
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL,
                                        CChebPolinom *Support,
                                        CNeuronBaseOCL *Embedding);
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL,
                                        CChebPolinom *Support,
                                        CNeuronBaseOCL *Embedding);
   //---
   virtual int       Type(void)   const   {  return defNeuronHimNetGCRU;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle);
   virtual bool      Load(int const file_handle);
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override { };
  };

Здесь уже недостаточно просто применить свёртку по соседям узла или прогнать данные через рекуррентную ячейку. Финансовые рынки, как мы знаем, редко ведут себя линейно: валютные пары образуют взаимосвязанные кластеры, акции двигаются по секторам, а сырьевые активы часто резонируют с валютой стран-экспортёров. Поэтому одна лишь временная модель не справится — ей нужна карта связей, и графовая свёртка даёт эту карту. Но сама по себе она статична. Чтобы в моменте принимать решение — стоит ли усиливать сигнал по EURUSD, если соседние пары GBPUSD и EURGBP демонстрируют противоположную динамику, или гасить его. Нужна управляющая логика. Именно это и реализует данный класс.

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

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

bool CNeuronHimNetGCRU::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                             uint units, uint window, uint window_out,
                             uint cheb_k, uint embed_dim,
                             ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, units * window_out,
                                                   optimization_type, batch))
      return false;
   activation = None;

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

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

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

   int index = 0;
   if(!cInpAndHidden.Init(0, index, OpenCL, (window + window_out)*units, optimization, iBatch))
      return false;
   cInpAndHidden.SetActivationFunction(None);

Затем на сцену выходит cZ_R, отвечающий за формирование комбинированных матриц Z и R — ключевых ворот GRU. Здесь уже подключается механизм работы с графовыми структурами: размерности тензора анализируемых данных, метод принимает количество членов полинома Чебышёва, что напрямую влияет на глубину аппроксимации графа.

   index++;
   if(!cZ_R.Init(0, index, OpenCL, units, window + window_out, cheb_k, optimization, iBatch))
      return false;
   cZ_R.SetActivationFunction(None);

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

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

   index++;
   if(!cZ_R_emb.Init(0, index, OpenCL, embed_dim, embed_dim,
                     2 * cheb_k * (window + window_out) * window_out,
                     1, units, optimization, iBatch))
      return false;
   cZ_R_emb.SetActivationFunction(None);
   index++;
   if(!cZe_Re.Init(0, index, OpenCL, 2 * window_out * units, optimization, iBatch))
      return false;
   cZe_Re.SetActivationFunction(None);
   index++;
   if(!cZ.Init(0, index, OpenCL, window_out * units, optimization, iBatch))
      return false;
   cZ.SetActivationFunction(None);
   index++;
   if(!cR.Init(0, index, OpenCL, window_out * units, optimization, iBatch))
      return false;
   cR.SetActivationFunction(None);

После этого метод инициализирует несколько более лёгких, но не менее значимых блоков — cZe_Re, cZ, cR. Эти модули формируют окончательные векторы Z и R, обеспечивая гибкое управление забыванием и обновлением информации. Их размерность напрямую зависит от окна признаков и количества элементов последовательности в тензоре результатов — фактически это отражение того, насколько модель способна держать в памяти актуальный рыночный контекст.

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

   index++;
   if(!cCandidate.Init(0, index, OpenCL, cInpAndHidden.Neurons(), optimization, iBatch))
      return false;
   cCandidate.SetActivationFunction(None);

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

   index++;
   if(!cHC.Init(0, index, OpenCL, units, window + window_out, cheb_k, optimization, iBatch))
      return false;
   cHC.SetActivationFunction(None);
   index++;
   if(!cHC_emb.Init(0, index, OpenCL, embed_dim, embed_dim,
                    (window + window_out) * window_out * cheb_k,
                    1, units, optimization, iBatch))
      return false;
   cHC_emb.SetActivationFunction(None);
   index++;
   if(!cHCe.Init(0, index, OpenCL, window_out * units, optimization, iBatch))
      return false;
   cHCe.SetActivationFunction(None);
//---
   if(!Output.Fill(0))
      return false;
//---
   return true;
  }

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

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

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

bool CNeuronHimNetGCRU::feedForward(CNeuronBaseOCL *NeuronOCL,
                                    CChebPolinom *Support,
                                    CNeuronBaseOCL *Embedding)
  {
   if(!NeuronOCL || !Support || !Embedding)
      return false;
   if(!SwapOutputs())
      return false;

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

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

   uint cheb_k = cZ_R.GetChebK();
   uint units = cZ_R.GetCount();
   uint window_out = Neurons() / units;
   uint window = cZ_R.GetWindow() - window_out;

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

   if(!Concat(NeuronOCL.getOutput(), PrevOutput, cInpAndHidden.getOutput(), window, window_out, units))
      return false;

Затем включается cZ_R, где полиномы Чебышева выполняет графовую свёртку: соседние узлы влияют друг на друга, но влияние ограничено K-hop, что обеспечивает экономное использование ресурсов и стабильность вычислений.

   if(!cZ_R.FeedForward(cInpAndHidden.AsObject(), Support))
      return false;
   if(!cZ_R_emb.FeedForward(Embedding))
      return false;

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

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

   if(!MatMul(cZ_R.getOutput(), cZ_R_emb.getOutput(), cZe_Re.getOutput(), 1,
                 (window + window_out)*cheb_k, 2 * window_out, units, true))
      return false;
   if(!Activation(cZe_Re.getOutput(), cZe_Re.getOutput(), SIGMOID))
      return false;
   if(!DeConcat(cZ.getOutput(), cR.getOutput(), cZe_Re.getOutput(), window_out,
                                                            window_out, units))
      return false;

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

   if(!ElementMult(cZ.getOutput(), PrevOutput, cZ.getPrevOutput()))
      return false;
   if(!Concat(NeuronOCL.getOutput(), cZ.getPrevOutput(), cCandidate.getOutput(),
                                                      window, window_out, units))
      return false;
   if(!cHC.FeedForward(cCandidate.AsObject(), Support))
      return false;
   if(!cHC_emb.FeedForward(Embedding))
      return false;
   if(!MatMul(cHC.getOutput(), cHC_emb.getOutput(), cHCe.getOutput(), 1, 
                  (window + window_out)*cheb_k, window_out, units, true))
      return false;
   if(!Activation(cHCe.getOutput(), cHCe.getOutput(), TANH))
      return false;
   if(!GateElementMult(PrevOutput, cHCe.getOutput(), cR.getOutput(), Output))
      return false;
//---
   return true;
  }

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

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

Метод calcInputGradients начинается с тщательной проверки всех компонентов: тензора исходных данных, объекта полиномов Чебышева и эмбеддингов.

bool CNeuronHimNetGCRU::calcInputGradients(CNeuronBaseOCL *NeuronOCL,
      CChebPolinom *Support,
      CNeuronBaseOCL *Embedding)
  {
   if(!NeuronOCL || !Support || !Embedding)
      return false;
//---
   uint cheb_k = cZ_R.GetChebK();
   uint units = cZ_R.GetCount();
   uint window_out = Neurons() / units;
   uint window = cZ_R.GetWindow() - window_out;

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

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

   if(!GateElementMultGrad(PrevOutput, cR.getPrevOutput(),
                           cHCe.getOutput(), cHCe.getGradient(),
                           cR.getOutput(), cR.getGradient(),
                           Gradient, None, TANH, cR.Activation()))
      return false;

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

   if(!MatMulGrad(cHC.getOutput(), cHC.getGradient(),
                  cHC_emb.getOutput(), cHC_emb.getGradient(),
                  cHCe.getGradient(), 1, (window + window_out)*cheb_k,
                  window_out, units, true))
      return false;

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

   if(!cHC.calcInputGradients(cCandidate.AsObject(), Support))
      return false;
   if(!DeConcat(NeuronOCL.getGradient(), cZ.getPrevOutput(), cCandidate.getGradient(),
                window, window_out, units))
      return false;

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

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

   if(!ElementMultGrad(cZ.getOutput(), cZ.getGradient(),
                       PrevOutput, cCandidate.getPrevOutput(),
                       cZ.getPrevOutput(), None, None))
      return false;
   if(!Concat(cZ.getGradient(), cR.getGradient(), cZe_Re.getGradient(),
              window_out, window_out, units))
      return false;
   if(!DeActivation(cZe_Re.getOutput(), cZe_Re.getGradient(), cZe_Re.getGradient(), SIGMOID))
      return false;

Особое внимание уделяется полиномам Чебышева. Сначала сохраняется предыдущий градиент, полученный от объекта cHC. Затем вызывается calcInputGradients для cZ_R, после чего накопленные градиенты аккуратно суммируются. Это позволяет модели учитывать прямое и косвенное влияние соседних узлов графа, обеспечивая стабильное и корректное обновление весов графовой свертки.

   if(!MatMulGrad(cZ_R.getOutput(), cZ_R.getGradient(),
                  cZ_R_emb.getOutput(), cZ_R_emb.getGradient(),
                  cZe_Re.getGradient(), 1, (window + window_out)*cheb_k,
                  2 * window_out, units, true))
      return false;
   CBufferFloat* temp = Support.getGradient();
   if(!Support.SetGradient(Support.getPrevOutput(), false) ||
      !cZ_R.calcInputGradients(cInpAndHidden.AsObject(), Support) ||
      !SumAndNormilize(temp, Support.getGradient(), temp, Support.GetDimension(), false, 0, 0, 0, 1) ||
      !Support.SetGradient(temp, false))
      return false;

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

   if(!DeConcat(cInpAndHidden.getPrevOutput(), cCandidate.getPrevOutput(),
                cInpAndHidden.getGradient(), window, window_out, units))
      return false;
   if(!SumAndNormilize(NeuronOCL.getGradient(), cInpAndHidden.getPrevOutput(),
                       NeuronOCL.getGradient(), window, false, 0, 0, 0, 1))
      return false;
   if(NeuronOCL.Activation() != None)
      if(!DeActivation(NeuronOCL.getOutput(), NeuronOCL.getGradient(), 
                       NeuronOCL.getGradient(), NeuronOCL.Activation()))
         return false;

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

   if(!Embedding.CalcHiddenGradients(cHC_emb.AsObject()))
      return false;
   temp = Embedding.getGradient();
   if(!Embedding.SetGradient(Embedding.getPrevOutput(), false) ||
      !Embedding.CalcHiddenGradients(cZ_R_emb.AsObject()) ||
      !SumAndNormilize(temp, Embedding.getGradient(), temp, cZ_R_emb.GetWindow(), false, 0, 0, 0, 1) ||
      !Embedding.SetGradient(temp, false))
      return false;
//---
   return true;
  }

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

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

Метод updateInputWeights начинается с простой, но принципиальной проверки — наличие объекта эмбеддингов. Без него обновление не имеет смысла, ведь именно через него модель впитывает пространственные и семантические связи графа. Если объект отсутствует, метод тут же прерывает выполнение, возвращая false.

bool CNeuronHimNetGCRU::updateInputWeights(CNeuronBaseOCL *NeuronOCL,
                                           CChebPolinom *Support,
                                           CNeuronBaseOCL *Embedding)
  {
   if(!Embedding)
      return false;
//---
   if(!cZ_R_emb.UpdateInputWeights(Embedding))
      return false;
   if(!cHC_emb.UpdateInputWeights(Embedding))
      return false;
//---
   return true;
  }

Далее управление передается двум главным хранилищам обучаемых параметров: сначала обновляются веса, отвечающие за блок Z-R (cZ_R_emb), затем — за блок кандидата H-C (cHC_emb). Оба вызова UpdateInputWeights отрабатывают последовательно, как два точных винта регулировки, каждый из которых влияет на свою часть вычислительного узла.

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

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

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



Заключение

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

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


Ссылки


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

#ИмяТипОписание
1Study.mq5СоветникСоветник офлайн обучения моделей
2StudyOnline.mq5 Советник Советник онлайн обучения моделей
3Test.mq5СоветникСоветник для тестирования модели
4Trajectory.mqhБиблиотека классаСтруктура описания состояния системы и архитектуры моделей
5NeuroNet.mqhБиблиотека классаБиблиотека классов для создания нейронной сети
6NeuroNet.clБиблиотекаБиблиотека кода OpenCL-программы
Прикрепленные файлы |
MQL5.zip (3013.86 KB)
Интеграция Discord с MetaTrader 5: Создание торгового бота с уведомлениями в реальном времени Интеграция Discord с MetaTrader 5: Создание торгового бота с уведомлениями в реальном времени
В этой статье мы рассмотрим, как интегрировать MetaTrader 5 и сервер Discord, чтобы получать торговые уведомления в реальном времени из любой точки мира. Мы узнаем, как настроить платформу и Discord, чтобы обеспечить отправку оповещений в Discord, а также поговорим о проблемах безопасности, возникающих в связи с использованием WebRequest и вебхуков для таких способов оповещения.
Обучение нелинейного U-Transformer на остатках линейной авторегрессионной модели Обучение нелинейного U-Transformer на остатках линейной авторегрессионной модели
Статья представляет инновационную гибридную систему для прогнозирования валютных курсов, которая сочетает линейную авторегрессионную модель с архитектурой U-Transformer для анализа остатков. Система автоматически переключается между источниками сигналов в зависимости от их качества и включает полноценную торговую логику с averaging/pyramiding стратегиями. Ключевое преимущество подхода заключается в том, что нейросеть обучается на остатках линейной модели, что упрощает задачу и снижает риск переобучения. Реализация выполнена полностью на MQL5 и готова к использованию в реальной торговле с автоматической адаптацией к изменяющимся рыночным условиям.
Построение модели для ограничения диапазона сигналов по тренду (Часть 10): Золотой крест и крест смерти Построение модели для ограничения диапазона сигналов по тренду (Часть 10): Золотой крест и крест смерти
Знаете ли вы, что стратегии "Золотой крест" (Golden Cross) и "Крест смерти" (Death Cross), основанные на пересечении скользящих средних, являются одними из самых надежных индикаторов для определения долгосрочных рыночных трендов? "Золотой крест" сигнализирует о бычьем тренде, когда более короткая скользящая средняя пересекает более длинную снизу вверх, в то время как "крест смерти" указывает на медвежий тренд, когда короткая скользящая средняя опускается ниже длинной. Несмотря на их простоту и эффективность, ручное применение этих стратегий часто приводит к упущенным возможностям или задержке сделок.
От начального до среднего уровня: Struct (I) От начального до среднего уровня: Struct (I)
Сегодня мы начнем изучать структуры более простым, практичным и комфортным способом. Структуры являются одной из основ программирования, независимо от того, структурированы они или нет. Я знаю, что по мнению многих, структуры - это просто коллекции данных, но уверяю вас, что это гораздо больше, чем просто структуры. И здесь мы начнем исследовать эту новую вселенную наиболее дидактическим способом.