preview
Нейросети в трейдинге: Многодоменная архитектура анализа финансовых данных (Основные компоненты)

Нейросети в трейдинге: Многодоменная архитектура анализа финансовых данных (Основные компоненты)

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

Введение

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

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

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

В практической части предыдущей статьи мы начали реализацию этих подходов средствами MQL5 и заложили основу будущего фреймворка. Были реализованы базовые элементы, позволяющие перейти от абстрактной концепции к прикладной модели: сформирован блок Per-Token FFN и введен слой сценарных токенов, задающих интерпретацию рыночной ситуации. Это еще не завершенная система, но уже рабочий каркас, в котором данные и контекст перестают существовать отдельно.

На текущем этапе этот каркас требует развития и детализации. В данной статье мы продолжим работу над фреймворком MDL и сосредоточимся на реализации его ключевых компонентов: Unified Tokenizer, Feature Self-Iteration и Domain-Aware Attention. Первый обеспечивает приведение разнородных рыночных данных к единому представлению. Второй позволяет выявлять и уточнять внутренние зависимости между признаками. Третий отвечает за учет текущего состояния рынка в контексте сценариев и задач при распределении внимания модели. В совокупности эти компоненты формируют основу архитектуры, способной интерпретировать данные в контексте текущей рыночной конфигурации, что и является критическим требованием для устойчивых торговых решений.


Модуль унифицированной токенизации

Начнем работу с модуля унифицированной токенизации — Unified Tokenizer. Здесь мы сразу фиксируем важный момент: на вход модели будет поступать не один однородный поток данных, а довольно пестрый набор разнородной информации. В нем будут и последовательности ценового движения, и значения технических индикаторов, и контекстные данные вроде состояния счета, наличия открытых позиций, объема риска и других параметров, без которых торговая картина остается неполной. В реальной торговле все это живет не по отдельности, а в тесной связке. И именно поэтому модель должна уметь сводить такой набор значений к единому, согласованному формату. Иначе она будет смотреть на рынок как на разрозненный архив цифр, а нам необходима цельная оценка торговой ситуации.

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

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

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

class CNeuronUnifiedTokenizer :  public CNeuronFieldPatternEmbedding
  {
protected:
   CNeuronBatchNormOCL        cNorm;
   CNeuronMultiWindowsConvOCL cProj;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronUnifiedTokenizer(void) {};
                    ~CNeuronUnifiedTokenizer(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint &dimensions[], uint units,
                          uint embed_size, uint candidates, uint topK,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      Save(const int file_handle) override;
   virtual bool      Load(const int file_handle) override;
   //---
   virtual int       Type(void) override const  {  return defNeuronUnifiedTokenizer; }
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   virtual void      TrainMode(bool flag) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
  };

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

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

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

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

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

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

bool CNeuronUnifiedTokenizer::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                   uint &dimensions[], uint units,
                                   uint embed_size, uint candidates, uint topK,
                                   ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   uint count = dimensions.Size();
   if(count < units)
      ReturnFalse;

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

   if(!CNeuronFieldPatternEmbedding::Init(numOutputs, myIndex, open_cl, embed_size,
                                          units, embed_size, candidates, topK, optimization_type, batch))
      ReturnFalse;

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

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

   uint index = 0;
   uint total_windows = 0;
   for(uint i = 0; i < count; i++)
      total_windows += dimensions[i];

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

   if(!cNorm.Init(0, index, OpenCL, total_windows, iBatch, optimization))
      ReturnFalse;
   cNorm.SetActivationFunction(None);

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

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

   index++;
   if(!cProj.Init(0, index, OpenCL, dimensions, embed_size, 1, 1, optimization, iBatch))
      ReturnFalse;
   cProj.SetActivationFunction(SIGMOID);
//---
   return true;
  }

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

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

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

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

bool CNeuronUnifiedTokenizer::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cNorm.FeedForward(NeuronOCL))
      ReturnFalse;

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

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

   if(!cProj.FeedForward(cNorm.AsObject()))
      ReturnFalse;

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

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

   if(!CNeuronFieldPatternEmbedding::feedForward(cProj.AsObject()))
      ReturnFalse;
//---
   return true;
  }

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

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

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


Domain-Feature Attention

Следующим этапом нашей работы станет построение модуля Domain-Feature Attention. В исходной архитектуре MDL для этой роли предлагается почти классический Cross-Attention, дополненный блоком Per-Token FFN. С инженерной точки зрения решение вполне аккуратное, но для нашей задачи у него есть важное ограничение. В качестве контекста здесь используются токены признаков, а значит, при росте глубины исторической последовательности и увеличении числа анализируемых параметров вычислительная нагрузка быстро становится заметной. Для финансовых рынков это уже не абстрактная сложность, а вполне практический вопрос. Особенно если речь идет об онлайн-анализе для принятия торговых решений.

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

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

