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

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

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

Введение

Рынок редко меняет направление по команде одного бара. Чаще он движется через накопление напряжения, смену плотности и постепенное смещение баланса между покупателями и продавцами. На графике это выглядит просто: свеча закрылась выше или ниже. Но внутри рыночного состояния уже мог измениться контекст. Объём перераспределился. Диапазон стал шире. Импульс потерял силу. Волатильность начала сжиматься. Цена ещё может стоять на месте, а сцена уже меняется.

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

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

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

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

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

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

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

На этом этапе у нас уже появился контур временной согласованности. CogDriverData формирует рыночную сцену. RankTCM удерживает и ранжирует память. Модель перестаёт быть полностью реактивной и получает возможность сопоставлять текущее состояние с прошлым контекстом. Однако этого всё ещё недостаточно для завершённого CogDriver-агента.

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

Именно к этому мы подходим в третьей статье. Следующий шаг — создание Forecast Head. На первый взгляд, это обычная прогнозная голова. Но в рамках CogDriver её роль шире. Мы не стремимся просто добавить ещё один блок, который предсказывает цену через несколько баров. Такая постановка слишком узкая. Цена — шумный и чувствительный показатель. Если заставить модель прогнозировать только будущий Close, она быстро окажется между двумя плохими вариантами: будет учить сглаженное среднее с запаздыванием или слишком остро реагировать на локальные выбросы.

Авторская визуализация CogDriver


Архитектура модуля прогнозирования

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

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

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

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

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

Здесь важно не переоценивать возможности такого подхода. Финансовый рынок не становится предсказуемым только потому, что мы добавили прогнозную голову. Будущее остаётся неопределённым. Любая прогнозная траектория может быть нарушена новостью, всплеском ликвидности или простой сменой поведения участников. Но задача Forecast Head не в том, чтобы устранить неопределённость. Её цель практичнее: заставить модель строить связное ожидание и наказывать внутренние представления, которые не проходят проверку ближайшим будущим.

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

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

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

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

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

Задачу практичнее реализовать стеком блоков Cross-Attention и Self-Attention. Cross-Attention согласует план с текущей рыночной ситуацией. Self-Attention делает прогноз согласованным на всём горизонте планирования. При этом рекуррентное уточнение плана требует особого внимания к организации механизмов внимания.

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

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

Полный отказ от позиционного кодирования приводит к другой проблеме. Без различия между шагами горизонта и при инициализации нулевыми запросами на холодном старте Cross- и Self-Attention фактически блокируются. Одинаковые запросы генерируют одинаковые ответы, и прогноз на всём горизонте становится плоским.

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

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

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


Построение объекта Forecast Head

После постановки задачи можно перейти к построению объекта Forecast Head. На этом этапе важно не потерять главную идею. Новый объект должен работать внутри контура CogDriver и поддерживать прогнозный план, который уточняется на каждом шаге среды. Модель не должна начинать прогнозирование заново на каждом баре. Она должна переносить ранее построенную гипотезу вперёд, сопоставлять её с новым состоянием рынка и корректировать только то, что перестало согласовываться с фактической динамикой.

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

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

