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

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

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

Введение

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

Фреймворк INFNet предлагает именно такую конструкцию. Логика фреймворка строится вокруг токенизированного представления данных и выделения специализированных узлов — hub-токенов, через которые организуется обмен информацией. Вместо прямого и дорогостоящего взаимодействия всех со всеми используется схема Aggregation and Broadcasting: сначала информация агрегируется в компактное представление, затем распространяется обратно, уже в согласованном виде. Это сохраняет линейную сложность по числу токенов и не теряет локальную детализацию при работе с длинными последовательностями.

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

Продолжая адаптацию идей INFNet, делаем следующий шаг от концепции к реализации. Сосредоточимся на построении механизма Aggregation and Broadcasting средствами MQL5. Именно здесь токены перестают быть статическими контейнерами признаков и включаются в процесс обмена информацией. Локальные сигналы проходят через слой агрегирования, согласуются с контекстом и возвращаются в виде уточнённых представлений. Это центральный узел всей системы, определяющий, насколько корректно модель сможет связать разрозненные рыночные факторы в единую картину.

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

Авторская визуализация фреймворка INFNet


Генерация hub-токенов

После того как исходные данные уже приведены к токенизированному виду, модель делает первый шаг к их согласованию. Сырым признакам здесь не доверяют — требуется компактное, управляемое представление. Эту роль и берут на себя hub-токены, выступая агрегированным слоем для каждой группы признаков.

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

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

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

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

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

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

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

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

class CNeuronINFNetHUBSequence  :   CNeuronSpikeConvBlock
  {
protected:
   CNeuronSpikeConvBlock   cProjection;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronINFNetHUBSequence(void);
                    ~CNeuronINFNetHUBSequence(void);
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint units_count, uint hub_size, uint variables,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override const  {  return defNeuronINFNetHUBSequence;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual bool      Clear(void) override;
   //---
   virtual uint      GetWindow(void) const { return cProjection.GetWindow();   }
   virtual uint      GetUnits(void) const { return cProjection.GetUnits(); }
   //---
   virtual void      SetOpenCL(COpenCLMy *obj)   override;
   virtual void      TrainMode(bool flag) override;
  };

Внутренняя структура объекта строится вокруг компонента cProjection. По сути, это та самая MLP-проекция, которая приводит признаки каждого временного шага к размерности hub-пространства. Но на этом мы сознательно отходим от оригинальной схемы. В базовом варианте после проекции используется простое усреднение — решение надёжное, но слишком статичное для финансовых данных, где вклад отдельных участков последовательности может резко меняться.

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

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

bool CNeuronINFNetHUBSequence::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                    uint window, uint units_count, uint hub_size, uint variables,
                                    ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   uint hidden_dim = (window + 3) / 4;
   if(!CNeuronSpikeConvBlock::Init(numOutputs, myIndex, open_cl, hidden_dim * units_count,
                     hidden_dim * units_count, hub_size, 1, variables, optimization_type, batch))
      ReturnFalse;
   if(!cProjection.Init(0, 0, OpenCL, window, window, hidden_dim, units_count, variables,
                                                                           optimization, iBatch))
      ReturnFalse;
//---
   return true;
  }

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

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

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

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

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

bool CNeuronINFNetHUBSequence::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cProjection.FeedForward(NeuronOCL))
      ReturnFalse;

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

Далее управление передаётся в родительский класс, но уже с результатом проекции.

   if(!CNeuronSpikeConvBlock::feedForward(cProjection.AsObject()))
      ReturnFalse;
//---
   return true;
  }

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

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

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

Следующий этап — формирование hub-токенов для контекстных признаков. И здесь важно отметить инженерную элегантность подхода: отдельный специализированный блок для этого фактически не требуется. Можно использовать реализованный выше объект CNeuronINFNetHUBSequence, не вводя дополнительной логики. Разница заключается лишь в параметрической настройке. В данном случае параметр units_count устанавливается равным числу контекстных токенов, а variables фиксируется в значении 1.

Такое решение имеет вполне строгую интерпретацию. При variables равном 1 модель отказывается от раздельного параметрического представления для каждого контекстного потока. Все контекстные токены рассматриваются как единое множество признаков. Фактически совокупность контекстных токенов подаётся как единое пространство признаков, после чего через стандартный механизм проекции и агрегации формируется компактный набор hub-токенов. Каждый из них уже содержит информацию обо всей контекстной области.

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