class CNeuronDomainAwareAttention :  public CNeuronPerTokenFFN
  {
protected:
   uint              iQUnits;
   uint              iXUnits;
   uint              iHeads;
   uint              iXDimension;
   //---
   CBufferFloat      cLogSumExp;
   CLayer            cPrepareQ;
   CLayer            cW0;
   //---
   virtual bool      AttentionOut(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Context);
   virtual bool      AttentionInsideGradients(CNeuronBaseOCL *prevLayer, CBufferFloat *SecondInput,
                                              CBufferFloat *SecondGradient,
                                              ENUM_ACTIVATION SecondActivation = None);
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override { ReturnFalse; }
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Context) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *prevLayer, CBufferFloat *SecondInput,
                                        CBufferFloat *SecondGradient,
                                        ENUM_ACTIVATION SecondActivation = None) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *prevLayer) override { ReturnFalse; }
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { ReturnFalse; }
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Context) override;

public:
                     CNeuronDomainAwareAttention(void) {};
                    ~CNeuronDomainAwareAttention(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint dimension_q, uint units_q, uint heads,
                          uint dimension_x, uint unit_x,
                          uint embed_size, uint candidates, uint topK,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      Save(const int file_handle) override;
   virtual bool      Load(const int file_handle) override;
   //---
   virtual int       Type(void) override const  {  return defNeuronDomainAwareAttention; }
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   virtual void      TrainMode(bool flag) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
  };

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

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

Внутри объекта предусмотрены параметры iQUnits, iXUnits, iHeads и iXDimension, которые задают размерности запросов, признаков, число голов внимания и размерность входного пространства признаков. Иными словами, класс сразу рассчитан на работу с раздельно определенными токенами контекста и токенами рыночной истории, а не с их смешением в одном общем массиве. Это дает более ясную геометрию вычислений и облегчает контроль над тем, как именно формируется итоговое представление.

Отдельного внимания заслуживают внутренние компоненты cLogSumExp, cPrepareQ и cW0. Первый используется для численно устойчивой работы с нормировкой внимания, что особенно важно при больших значениях длины последовательности и при использовании схем, близких к FlashAttention. Второй отвечает за подготовку Query-представления, то есть за приведение контекстных токенов к форме, удобной для последующего сопоставления с признаками.

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

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

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

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

bool CNeuronDomainAwareAttention::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                       uint dimension_q, uint units_q, uint heads,
                                       uint dimension_x, uint unit_x,
                                       uint embed_size, uint candidates, uint topK,
                                       ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   cLogSumExp.BufferFree();
//---
   if(!CNeuronPerTokenFFN::Init(numOutputs, myIndex, open_cl, dimension_q, units_q,
                                embed_size, candidates, topK, optimization_type, batch))
      ReturnFalse;

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

   iQUnits = units_q;
   iXUnits = unit_x;
   iHeads = heads;
   iXDimension = dimension_x;

После этого формируется внутренний вычислительный конвейер. Блок cPrepareQ отвечает за подготовку Query-представления. Здесь последовательно добавляются два слоя CNeuronFieldAwareConv, которые выполняют роль матриц преобразования, аналогичных матрицам 𝑊Q и 𝑊K, но уже в логике оптимизации вычислений, предложенной в фреймворке STCA. Первый расширяет исходное пространство запросов до размерности, согласованной с числом голов внимания.

   cPrepareQ.Clear();
   cW0.Clear();
   cPrepareQ.SetOpenCL(OpenCL);
   cW0.SetOpenCL(OpenCL);
//--- Query
   uint index = 0;
   uint head_size = (dimension_q + heads - 1) / heads;
     {
      CNeuronFieldAwareConv* conv = new CNeuronFieldAwareConv();
      if(!conv ||
         !conv.Init(0, index, OpenCL, dimension_q, heads * head_size, units_q, embed_size,
                    candidates, topK, optimization, iBatch) ||
         !cPrepareQ.Add(conv))
         DeleteObjAndFalse(conv);

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

      index++;
      conv = new CNeuronFieldAwareConv();
      if(!conv ||
         !conv.Init(0, index, OpenCL, head_size, dimension_x, units_q * heads, embed_size,
                    candidates, topK, optimization, iBatch) ||
         !cPrepareQ.Add(conv))
         DeleteObjAndFalse(conv);
     }

Важно, что оба слоя добавляются динамически, и их параметры напрямую зависят от конфигурации задачи. Более того, использование CNeuronFieldAwareConv позволяет организовать независимое Per-Token формирование запросов. Каждый токен обрабатывается отдельно, со своими параметрами и собственной траекторией преобразования. Это устраняет эффект усреднения, характерный для более простых реализаций, и позволяет сохранить индивидуальность каждого элемента.

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

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