class CNeuronCogDriverForecastHead : public CNeuronMLMHAttentionOCL
  {
protected:
   uint              iVariables;
   CNeuronBaseOCL    cPlanInput;
   CNeuronConvOCL    cContextProjection;
   CNeuronLearnabledPE cQueryPE;
   CLayer            cCrossAttention;
   CLayer            cCrossResidual;
   CLayer            cCrossFF1;
   CLayer            cCrossFF2;
   CLayer            cCrossFFOutput;
   CLayer            cSelfOutput;
   CNeuronBaseOCL    cErrorInput;
   CNeuronConvOCL    cErrorHead;
   CBufferFloat      cContextKV;
   CBufferFloat      cContextKVGradient;
   CBufferFloat      cShiftedConfidence;
   CBufferFloat      cDeltaMask;
   CBufferFloat      cPlanDelta;
   CBufferFloat      cPlanDeltaNorm;
   CBufferFloat      cConfidence;
   //---
   virtual bool      ShiftState(void);
   virtual bool      BuildContextKV(void);
   virtual bool      BuildContextProjectionGradient(void);
   virtual bool      BuildErrorInput(CNeuronBaseOCL *NeuronOCL);
   virtual bool      BuildConfidence(void);
   virtual bool      BuildDeltaNorm(void);
   virtual bool      CrossLayerForward(uint layer, CNeuronBaseOCL *plan, CNeuronBaseOCL *context);
   virtual bool      CrossLayerBackward(uint layer, CNeuronBaseOCL *plan, CNeuronBaseOCL *context,
                                                                                CBufferFloat *out_grad);
   virtual bool      CrossLayerUpdateWeights(uint layer, CNeuronBaseOCL *plan, CNeuronBaseOCL *context);
   virtual bool      SelfLayerForward(uint layer, CNeuronBaseOCL *NeuronOCL);
   virtual bool      SelfLayerBackward(uint layer, CNeuronBaseOCL *NeuronOCL, CBufferFloat *out_grad);
   virtual bool      SelfLayerUpdateWeights(uint layer, CNeuronBaseOCL *NeuronOCL);
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronCogDriverForecastHead(void) : iVariables(0) {};
                    ~CNeuronCogDriverForecastHead(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint dimension, uint horizon, uint variables, uint forecast_blocks,
                          uint heads, ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override const  {  return defNeuronCogDriverForecastHead;   }
   virtual CBufferFloat *GetExpectedError(void) { return cErrorHead.getOutput(); }
   virtual CBufferFloat *GetConfidence(void) { return GetPointer(cConfidence); }
   virtual CBufferFloat *GetPlanDeltaNorm(void) { return GetPointer(cPlanDeltaNorm); }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   virtual void      TrainMode(bool flag) override;
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual bool      Clear(void) override;
  };

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

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

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

Сдвиг плана имеет принципиальное значение. Если на предыдущем шаге модель построила прогноз на горизонты от t+1 до t+H, то после перехода среды на новый бар часть этого прогноза уже приблизилась к реальности. Прежний прогноз t+2 становится прогнозом t+1, прежний t+3 — прогнозом t+2 и так далее. Последний элемент горизонта освобождается для нового дальнего шага. Такой перенос позволяет модели сопровождать гипотезу во времени, а не создавать независимые предсказания на каждом баре.

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

bool CNeuronCogDriverForecastHead::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                       uint dimension, uint horizon, uint variables, uint forecast_blocks,
                                       uint heads, ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(dimension == 0 || horizon == 0 || variables == 0 || forecast_blocks == 0 || heads == 0 ||
      dimension % heads != 0)
      ReturnFalse;
   uint head_dim = dimension / heads;
   if(!CNeuronMLMHAttentionOCL::Init(numOutputs, myIndex, open_cl, dimension, head_dim,
                                     heads, horizon, forecast_blocks, optimization_type, batch))
      ReturnFalse;

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

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

   iVariables = variables;
   SetActivationFunction(None);

Это важно, поскольку Forecast Head работает с уже сформированными латентными представлениями и не должен искажать их дополнительной нелинейностью на уровне контейнера.

Следующий шаг — инициализация обёрток и проекций. Объект cPlanInput создаёт рабочее представление прогнозного плана. Его размер определяется как произведение числа признаков на длину горизонта (iUnits*iWindow).

//--- wrappers and trainable context projection
   if(!cPlanInput.Init(0, 0, OpenCL, iUnits * iWindow, optimization, iBatch))
      ReturnFalse;
   cPlanInput.SetActivationFunction(None);

Он не использует активацию, поскольку хранит состояние, а не преобразует его.

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

   if(!cContextProjection.Init(0, 1, OpenCL, iWindow, iWindow,
                               2 * iUnits * iWindow, iVariables,
                               optimization, iBatch))
      ReturnFalse;
   cContextProjection.SetActivationFunction(None);

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

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

   if(!cQueryPE.Init(0, 2, OpenCL, iUnits * iWindow, optimization, iBatch))
      ReturnFalse;
   cQueryPE.SetActivationFunction(None);

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

//--- forecast blocks
   cCrossAttention.Clear();
   cCrossResidual.Clear();
   cCrossFF1.Clear();
   cCrossFF2.Clear();
   cCrossFFOutput.Clear();
   cSelfOutput.Clear();
   cCrossAttention.SetOpenCL(OpenCL);
   cCrossResidual.SetOpenCL(OpenCL);
   cCrossFF1.SetOpenCL(OpenCL);
   cCrossFF2.SetOpenCL(OpenCL);
   cCrossFFOutput.SetOpenCL(OpenCL);
   cSelfOutput.SetOpenCL(OpenCL);

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

Сначала создаётся объект CCrossMHFlashAttention, реализующий многоголовое перекрёстное внимание. Он получает параметры окна, числа голов и размерностей, необходимых для сопоставления плана и контекста. После успешной инициализации объект добавляется в контейнер cCrossAttention.

   uint index = 3;
   uint plan_total = iUnits * iWindow;
   for(uint i = 0; i < iLayers; i++)
     {
      CCrossMHFlashAttention *cross = new CCrossMHFlashAttention();
      if(!cross ||
         !cross.Init(0, index++, OpenCL, iWindowKey, iHeads, false,
                     iUnits, iVariables * iUnits, optimization, iBatch) ||
         !cCrossAttention.Add(cross))
         DeleteObjAndFalse(cross);

Затем создаётся слой для остаточного соединения (cCrossResidual). Его размер соответствует полному размеру плана (plan_total), и он не использует активацию.

      CNeuronBaseOCL *holder = new CNeuronBaseOCL();
      if(!holder ||
         !holder.Init(0, index++, OpenCL, plan_total, optimization, iBatch) ||
         !cCrossResidual.Add(holder))
         DeleteObjAndFalse(holder);
      holder.SetActivationFunction(None);

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

      CNeuronConvOCL *ff1 = new CNeuronConvOCL();
      if(!ff1 ||
         !ff1.Init(0, index++, OpenCL, iWindow, iWindow, 4 * iWindow, iUnits,
                   optimization, iBatch) ||
         !cCrossFF1.Add(ff1))
         DeleteObjAndFalse(ff1);
      ff1.SetActivationFunction(GELU);
      CNeuronConvOCL *ff2 = new CNeuronConvOCL();
      if(!ff2 ||
         !ff2.Init(0, index++, OpenCL, 4 * iWindow, 4 * iWindow, iWindow, iUnits,
                   optimization, iBatch) ||
         !cCrossFF2.Add(ff2))
         DeleteObjAndFalse(ff2);
      ff2.SetActivationFunction(None);

Затем создаётся ещё один базовый слой для выхода Feed-Forward части (cCrossFFOutput) и аналогичный слой для хранения результата Self-Attention (cSelfOutput). Оба слоя имеют размер полного плана и не используют активацию.

      holder = new CNeuronBaseOCL();
      if(!holder ||
         !holder.Init(0, index++, OpenCL, plan_total, optimization, iBatch) ||
         !cCrossFFOutput.Add(holder))
         DeleteObjAndFalse(holder);
      holder.SetActivationFunction(None);
      holder = new CNeuronBaseOCL();
      if(!holder ||
         !holder.Init(0, index++, OpenCL, plan_total, optimization, iBatch) ||
         !cSelfOutput.Add(holder))
         DeleteObjAndFalse(holder);
      holder.SetActivationFunction(None);
     }

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

   uint error_width = (iVariables + 1) * iWindow;
   if(!cErrorInput.Init(0, index++, OpenCL, iUnits * error_width, optimization, iBatch))
      ReturnFalse;
   cErrorInput.SetActivationFunction(None);

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

   if(!cErrorHead.Init(0, index++, OpenCL, error_width, error_width, 1, iUnits,
                       optimization, iBatch))
      ReturnFalse;
   cErrorHead.SetActivationFunction(SoftPlus);

На следующем этапе инициализируются буферы состояния и диагностики. Буфер cContextKV хранит ключи и значения контекста, а cContextKVGradient — соответствующие градиенты. Их размер определяется как удвоенное произведение числа переменных, признаков и длины окна.

//--- state and diagnostics
   uint context_kv_total = 2 * iVariables * iUnits * iWindow;
   if(!cContextKV.BufferInit(context_kv_total, 0) || !cContextKV.BufferCreate(OpenCL))
      ReturnFalse;
   if(!cContextKVGradient.BufferInit(context_kv_total, 0) || !cContextKVGradient.BufferCreate(OpenCL))
      ReturnFalse;

Буфер cShiftedConfidence используется для переноса уверенности между шагами.

   if(!cShiftedConfidence.BufferInit(iUnits, 0) || !cShiftedConfidence.BufferCreate(OpenCL))
      ReturnFalse;

Буфер cDeltaMask хранит маску изменений плана.

   if(!cDeltaMask.BufferInit(iUnits, 0) || !cDeltaMask.BufferCreate(OpenCL))
      ReturnFalse;

Буфер cPlanDelta фиксирует разницу между предыдущим и текущим прогнозом. Буфер cPlanDeltaNorm содержит нормированную величину этого изменения.

   if(!cPlanDelta.BufferInit(plan_total, 0) || !cPlanDelta.BufferCreate(OpenCL))
      ReturnFalse;
   if(!cPlanDeltaNorm.BufferInit(iUnits, 0) || !cPlanDeltaNorm.BufferCreate(OpenCL))
      ReturnFalse;

Буфер cConfidence хранит текущую оценку уверенности модели.

   if(!cConfidence.BufferInit(iUnits, 0) || !cConfidence.BufferCreate(OpenCL))
      ReturnFalse;
//---
   return Clear();
  }

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

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

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

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