Со сценарными hub-токенами ситуация ещё более прямая — здесь архитектура уже собрана на предыдущем этапе. Основная логика их формирования инкапсулирована в объекте CNeuronINFNetScenarios, и повторно изобретать механизм агрегации нет необходимости.

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

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



Агрегация

После формирования hub-токенов архитектура переходит к следующему узлу — агрегации данных. Здесь отдельные представления начинают слышать друг друга. В оригинальном алгоритме для каждой группы hub-токенов предлагается последовательно выполнять Cross-Attention со всеми группами исходных токенов. Формально это даёт девять вызовов: три группы hub-токенов на три группы исходных данных. Решение корректное, но с инженерной точки зрения — тяжеловесное. Каждый вызов — это не только математика внимания, но и накладные расходы на запуск кернелов, синхронизацию и работу с памятью.

Если смотреть на процесс без привязки к буквальной реализации, становится очевидно, что часть этой нагрузки можно сократить. Все группы hub-токенов обрабатываются по одному и тому же алгоритму. Это означает, что их можно без потери смысла объединить в единый тензор и использовать в качестве общего набора запросов. Такой шаг уменьшает количество вызовов Cross-Attention с девяти до трёх — по одному на каждую группу исходных токенов. С точки зрения вычислений это уже более аккуратная конструкция: меньше запусков, меньше служебных операций, выше плотность полезной работы.

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

Попытка решить задачу через параллельную обработку, по аналогии с многоголовым вниманием, тоже не даёт чистого выигрыша. Разные размеры тензоров требуют либо выравнивания, либо условной логики внутри кернелов. Оба варианта ведут к появлению пустых вычислений — потоков, которые ничего не делают, но потребляют ресурсы. Дополнительно возникает необходимость копирования hub-токенов для каждой головы, что в условиях OpenCL быстро съедает выигрыш от параллелизма. В итоге такая оптимизация начинает работать против нас.

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

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



Broadcast Gated Unit

После этапа агрегации архитектура переходит к блоку Broadcast Gated Unit — и здесь модель возвращает собранную информацию обратно в исходное пространство признаков. Если Cross-Attention отвечает за извлечение и согласование сигналов, то BGU — за их корректное внедрение в каждую группу токенов.

Механика выглядит строго и без лишней экзотики. Результаты Cross-Attention сначала конкатенируются, формируя единое представление для всех hub-токенов. Далее небольшая MLP, работающая в разрезе этих hub-токенов, генерирует два набора параметров — коэффициенты масштабирования и смещения. Речь идёт о параметризованной аффинной трансформации, которая применяется к исходным токенам соответствующих групп.

Ключевой момент здесь — именно Broadcast. Полученные параметры не остаются внутри hub-пространства, а распространяются обратно на исходные данные. Это превращает hub-токены из пассивного агрегата в активный механизм управления: они аккумулируют информацию и напрямую влияют на представление входных признаков.

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

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

Вынесение Broadcast Gated Unit в OpenCL-kernel — решение не декоративное, а строго прагматичное. Здесь убираем лишние проходы по данным и фиксируем весь цикл scale → shift → broadcast в одном вычислительном акте.

Сам kernel INFNetBGU построен как универсальный обработчик для трёх групп токенов — последовательных, контекстных и сценарных. Это важно: вместо трёх отдельных ядер используется единый механизм с ветвлением по индексу v. Такая схема снижает количество запусков и удерживает данные в одном вычислительном контексте.

