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

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

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

Введение

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

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

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

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

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

Далее строим базовое ядро архитектуры (backbone) в духе ReGEN-TAD — генеративный модуль параллельного анализа. Одно и то же состояние обрабатывается двумя классами моделей:

  • трансформером, отвечающим за глобальные зависимости и контекст;
  • рекуррентной моделью, фиксирующей последовательную динамику и локальные инерции.

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

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



Модуль токенизации разности

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

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

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

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

class CNeuronDifferenceTokenizer :  public CNeuronBaseOCL
  {
protected:
   CLayer            cFlow;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override { ReturnFalse; }
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { ReturnFalse; }
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *second) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override { ReturnFalse; }
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL,
                                        CBufferFloat *SecondInput,
                                        CBufferFloat *SecondGradient,
                                        ENUM_ACTIVATION SecondActivation = None) override;

public:
                     CNeuronDifferenceTokenizer(void) {};
                    ~CNeuronDifferenceTokenizer(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint inpSize,
                          uint tokenSize, 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 defNeuronDifferenceTokenizer; }
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   virtual void      TrainMode(bool flag) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
  };

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

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

bool CNeuronDifferenceTokenizer::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                      uint inpSize, uint tokenSize, ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, tokenSize, optimization_type, batch))
      ReturnFalse;

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

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

   cFlow.Clear();
   cFlow.SetOpenCL(OpenCL);
//---
   uint index = 0;
   CNeuronBaseOCL* neuron = new CNeuronBaseOCL();
   if(!neuron ||
      !neuron.Init(0, index, OpenCL, inpSize, optimization, iBatch) ||
      !cFlow.Add(neuron))
      DeleteObjAndFalse(neuron);
   neuron.SetActivationFunction(None);

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

   index++;
   CNeuronBatchNormOCL* norm = new CNeuronBatchNormOCL();
   if(!norm ||
      !norm.Init(0, index, OpenCL, inpSize, optimization, iBatch) ||
      !cFlow.Add(norm))
      DeleteObjAndFalse(norm);
   norm.SetActivationFunction(None);

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

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

   index++;
   uint window = 3;
   uint units = (inpSize + window - 1) / window;
   CNeuronSpikeConvBlock* conv = new CNeuronSpikeConvBlock();
   if(!conv ||
      !conv.Init(0, index, OpenCL, window, window, window, units, 1, optimization, iBatch) ||
      !cFlow.Add(conv))
      DeleteObjAndFalse(conv);

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

   conv = new CNeuronSpikeConvBlock();
   if(!conv ||
      !conv.Init(tokenSize, index, OpenCL, 2 * window, window, window, units - 1, 1, optimization, iBatch) ||
      !cFlow.Add(conv))
      DeleteObjAndFalse(conv);
//---
   return true;
  }

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

Финальная агрегация выполняется средствами родительского класса — полносвязным слоем, который объединяет подготовленные признаки в итоговый токен фиксированного размера tokenSize.

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

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

bool CNeuronDifferenceTokenizer::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput)
  {
   if(!NeuronOCL || !SecondInput)
      ReturnFalse;
   CNeuronBaseOCL* neuron = cFlow[0];
   if(!neuron || NeuronOCL.Neurons() < neuron.Neurons() ||
      SecondInput.Total() < neuron.Neurons())
      ReturnFalse;

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

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

   if(!Different(NeuronOCL.getOutput(), SecondInput, neuron.getOutput(), neuron.Neurons(), 0, 0, 0, 1))
      ReturnFalse;

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

Запускается последовательный проход по cFlow.

   for(int i = 1; i < cFlow.Total(); i++)
     {
      neuron = cFlow[i];
      if(!neuron ||
         !neuron.FeedForward(cFlow[i - 1]))
         ReturnFalse;
     }

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

Принципиально важно: на этом этапе нет финального решения. Блок cFlow не формирует токен — он подготавливает его основу.

Финальный шаг — вызов одноимённого метода родительского класса с передачей подготовленных данных.

   if(!CNeuronBaseOCL::feedForward(neuron))
      ReturnFalse;
//---
   return true;
  }

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

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

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

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



Модуль параллельного анализа

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

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

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

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

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

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

class CNeuronReGENTADBackbone :  public CNeuronBaseOCL
  {
protected:
   CLayer                     cSeqToken;
   CLayer                     cTransformer;
   CLayer                     cRecurrent;
   CNeuronDifferenceTokenizer cDifference;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronReGENTADBackbone(void) {};
                    ~CNeuronReGENTADBackbone(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint &dimensions[], uint units_s, uint heads, uint stack_size,
                          uint layers, 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 defNeuronReGENTADBackbone; }
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   virtual void      TrainMode(bool flag) override;
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override { };
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual bool      Clear(void) override;
  };