bool CNeuronCogDriverForecastHead::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!OpenCL || !NeuronOCL || NeuronOCL.Neurons() < int(iVariables * iWindow))
      ReturnFalse;

В начале выполняется проверка корректности входных данных и доступности OpenCL-контекста. Затем вызывается ShiftState, который переносит прогнозный план на следующий шаг и подготавливает его к уточнению.

//--- recurrent plan state is captured before Output is overwritten
   if(!ShiftState())
      ReturnFalse;

После этого входное латентное состояние проецируется в пространство контекста с помощью cContextProjection, и на его основе формируются ключи и значения через BuildContextKV.

//--- variables x dimension context -> trainable K/V tokens for variables * horizon
   if(!cContextProjection.FeedForward(NeuronOCL))
      ReturnFalse;
   if(!BuildContextKV())
      ReturnFalse;

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

//--- forecast transformer blocks
   CNeuronBaseOCL *plan = cPlanInput.AsObject();
   CNeuronBaseOCL *context = cContextProjection.AsObject();
   for(uint i = 0; i < iLayers; i++)
     {
      CNeuronBaseOCL *ff_out = cCrossFFOutput.At((int)i);
      if(!ff_out)
         ReturnFalse;
      if(!CrossLayerForward(i, plan, context))
         ReturnFalse;
      if(!SelfLayerForward(i, ff_out))
         ReturnFalse;
      if(i + 1 < iLayers)
         plan = cSelfOutput.At((int)i);
     }