__kernel void INFNetBGU(__global const float2*  scal_and_shift,
                        __global const float*   sequence,
                        __global const float*   context,
                        __global const float*   scenarios,
                        __global float*      sequence_out,
                        __global float*      context_out,
                        __global float*      scenarios_out,
                        const int   sequence_units,
                        const int   sequence_vars,
                        const int   context_units,
                        const int   scenarios_units
                       )
  {
   const int id_n = get_global_id(0);
   const int d = get_global_id(1);
   const int v = get_global_id(2);
   const int total_n = get_global_size(0);
   const int dimension = get_global_size(1);
   const int variables = get_global_size(2);

Первое, что делает kernel, — определяет, с какой группой данных он работает. Индекс v играет роль селектора:

  • v < sequence_vars — работа с последовательностями, причём с учётом параметрической независимости (variables);

   __global const float* inputs = sequence;
   __global float* outputs = sequence_out;
   int units = 0;
   int var = 0;
   float2 ss = 0;
   if(v < sequence_vars)
     {
      units = sequence_units;
      var = v;
      ss = scal_and_shift[RCtoFlat(v, d, sequence_vars + scenarios_units + 1, dimension, 0)];
     }

  • v == sequence_vars — переход к контекстным токенам;

   else
      if(v == sequence_vars)
        {
         inputs = context;
         outputs = context_out;
         units = context_units;
         if(id_n < units)
            ss = scal_and_shift[RCtoFlat(v, d, sequence_vars + scenarios_units + 1, dimension, 0)];
        }

  • далее — сценарные токены.

      else
        {
         inputs = scenarios;
         outputs = scenarios_out;
         units = scenarios_units;
         if(id_n < units)
            ss = scal_and_shift[RCtoFlat(sequence_vars + 1 + id_n, d, sequence_vars + scenarios_units + 1,
                                                                                           dimension, 0)];
        }

Это решение аккуратно отражает архитектуру INFNet.

Параметры масштабирования и смещения (scal_and_shift) подаются в векторном виде float2, где:

  • s0 — коэффициент масштабирования,
  • s1 — смещение.

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

   ss = IsNaNOrInf2(ss, (float2)0);
   float sig_a = fActivation(ss.s0, ActFunc_SIGMOID);

Далее масштабирование пропускается через сигмоиду. И итоговое преобразование реализовано со встроенной остаточной связью. Добавка +1 к коэффициенту масштабирования гарантирует, что даже при нулевом сигнале от MLP исходное значение не гасится. Это аккуратная, математически чистая интеграция Residual Connection прямо внутрь BGU.

   for(int i = id_n; i < units; i += total_n)
     {
      int shift = RCtoFlat(i, d, units, dimension, var);
      float val = IsNaNOrInf(inputs[shift], 0);
      val = IsNaNOrInf(val * (1 + sig_a) + ss.s1, 0);
      outputs[shift] = val;
     }
  }

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

В результате kernel реализует сразу несколько ключевых принципов:

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

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

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

Если говорить по сути, BGU реализует преобразование вида y = x * (1 + sigmoid(α)) + β. Это удобная и живая конструкция: исходный сигнал не уничтожается, а лишь мягко подстраивается под текущий контекст. Но именно такая мягкость требует аккуратного градиентного учета. Ошибка должна вернуться и в сам токен, и в α, и в β. Причём для токена градиент проходит напрямую через коэффициент (1 + sigmoid(α)), а для параметров уже приходится суммировать вклад всех элементов, которые пользовались одним и тем же набором коэффициентов масштабирования и суммирования.

Поэтому в kernel INFNetBGUGrad всё построено на зеркальной логике к прямому проходу. Сначала определяется, к какой группе относится текущий поток: последовательности, контекст или сценарии.