Внутри него анализируемое состояние сначала проходит через cSeqToken, где приводится к единому токенному представлению. Далее это унифицированное представление передаётся в два независимых потока — cTransformer и cRecurrent. Каждый из них строит собственное латентное описание одного и того же состояния, но делает это через разные индуктивные механизмы. Важно, что до финального этапа между ними нет никакого обмена информацией — это принципиально исключает раннее усреднение и сохраняет чистоту различий.

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

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

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

bool CNeuronReGENTADBackbone::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                   uint &dimensions[], uint units_s, uint heads, uint stack_size,
                                   uint layers, uint embed_size, uint candidates, uint topK,
                                   ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(dimensions.Size() < 2 || layers < 1 || units_s < 1)
      ReturnFalse;
   uint iSequenceDim = units_s * dimensions[0];
   uint iContextDim = 0;
   for(uint i = 1; i < dimensions.Size(); i++)
      iContextDim += dimensions[i];
   uint iScenariosUnits = dimensions.Size() + 1;
   uint iContextUnits = dimensions.Size() - 1;
//---
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, (iScenariosUnits + iContextUnits + 3)*embed_size,
                                                                                 optimization_type, batch))
      ReturnFalse;
   activation = None;

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

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

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

   cSeqToken.Clear();
   cTransformer.Clear();
   cRecurrent.Clear();
   cSeqToken.SetOpenCL(OpenCL);
   cTransformer.SetOpenCL(OpenCL);
   cRecurrent.SetOpenCL(OpenCL);
//--- Sequence tokenizer
   uint index = 0;
   CNeuronBaseOCL* neuron = new CNeuronBaseOCL();
   if(!neuron ||
      !neuron.Init(0, index, OpenCL, iSequenceDim, optimization, iBatch) ||
      !cSeqToken.Add(neuron))
      DeleteObjAndFalse(neuron);
   neuron.SetActivationFunction(None);
   index++;
   CNeuronSequenceTokenizer* seq_tok = new CNeuronSequenceTokenizer();
   if(!seq_tok ||
      !seq_tok.Init(0, index, OpenCL, dimensions[0], units_s, embed_size, optimization, iBatch) ||
      !cSeqToken.Add(seq_tok))
      DeleteObjAndFalse(seq_tok);
   seq_tok.SetActivationFunction(SoftPlus);

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

//--- Recurrent
   CNeuronLSTMOCL* lstm = NULL;
   uint size = seq_tok.Neurons();
   for(uint i = 0; i < layers; i++)
     {
      lstm = new CNeuronLSTMOCL();
      index++;
      if(!lstm ||
         !lstm.Init(0, index, OpenCL, size, optimization, iBatch) ||
         !cRecurrent.Add(lstm))
         DeleteObjAndFalse(lstm);
      lstm.SetActivationFunction(TANH);
     }
   index++;
   CNeuronBatchNormOCL* norm = new CNeuronBatchNormOCL();
   if(!norm ||
      !norm.Init(0, index, OpenCL, lstm.Neurons(), optimization, iBatch) ||
      !cRecurrent.Add(norm))
      DeleteObjAndFalse(norm);

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

Совсем иначе устроена ветвь трансформера. Для работы INFNet-блока одного только токена последовательности недостаточно. Перед передачей в CNeuronINFNetBlock конкатенируем токен последовательности с ранее выделенным контекстом.

//--- Transformer
   index++;
   neuron = new CNeuronBaseOCL();
   if(!neuron ||
      !neuron.Init(0, index, OpenCL, iContextDim, optimization, iBatch) ||
      !cTransformer.Add(neuron))
      DeleteObjAndFalse(neuron);
   neuron.SetActivationFunction(None);
   index++;
   neuron = new CNeuronBaseOCL();
   if(!neuron ||
      !neuron.Init(0, index, OpenCL, iContextDim + seq_tok.Neurons(), optimization, iBatch) ||
      !cTransformer.Add(neuron))
      DeleteObjAndFalse(neuron);
   neuron.SetActivationFunction(None);

И уже это объединённое представление подаём в трансформерную магистраль.

   uint dims[];
   if(ArrayCopy(dims, dimensions, 0, 0, dimensions.Size()) < (int)dimensions.Size())
      ReturnFalse;
   dims[0] = seq_tok.Neurons();
   index++;
   CNeuronINFNetBlock* transf = new CNeuronINFNetBlock();
   if(!transf ||
      !transf.Init(0, index, OpenCL, dims, 1, heads, stack_size, layers, embed_size,
                                          candidates, topK, optimization, iBatch) ||
      !cTransformer.Add(transf))
      DeleteObjAndFalse(transf);

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

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