Если блок не последний, обновлённый план берётся из cSelfOutput и передаётся на следующий уровень. Так план последовательно уточняется через стек Cross- и Self-блоков.

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

//--- side outputs
   if(!BuildErrorInput(NeuronOCL))
      ReturnFalse;
   if(!BuildConfidence())
      ReturnFalse;
   if(!BuildDeltaNorm())
      ReturnFalse;
//---
   return true;
  }

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

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



Заключение

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

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

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

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

Таким образом, Forecast Head становится промежуточным звеном между памятью и действием. Она не заменяет Actor и не превращает прогноз в торговый сигнал напрямую. Её задача тоньше: сделать внутреннее состояние CogDriver более прогнозным, связным и проверяемым будущими данными. Это должно снизить дрожание решений и дать модели более зрелую основу для обучения.

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


Ссылки


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

# Имя Тип Описание
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 (3831.97 KB)
Теория графов: Применение алгоритма поиска в ширину (BFS) в торговой системе Теория графов: Применение алгоритма поиска в ширину (BFS) в торговой системе
Поиск в ширину (BFS) использует обход по уровням для моделирования структуры рынка как ориентированного графа свингов цен, развивающихся во времени. Анализируя исторические бары или сессии слой за слоем, BFS отдает приоритет недавнему поведению цен, при этом учитывая более глубокую рыночную память.
Преодоление проблем доступности в торговых инструментах на MQL5 (Часть IV): Удаленная голосовая торговля Преодоление проблем доступности в торговых инструментах на MQL5 (Часть IV): Удаленная голосовая торговля
Узнайте о практическом способе выполнения сделок в MetaTrader 5 из голосовых сообщений Telegram с помощью промежуточного ПО на Python и советника MQL5, выступающего в качестве HTTP-клиента. В статье рассматриваются архитектура, опрос WebRequest, очереди в памяти, парсинг JSON с удалением нулевых символов-терминаторов и ограниченная грамматика команд с размером лота по умолчанию 0,001. Вы настроите среду и проверите задержку передачи данных, подходящую для мобильных интернет-соединений.
Встраивание торговой дисциплины в код (Часть 2): Разработка механизма принудительного соблюдения дневного лимита сделок на уровне всего счета в MQL5 Встраивание торговой дисциплины в код (Часть 2): Разработка механизма принудительного соблюдения дневного лимита сделок на уровне всего счета в MQL5
Мы разработали систему, которая устанавливает ежедневный лимит на количество сделок, чтобы вы соблюдали свои торговые правила. Она отслеживает все совершенные сделки на счете и автоматически вмешивается при достижении установленного лимита, предотвращая дальнейшую активность. Благодаря интеграции контроля непосредственно в платформу, система обеспечивает поддержание дисциплины даже при повышении рыночного давления.
Встраивание торговой дисциплины в код (Часть 1): Как создать системную дисциплину в реальной торговле с помощью MQL5 Встраивание торговой дисциплины в код (Часть 1): Как создать системную дисциплину в реальной торговле с помощью MQL5
Надежная дисциплина формируется тогда, когда создается посредством системного подхода, а не силы воли. В статье, используя MQL5, реализованы ограничения в реальном времени — лимиты частоты сделок и дневные ограничения по эквити, — которые отслеживают поведение и срабатывают при нарушении лимитов. Читатели получают практический шаблон для уровней управления, которые стабилизируют исполнение ордеров под рыночным давлением.