//--- W0
   index++;
   CNeuronBaseOCL* neuron = new CNeuronBaseOCL();
   if(!neuron ||
      !neuron.Init(0, index, OpenCL, dimension_x * units_q * heads, optimization, iBatch) ||
      !cW0.Add(neuron))
      DeleteObjAndFalse(neuron);
   neuron.SetActivationFunction(None);

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

   index++;
   CNeuronSpikeConvBlock* conv = new CNeuronSpikeConvBlock();
   if(!conv ||
      !conv.Init(0, index, OpenCL, dimension_x, dimension_x, head_size, units_q * heads,
                                                             1, optimization, iBatch) ||
      !cW0.Add(conv))
      DeleteObjAndFalse(conv);
   index++;
   conv = new CNeuronSpikeConvBlock();
   if(!conv ||
      !conv.Init(0, index, OpenCL, heads * head_size, heads * head_size, dimension_q,
                                                    units_q, 1, optimization, iBatch) ||
      !cW0.Add(conv))
      DeleteObjAndFalse(conv);

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

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

   if(!cLogSumExp.BufferInit(units_q * heads, 0) ||
      !cLogSumExp.BufferCreate(OpenCL))
      ReturnFalse;
//---
   return true;
  }

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

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

  • подготовка запросов,
  • вычисление внимания,
  • агрегация,
  • завершающая Per-Token обработка.

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

bool CNeuronDomainAwareAttention::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Context)
  {
   CNeuronBaseOCL* prev = NeuronOCL;
   CNeuronBaseOCL* curr = NULL;
   for(int i = 0; i < cPrepareQ.Total(); i++)
     {
      curr = cPrepareQ[i];
      if(!curr ||
         !curr.FeedForward(prev))
         ReturnFalse;
      prev = curr;
     }

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

Далее управление передается в AttentionOut, где фактически происходит вызов кернела MHFlashSTCA.

   if(!AttentionOut(prev, Context))
      ReturnFalse;

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

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

   prev = cW0[0];
   for(int i = 1; i < cW0.Total(); i++)
     {
      curr = cW0[i];
      if(!curr ||
         !curr.FeedForward(prev))
         ReturnFalse;
      prev = curr;
     }

В конечном итоге данные приводятся к размерности, сопоставимой с исходным пространством запросов.

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

   if(!SumAndNormalize(NeuronOCL.getOutput(), prev.getOutput(), prev.getOutput(),
                       prev.Neurons() / iQUnits, true, 0, 0, 0, 1))
      ReturnFalse;

Для финансовых данных это критично. Без такой стабилизации даже корректно рассчитанное внимание может давать нестабильный выход.

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

   if(!CNeuronPerTokenFFN::feedForward(prev))
      ReturnFalse;
//---
   return true;
  }

Это финальный штрих, после которого данные готовы к передаче в следующие слои модели.

В итоге объект CNeuronDomainAwareAttention представляет собой полноценный конвейер: подготовка запросов → внимание → агрегация → нормализация → Per-Token обработка. Такая организация позволяет сохранить баланс между выразительностью и вычислительной эффективностью, что и требуется при работе с финансовыми временными рядами.


Feature Self-Iteration

Далее мы переходим к работе над модулем Feature Self-Iteration. В исходной версии MDL для этой задачи предлагается использовать классический Self-Attention, что выглядит естественным с точки зрения архитектуры. Признаки должны взаимодействовать между собой, уточняя внутренние зависимости. Однако при переходе к финансовым временным рядам такая схема быстро сталкивается с ограничениями. С ростом глубины истории и числа признаков вычислительная сложность начинает расти быстрее, чем практическая отдача от такого пересчета.

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

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

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

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

class CNeuronFeatureSelfIteration   :  public CNeuronDomainAwareAttention
  {
protected:
   CNeuronAddToStack          cStack;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Context) override
     { return        feedForward(NeuronOCL); }
   virtual bool      calcInputGradients(CNeuronBaseOCL *prevLayer, CBufferFloat *SecondInput,
                                        CBufferFloat *SecondGradient,
                                        ENUM_ACTIVATION SecondActivation = None) override
     { return        calcInputGradients(prevLayer); }
   virtual bool      calcInputGradients(CNeuronBaseOCL *prevLayer) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Context) override
     { return        updateInputWeights(NeuronOCL); }