После того как обе ветви сформировали свои латентные представления, в работу включается cDifference. Он сравнивает уже не сырые признаки и не промежуточные фрагменты, а две завершённые интерпретации одного и того же состояния рынка.

   index++;
   if(!cDifference.Init(0, index, OpenCL, norm.Neurons(), embed_size, optimization, iBatch))
      ReturnFalse;
   cDifference.SetActivationFunction(TANH);
//---
   return true;
  }

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

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

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

bool CNeuronReGENTADBackbone::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      ReturnFalse;
//---
   CNeuronBaseOCL* sequence = cSeqToken[0];
   CNeuronBaseOCL* context = cTransformer[0];
   if(!sequence || !context)
      ReturnFalse;
   uint iSequenceDim = sequence.Neurons();
   uint iContextDim = context.Neurons();
   if(!DeConcat(sequence.getOutput(), context.getOutput(), NeuronOCL.getOutput(),
                iSequenceDim, iContextDim, 1))
      ReturnFalse;

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

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

//--- Sequence tokenizer
   for(int i = 1; i < cSeqToken.Total(); i++)
     {
      sequence = cSeqToken[i];
      if(!sequence ||
         !sequence.FeedForward(cSeqToken[i - 1]))
         ReturnFalse;
     }
   iSequenceDim = sequence.Neurons();

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

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

//--- Transformer
   context = cTransformer[1];
   if(!context ||
      !Concat(sequence.getOutput(), cTransformer[0].getOutput(), context.getOutput(),
              iSequenceDim, iContextDim, 1))
      ReturnFalse;

Только после этого объединённое представление передаётся дальше по слоям cTransformer, включая CNeuronINFNetBlock. Это полностью соответствует логике, заложенной в инициализации: INFNet анализирует состояние рынка в связке с контекстом, формируя сценарную интерпретацию.

   for(int i = 2; i < cTransformer.Total(); i++)
     {
      context = cTransformer[i];
      if(!context ||
         !context.FeedForward(cTransformer[i - 1]))
         ReturnFalse;
     }

Параллельно запускается рекуррентная ветвь. И здесь принципиально другое поведение: в неё подаётся чистый токен последовательности без контекста.

//--- Recurrent
   sequence = cRecurrent[0];
   if(!sequence ||
      !sequence.FeedForward(cSeqToken[-1]))
      ReturnFalse;
   for(int i = 1; i < cRecurrent.Total(); i++)
     {
      sequence = cRecurrent[i];
      if(!sequence ||
         !sequence.FeedForward(cRecurrent[i - 1]))
         ReturnFalse;
     }

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

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

   if(!cDifference.FeedForward(context, sequence.getOutput()))
      ReturnFalse;

Это уже не просто вычисление — это фиксация расхождения между двумя интерпретациями.

Финальный шаг — сборка итогового вектора. С помощью Concat объединяются три компонента:

  • результат магистрали трансформера (сценарная интерпретация),
  • результат рекуррентной ветви (последовательная динамика),
  • токен разности (оценка риска).
   if(!Concat(context.getOutput(), sequence.getOutput(), cDifference.getOutput(), Output,
              context.Neurons(), sequence.Neurons(), cDifference.Neurons(), 1))
      ReturnFalse;
//---
   return true;
  }

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

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

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

Именно в этой точке появляется то, чего обычно не хватает торговым системам — не просто прогноз, а встроенная проверка его надёжности.

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

bool CNeuronReGENTADBackbone::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      ReturnFalse;
//---
   CNeuronBaseOCL* sequence = cRecurrent[-1];
   CNeuronBaseOCL* context = cTransformer[-1];
   if(!sequence || !context)
      ReturnFalse;
   uint iSequenceDim = sequence.Neurons();
   uint iContextDim = context.Neurons();
   if(!DeConcat(context.getGradient(), sequence.getGradient(), cDifference.getGradient(), Gradient,
                iContextDim, iSequenceDim, cDifference.Neurons(), 1))
      ReturnFalse;
   Deactivation(cDifference);
   Deactivation(context);
   Deactivation(sequence);

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

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

   CBufferFloat* temp = context.getGradient();
   if(!context.SetGradient(context.getPrevOutput(), false) ||
      !context.getGradient().Fill(0) ||
      !sequence.getPrevOutput().Fill(0))
      ReturnFalse;
   if(!context.CalcHiddenGradients(cDifference.AsObject(),
                                   sequence.getOutput(),
                                   sequence.getPrevOutput(),
                                   (ENUM_ACTIVATION)sequence.Activation()))
      ReturnFalse;
   if(!SumAndNormalize(temp, context.getGradient(), temp, 1, false, 0, 0, 0, 1) ||
      !context.SetGradient(temp, false) ||
      !SumAndNormalize(sequence.getGradient(), sequence.getPrevOutput(), sequence.getGradient(),
                                                                          1, false, 0, 0, 0, 1))
      ReturnFalse;

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