__kernel void INFNetBGUGrad(__global const float2* scal_and_shift,
                            __global float2*       scal_and_shift_grad,
                            __global const float*  sequence,
                            __global const float*  context,
                            __global const float*  scenarios,
                            __global float*        sequence_grad,
                            __global float*        context_grad,
                            __global float*        scenarios_grad,
                            __global const float*  sequence_out_grad,
                            __global const float*  context_out_grad,
                            __global const float*  scenarios_out_grad,
                            const int sequence_units,
                            const int sequence_vars,
                            const int context_units,
                            const int scenarios_units
                           )
  {
   const int id_n      = get_local_id(0);
   const int d         = get_global_id(1);
   const int v         = get_global_id(2);
   const int total_n   = get_local_size(0);
   const int dimension = get_global_size(1);
//---
   __local float4 Temp[LOCAL_ARRAY_SIZE];
//---
   __global const float* inputs   = sequence;
   __global float*       grad_in  = sequence_grad;
   __global const float* grad_out = sequence_out_grad;
   int    units          = 0;
   int    var            = 0;
   int    ss_idx         = 0;
   float2 ss             = (float2)0;
   bool   scenarios_mode = false;
   const int ss_rows = sequence_vars + scenarios_units + 1;
   if(v < sequence_vars)
     {
      units  = sequence_units;
      var    = v;
      ss_idx = RCtoFlat(v, d, ss_rows, dimension, 0);
      ss     = IsNaNOrInf2(scal_and_shift[ss_idx], (float2)0);
     }
   else
      if(v == sequence_vars)
        {
         inputs   = context;
         grad_in  = context_grad;
         grad_out = context_out_grad;
         units    = context_units;
         if(id_n < units)
           {
            ss_idx   = RCtoFlat(v, d, ss_rows, dimension, 0);
            ss       = IsNaNOrInf2(scal_and_shift[ss_idx], (float2)0);
           }
        }

Для последовательных и контекстных токенов параметры scale/shift общие для группы, а значит, их градиенты нужно аккумулировать по всем связанным токенам. Это как раз тот случай, когда локальная редукция через LocalSum4 оказывается просто необходимой. Собираем вклад всех элементов в пределах work-group и только потом получаем итоговый градиент по параметрам. Так модель обучается строго и без лишнего шума.

      else
        {
         inputs         = scenarios;
         grad_in        = scenarios_grad;
         grad_out       = scenarios_out_grad;
         units          = scenarios_units;
         scenarios_mode = true;
         if(id_n < units)
           {
            ss_idx = RCtoFlat(sequence_vars + 1 + id_n, d, ss_rows, dimension, 0);
            ss     = IsNaNOrInf2(scal_and_shift[ss_idx], (float2)0);
           }
        }
   const float sig_a = fActivation(ss.s0, ActFunc_SIGMOID);
   float d_alpha_acc = 0.0f;
   float d_beta_acc  = 0.0f;
   for(int i = id_n; i < units; i += total_n)
     {
      int   shift = RCtoFlat(i, d, units, dimension, var);
      float x     = IsNaNOrInf(inputs[shift],  0.0f);
      float dy    = IsNaNOrInf(grad_out[shift], 0.0f);
      grad_in[shift] = IsNaNOrInf(dy * (1 + sig_a), 0.0f);
      d_alpha_acc += dy * x;
      d_beta_acc  += dy;
     }
   if(!scenarios_mode)
     {
      float4 sums = LocalSum4((float4)(d_alpha_acc, d_beta_acc, 0.0f, 0.0f), 0, Temp);
      if(get_local_id(0) == 0)
        {
         sums.s0 = Deactivation(sums.s0, sig_a, ActFunc_SIGMOID);
         scal_and_shift_grad[ss_idx] = sums.lo;
        }
     }

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

   else
      if(id_n < units)
         scal_and_shift_grad[ss_idx] = (float2)(Deactivation(d_alpha_acc, sig_a, ActFunc_SIGMOID),
                                                IsNaNOrInf(d_beta_acc,  0.0f));
  }

Особенно важно, что обратный проход полностью повторяет логику адресации прямого. То есть там, где на прямом проходе один набор scale/shift обслуживал множество токенов, на обратно — эти вклады точно так же собираются в одну точку. Благодаря этому BGU остается действительно обучаемым механизмом, который умеет и передавать сигнал, и корректно принимать на себя всю ответственность за его преобразование.

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

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



Заключение

На этом этапе мы не закрываем проект и не подводим итог всей работе, а лишь завершаем очередной инженерный виток. Мы крупными мазками обозначили основные контуры архитектуры, ключевые узлы и реализовали недостающие компоненты, которые позволяют INFNet работать как прикладной механизм в среде MQL5. Здесь особенно хорошо видно, что модель держится на цепочке согласованных решений: токенизация данных, формирование hub-токенов, агрегация через Cross-Attention и возврат сигнала через Broadcast Gated Unit.

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

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


Ссылки


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

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

Проект представлен на forge.mql5.io/dng.

Прикрепленные файлы |
MQL5.zip (3717.49 KB)
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Моделирование рынка (Часть 23): Первые шаги на SQL (VI) Моделирование рынка (Часть 23): Первые шаги на SQL (VI)
В этой статье мы рассмотрим, как выполнить визуализацию и, следовательно, поймем, как структурирована база данных. Это было сделано с помощью анализа внутренней структуры базы данных. Хотя подобные вещи могут показаться излишними, они вполне оправданы, если мы действительно намерены стать администраторами баз данных. Да, есть люди, которые зарабатывают на жизнь, поддерживая и создавая базы данных.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Создание прибыльной торговой системы (Часть 2): Тонкости управления размером позиции Создание прибыльной торговой системы (Часть 2): Тонкости управления размером позиции
Даже при использовании системы с положительными ожиданиями, на успех или неудачу может повлиять размер позиции. Это ключевой аспект управления рисками — преобразование статистических преимуществ в реальные результаты при одновременной защите вашего капитала.