public:
                     CNeuronFeatureSelfIteration(void) {};
                    ~CNeuronFeatureSelfIteration(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint dimension_q, uint units_q, uint heads, uint stack_size,
                          uint embed_size, uint candidates, uint topK,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      Save(const int file_handle) override;
   virtual bool      Load(const int file_handle) override;
   //---
   virtual int       Type(void) override const  {  return defNeuronFeatureSelfIteration; }
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   virtual void      TrainMode(bool flag) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual bool      Clear(void) override;
   //---
   virtual CNeuronAddToStack*   GetStack(void) { return cStack.AsObject(); }
  };

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

С архитектурной точки зрения класс унаследован от CNeuronDomainAwareAttention, и это тоже неслучайно. Мы сохраняем уже отлаженную логику доменно-ориентированного внимания, но переносим ее в режим итеративного обновления. Поэтому в CNeuronFeatureSelfIteration переопределены методы прямого прохода, расчета градиентов и обновления весов. При этом интерфейс остается минимальным: вариант с контекстом сведён к базовому вызову без отдельной ветки, что подчеркивает главную идею модуля — работать через внутреннюю последовательность накопления, а не через разветвленную схему внешних входов.

Отдельно стоит отметить метод инициализации. В нем сначала вызывается инициализация родительского класса.

bool CNeuronFeatureSelfIteration::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                       uint dimension, uint units, uint heads, uint stack_size,
                                       uint embed_size, uint candidates, uint topK,
                                       ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronDomainAwareAttention::Init(numOutputs, myIndex, open_cl, dimension, units, heads,
                                         dimension, stack_size * units, embed_size,
                                         candidates, topK, optimization_type, batch))
      ReturnFalse;

Важно отметить, что в качестве параметров dimension_q и dimension_x передается одно и то же значение dimension. Это логично. На входе и в контексте мы работаем с признаками одной природы. А вот параметр unit_x задается как произведение stack_size*units. То есть размер признакового пространства контекста сразу увязывается с глубиной стека. Модуль изначально проектируется на накопленную историю признаков состояний. Это и есть та точка, где обычное внимание превращается в механизм итеративного обновления.

Далее инициализируется стек.

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

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

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

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

bool CNeuronFeatureSelfIteration::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cStack.FeedForward(NeuronOCL))
      ReturnFalse;

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

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

   if(!CNeuronDomainAwareAttention::feedForward(NeuronOCL, cStack.getOutput()))
      ReturnFalse;
//---
   return true;
  }

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

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

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


Заключение

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

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

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

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


Ссылки


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

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

Проект представлен по ссылке.

Прикрепленные файлы |
MQL5.zip (3837.37 KB)
Торговые инструменты на MQL5 (Часть 14): Прокручиваемый текстовый холст с пиксельной точностью, сглаживанием и закругленной полосой прокрутки Торговые инструменты на MQL5 (Часть 14): Прокручиваемый текстовый холст с пиксельной точностью, сглаживанием и закругленной полосой прокрутки
В этой статье мы улучшим ценовую панель на основе холста (canvas) в MQL5, добавляя прокручиваемую текстовую панель с пиксельной точностью для руководств по использованию, преодолевающую собственные ограничения на прокрутку за счет настраиваемого сглаживания и округлого дизайна полосы прокрутки с функцией расширения при наведении курсора. Текстовая панель поддерживает фоны темы оформления с непрозрачностью, динамический перенос строк для содержимого, такого как инструкции и контакты, и интерактивную навигацию с помощью кнопок вверх / вниз, перетаскивания ползунка и прокрутки колесика мыши в области основного текста.
Оптимизатор ястребов Харриса — Harris Hawks Optimization (HHO) Оптимизатор ястребов Харриса — Harris Hawks Optimization (HHO)
Мы реализуем в MQL5 алгоритм Harris Hawks Optimization и разбираем пять режимов движения агентов, управляемых единственным параметром — убывающей энергией побега E. Представлен класс C_AO_HHO, совместимый с унифицированным тестовым стендом, с воспроизводимой реализацией полёта Леви. Алгоритм протестирован на функциях Hilly, Forest и Megacity при 5, 25 и 500 координатах — результаты указывают на аномальное поведение.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Оптимизация и форвард-анализ стратегий (Часть 1): Метод Пардо — базовая модель Оптимизация и форвард-анализ стратегий (Часть 1): Метод Пардо — базовая модель
Статья показывает, как выстроить воспроизводимый процесс разработки и проверки торговых систем в MetaTrader 5: от формализации правил входа/выхода и риск‑менеджмента до пост‑оптимизационной валидации. В основу положен "Метод Пардо": разбиение истории на in‑sample/out‑of‑sample, форвард‑тестирование, мульти‑рынки/таймфреймы и выбор устойчивых "плато" параметров вместо единичных пиков. На примерах PardoSystem и советников PardoEA / Breakout_Bounce показан практический тест‑план для тестера стратегий MetaTrader 5.