//--- Recurrent
   for(int i = cRecurrent.Total() - 2; i >= 0; i--)
     {
      sequence = cRecurrent[i];
      if(!sequence ||
         !sequence.CalcHiddenGradients(cRecurrent[i + 1]))
         ReturnFalse;
     }

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

Точно так же назад разворачивается и ветвь трансформера.

//--- Transformer
   for(int i = cTransformer.Total() - 2; i >= 1; i--)
     {
      context = cTransformer[i];
      if(!context ||
         !context.CalcHiddenGradients(cTransformer[i + 1]))
         ReturnFalse;
     }
   context = cTransformer[0];
   sequence = cSeqToken[-1];
   if(!context || !sequence)
      ReturnFalse;
   iSequenceDim = sequence.Neurons();
   iContextDim = context.Neurons();
   if(!DeConcat(sequence.getPrevOutput(), context.getGradient(), cTransformer[1].getGradient(),
                iSequenceDim, iContextDim, 1))
      ReturnFalse;

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

//--- Sequence tokenizer
   if(!sequence.CalcHiddenGradients(cRecurrent[0]))
      ReturnFalse;
   if(sequence.Activation() != None)
      if(!DeActivation(sequence.getOutput(), sequence.getPrevOutput(),
                       sequence.getPrevOutput(), sequence.Activation()))
         ReturnFalse;
   if(!SumAndNormalize(sequence.getGradient(), sequence.getPrevOutput(), sequence.getGradient(), 1, false, 0, 0, 0, 1))
      ReturnFalse;
   for(int i = cSeqToken.Total() - 2; i >= 0; i--)
     {
      sequence = cSeqToken[i];
      if(!sequence ||
         !sequence.CalcHiddenGradients(cSeqToken[i + 1]))
         ReturnFalse;
     }
   iSequenceDim = sequence.Neurons();

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

   if(!Concat(sequence.getGradient(), context.getGradient(), NeuronOCL.getGradient(),
              iSequenceDim, iContextDim, 1))
      ReturnFalse;
   Deactivation(NeuronOCL);
//---
   return true;
  }

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

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



Заключение

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

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

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


Ссылки


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

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

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

Прикрепленные файлы |
MQL5.zip (3740.58 KB)
Событийная архитектура в MQL5: как превратить советник в полноценную торговую систему Событийная архитектура в MQL5: как превратить советник в полноценную торговую систему
Статья посвящена событийной архитектуре в MQL5 и описывает переход от монолитной модели OnTick к распределённой обработке. Разбираются предопределённые и пользовательские события, сервисы и обмен сообщениями между программами, а также типовые архитектурные ошибки. На практическом примере показано, как организовать взаимодействие индикаторов и советника, чтобы снизить нагрузку, повысить читаемость и упростить сопровождение.
Разработка инструментария для анализа Price Action (Часть 37): Индикатор смещения настроений Разработка инструментария для анализа Price Action (Часть 37): Индикатор смещения настроений
Рыночные настроения – одна из самых недооцененных, но при этом мощных сил, влияющих на движение цены. В то время как большинство трейдеров полагаются на запаздывающие индикаторы или догадки, советник Sentiment Tilt Meter (STM) преобразует рыночные данные в наглядный визуальный ориентир и в реальном времени показывает, склоняется ли рынок к бычьему или медвежьему сценарию либо остается нейтральным. Это упрощает подтверждение сделок, помогает избегать ложных входов и эффективнее выбирать момент входа в рынок.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Автоматизация греков Блэка-Шоулза: Расширенный скальпинг и микроструктурная торговля Автоматизация греков Блэка-Шоулза: Расширенный скальпинг и микроструктурная торговля
Гамма и Дельта изначально разрабатывались как инструменты управления рисками для хеджирования опционной экспозиции, но со временем они превратились в мощные инструменты для продвинутого скальпинга, моделирования потока ордеров и торговли на основе рыночной микроструктуры. Сегодня они служат индикаторами ценовой чувствительности и поведения ликвидности в режиме реального времени, позволяя трейдерам с удивительной точностью прогнозировать краткосрочную волатильность.