preview
Нейросети в трейдинге: Адаптация прогноза при смене рыночного режима (Объект верхнего уровня)

Нейросети в трейдинге: Адаптация прогноза при смене рыночного режима (Объект верхнего уровня)

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

Введение

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

В нашей модели эти уровни адаптации разделены. Основой служит архитектура ORION. Её энкодер формирует внутреннее описание рыночной ситуации, а последующие блоки строят сценарий и выбирают действие. Разработанный ранее D2Skill вмешивается в конце этой цепочки и точечно корректирует торговую политику. OMPB работает внутри энкодера и перестраивает признаки до того, как они превратятся в прогноз. Такое разделение важно, когда ошибка связана не с выбором Buy/Sell, а с неверной оценкой состояния рынка. Коррекция политики может изменить реакцию, но не исправляет признаки, которые привели сценарный блок к ошибочному ожиданию.

Такое размещение сохраняет принципы авторского фреймворка OMPB. Базовая модель не переучивается вслед за каждым новым наблюдением. Адаптация сосредоточена в небольшом байесовском модуле. Схема Predict-Then-Update сохраняет причинность: прогноз строится по доступной истории, а корректирующая часть обновляется только после появления фактического результата. В авторской работе поправка применяется к готовому прогнозу. Мы переносим тот же принцип внутрь энкодера и изменяем представление рынка перед ScenarioForecast.

В первой статье мы разобрали теоретическую схему OMPB и реализовали байесовский линейный слой CNeuronBayesianLinearOCL. Он формирует детерминированный рабочий выход по средним параметрам и создаёт случайные реализации posterior для оценки неопределённости. Во второй статье появились OpenCL-операции для Disagreement, Mismatch и KL-дивергенции. Эти компоненты уже выполняют необходимые расчёты, но пока работают раздельно. Объект верхнего уровня должен задать им общий порядок: сформировать поправку, сравнить Source и Target, подготовить ограничения и передать результат дальше по сети.

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


Задачи объекта верхнего уровня

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

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

= H + G ⊙ ΔH,

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

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

Сравнение должно описывать реакцию корректирующего распределения, а не геометрическое сходство свечей. Расстояние между ценами само по себе мало говорит об устойчивости нового преобразования. Для каждого сохранённого состояния получим несколько реализаций байесовской поправки. Их расхождение оценим отдельно на Source и Target, а разницу двух оценок выразим через Mismatch.

Mismatch = |DS(Q) − DT(Q)|.

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

Свободу адаптации также нужно ограничить. Несколько необычных баров способны заметно сдвинуть параметры гибкой модели, особенно при малом объёме Target. Низкая скорость обучения уменьшает отдельный шаг, но не контролирует накопленное отклонение. KL-дивергенция удерживает posterior возле prior, а отдельная опора ограничивает рост коэффициента затвора. Первое ограничение относится к форме байесовского распределения. Второе регулирует долю поправки, которая попадает в энкодер.

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

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

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


Реализация средствами MQL5

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

Байесовскую поправку формирует cResidual на базе CNeuronBayesianLinearOCL. Параметры затвора хранятся в cAlpha, а готовые коэффициенты — в cGate. Объекты cReference и cCurrent отвечают за Source и Target. Остальные буферы обслуживают случайные реализации, расстояния, градиенты и числовые проверки. С точки зрения торговой модели роли разделены ясно: одна ветвь предлагает изменение признаков, затвор определяет его долю, а две истории показывают поведение этой поправки на исходном и свежем рынке.

   CNeuronBayesianLinearOCL cResidual;
   CParams                  cAlpha;
   CNeuronBaseOCL           cGate;
   CNeuronAddToStack       *cReference;
   CNeuronAddToStack       *cCurrent;

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

Метод инициализации фиксирует геометрию дальнейших вычислений. Параметр samples задаёт число реализаций posterior. Reference_capacity определяет объём опорной выборки, а current_capacity — длину свежей истории. Эти значения влияют и на память, и на поведение калибровки. Короткий Target быстрее реагирует на смену режима, а длинный сильнее сглаживает одиночные выбросы. Фиксированная геометрия полезна и для OpenCL-реализации: размеры групп и буферов известны до первого прохода, а новые бары не требуют повторного выделения памяти.

bool CNeuronOMPBOCL::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint dimension, uint variables, uint samples,
                          uint reference_capacity, uint current_capacity,
                          ENUM_OPTIMIZATION optimization_type, uint batch)

Чтобы новый слой не изменил уже обученный энкодер сразу после создания, затвор инициализируем почти закрытым. В параметр cAlpha записывается −6, а сигмовидная функция переводит это значение в малый положительный коэффициент.

G₀ = 1 / (1 + e⁶) ≈ 0,00247.

Коэффициент 0,00247 пропускает около 0,247 % рассчитанной поправки. Фактический сдвиг признаков зависит от величины ΔH и не связан напрямую с процентным изменением цены, доходностью или риском позиции. Ненулевое значение сохраняет путь градиенту: корректирующая ветвь может начать обучение, хотя её влияние на энкодер остаётся минимальным.

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

   if(!cResidual.Init(0, 0, OpenCL, variables, dimension, dimension, samples,
                      optimization_type, batch))
      ReturnFalseEx("residual posterior");
   if(!cAlpha.Init(0, 0, OpenCL, variables, optimization_type, batch))
      ReturnFalseEx("alpha parameters");
//--- CParams stores a logical alpha as an input weight and a bias.
   CBufferFloat *alpha_weights = cAlpha.getWeightsParams();
   if(!alpha_weights)
      ReturnFalseEx("alpha weights are null");
   if(alpha_weights.Total() != int(2 * variables))
      ReturnFalseEx("alpha physical shape mismatch");
   if(!alpha_weights.Fill(0.0f))
      ReturnFalseEx("alpha posterior reset");
   if(!alpha_weights.BufferRead())
      ReturnFalseEx("read alpha weights");
   for(uint index = 0; index < variables; index++)
      if(!alpha_weights.Update(2 * index, -6.0f))
         ReturnFalseEx("initialize alpha logit");
   if(!alpha_weights.BufferWrite())
      ReturnFalseEx("write alpha weights");
   if(!cAlpha.FeedForward())
      ReturnFalseEx("alpha forward");

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

   if(!cReference.Init(0, 0, OpenCL, reference_capacity, dimension, variables,
                       optimization_type, batch))
      ReturnFalseEx("reference history");
   if(!cCurrent.Init(0, 0, OpenCL, current_capacity, dimension, variables,
                     optimization_type, batch))
      ReturnFalseEx("current history");

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

Параметр K задаёт количество наборов параметров одной и той же поправки. Сценарии будущей цены здесь не формируются. Для каждого состояния строится матрица расстояний K × K. Память и объём вычислений растут квадратично, а допустимое значение K ограничено 64. Все рабочие буферы создаются заранее. При обработке очередного бара объект использует уже выделенную память и оставляет основные операции на GPU.

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

Новый вход приходит из предыдущего слоя энкодера и опирается только на доступную историю. Прогнозная ошибка появится позже, когда станет известно фактическое продолжение движения. На этом этапе объект может передать признаки без изменений, сохранить их в Source или рассчитать текущую поправку. Конкретное действие задаёт режим, а не скрытая эвристика по волатильности или размеру последней свечи. Такой порядок сохраняет схему Predict-Then-Update внутри энкодера.

В начале метода прямого прохода сбрасывается bForwardAccepted. Флаг относится только к текущему входу, и результат предыдущего бара не может разрешить обновление после ошибки на новом. Режим OMPB_BYPASS вызывает Identity и копирует вход на выход. В OMPB_REFERENCE выполняется то же копирование, а свободная позиция Source дополнительно получает текущее состояние.

bool CNeuronOMPBOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
//--- Every forward invalidates older updates, including identity-only modes.
   bForwardAccepted = false;
   bNumericRejected = false;
   if(!NeuronOCL)
      ReturnFalseEx("source layer is null");
   if(NeuronOCL.Neurons() != int(iDimension * iVariables))
      ReturnFalseEx("source dimension mismatch");
   if(eMode == OMPB_BYPASS)
      return(Identity(NeuronOCL));
   if(eMode == OMPB_REFERENCE)
     {
      if(!Identity(NeuronOCL))
         ReturnFalseEx("reference identity output");
//--- Freeze the completed reference until an explicit ResetReference call.
      if(iReferenceCount < iReferenceCapacity)
        {
         if(!cReference.FeedForward(NeuronOCL.AsObject()))
            ReturnFalseEx("reference history update");
         iReferenceCount++;
        }
      return(true);
     }

После заполнения cReference перестаёт обновляться и сохраняет точку отсчёта. Допустим, несколько недель рынок двигался плавным трендом, а затем перешёл к резкому диапазону. Если Source продолжит следовать за новыми барами, он постепенно станет копией Target, и внутреннее расхождение уменьшится без восстановления качества модели. Опорный участок сохраняется до явного вызова ResetReference. Его содержательность определяется выбранными данными, а не самим механизмом фиксации.

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

Флаг bSourceAnchor задаёт отдельный опорный пример основной задачи и не совпадает с режимом накопления Source. Поправка для такого входа рассчитывается, но байесовские регуляризаторы временно отключаются, а Target не пополняется. Это позволяет сопоставлять вклад прогнозной ошибки на опорных и текущих примерах, не смешивая сами истории. Перед использованием cResidual и затвора проходят числовые проверки.

//--- Source anchors retain mean training while suppressing Bayesian regularizers.
   cResidual.SetRegularizersEnabled(!bSourceAnchor);
   bool rejected = false;
   if(!cResidual.FeedForwardChecked(NeuronOCL.AsObject(), GetPointer(cNumericFlags),
                                    GetPointer(cNumericOnes), GetPointer(cNumericScalar), rejected))
      ReturnFalseEx("checked residual posterior mean");
   if(rejected)
     {
      iInvalidFallbacks++;
      return(Identity(NeuronOCL));
     }
   if(!PrepareGateChecked(rejected))
      ReturnFalseEx("checked prepare gate");
   if(rejected)
     {
      iInvalidFallbacks++;
      return(Identity(NeuronOCL));
     }

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

Из прошедшей проверку поправки формируется кандидат H + ΔH. Он хранится отдельно, поскольку исходная ΔH понадобится в обратном проходе. Смешивание кандидата с исходным входом через затвор приводит к остаточной формуле, заданной в постановке задачи.

G ⊙ (H + ΔH) + (1 − G) ⊙ H = H + G ⊙ ΔH.

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

//--- Form H + delta without overwriting the posterior residual output.
   if(!SumAndNormalize(NeuronOCL.getOutput(), cResidual.getOutput(), &cCandidate,
                       iDimension, false, 0, 0, 0, 1.0f, GetPointer(cNumericFlags)))
      ReturnFalseEx("checked residual candidate");
   if(!ReduceNumericFlags(GetPointer(cNumericFlags), GetPointer(cNumericOnes),
                          GetPointer(cNumericScalar), iDimension * iVariables, rejected))
      ReturnFalseEx("candidate numeric reduction");
   if(rejected)
     {
      iInvalidFallbacks++;
      return(Identity(NeuronOCL));
     }
//--- Gate the residual candidate against the original input state.
   if(!GateElementMult(&cCandidate, NeuronOCL.getOutput(), cGate.getOutput(), getOutput(),
                       GetPointer(cNumericFlags)))
      ReturnFalseEx("checked gated output");
   if(!ReduceNumericFlags(GetPointer(cNumericFlags), GetPointer(cNumericOnes),
                          GetPointer(cNumericScalar), iDimension * iVariables, rejected))
      ReturnFalseEx("gated output numeric reduction");
   if(rejected)
     {
      iInvalidFallbacks++;
      return(Identity(NeuronOCL));
     }

Формально корректные числа ещё не гарантируют допустимую адаптацию. Posterior способен постепенно уйти далеко от prior и продолжать выдавать конечные значения. Метод CalculateKL собирает средние и логарифмы дисперсий весов и смещений, а ядро OMPBGaussianKL рассчитывает вклад каждого параметра. Сумма в m_fLastKL отражает отклонение распределения параметров коррекции от prior. Рыночные режимы в этом расчёте не сравниваются.

Порог m_fMaxKL отклоняет поправку, когда сумма превышает допустимое значение. Масштаб зависит от размера слоя, поскольку m_fLastKL хранит сумму, а не среднее. Один и тот же порог имеет разный смысл для posterior разной размерности. Нулевое значение отключает проверку. Градиент KL формируется внутри байесовского слоя, а верхний объект использует сумму ещё и как диагностический допуск.

После успешной проверки текущий вход добавляется в Target, и PrepareRegularizers готовит дополнительные воздействия. Параметры пока остаются неизменными. Флаг bForwardAccepted устанавливается лишь после завершения всей последовательности. Отклонённая поправка не может запустить обновление весов.

   if(!CalculateKL())
      ReturnFalseEx("KL calculation");
   if(bNumericRejected)
     {
      iInvalidFallbacks++;
      return(Identity(NeuronOCL));
     }
   if(m_fMaxKL > 0.0f && m_fLastKL > m_fMaxKL)
     {
      iKLRejects++;
      return(Identity(NeuronOCL));
     }
//--- Source anchor uses the mean Forecast path without mutating target history.
   if(eMode == OMPB_CALIBRATE && !bSourceAnchor)
     {
      if(!cCurrent.FeedForward(NeuronOCL.AsObject()))
         ReturnFalseEx("current history update");
      iCurrentCount = MathMin(iCurrentCount + 1, iCurrentCapacity);
     }
   if(!PrepareRegularizers())
      ReturnFalseEx("regularizer preparation");
   if(bNumericRejected)
     {
      iInvalidFallbacks++;
      return(Identity(NeuronOCL));
     }
//--- Finalize the residual output after history and regularizer handling.
   bForwardAccepted = true;
   return(true);
  }

Расчёт Mismatch начинается с очистки временного градиента затвора. PrepareRegularizers также проверяет режим, флаг обучения и заполненность обеих историй. Свободная ячейка буфера не представляет реальное состояние рынка, а нулевое заполнение исказило бы оценку. Сравнение запускается только после того, как Source и Target накопили заданное число входов.

bool CNeuronOMPBOCL::PrepareRegularizers(void)
  {
   m_fLastDisagreement = 0.0f;
   if(!cAlphaDisagreement.Fill(0.0f))
      ReturnFalseEx("clear alpha disagreement");
   if(eMode != OMPB_CALIBRATE || bSourceAnchor || !bTrain ||
      iReferenceCount < iReferenceCapacity || iCurrentCount < iCurrentCapacity)
      return(true);
   const float inv_tau2 = 1.0f / MathPow(MathAbs(m_fTau), 2);
//--- Build the source and target posterior distributions before disagreement.
   if(!CalculateDistribution(cReference, GetPointer(cReferenceHistoryTranspose),
                             GetPointer(cReferenceSamples),
                             GetPointer(cReferenceSamplesTranspose), iReferenceCapacity,
                             GetPointer(cReferenceDistance)))
      ReturnFalseEx("reference distribution");
   if(bNumericRejected)
      return(true);
   if(!CalculateDistribution(cCurrent, GetPointer(cCurrentHistoryTranspose),
                             GetPointer(cCurrentSamples),
                             GetPointer(cCurrentSamplesTranspose), iCurrentCapacity,
                             GetPointer(cCurrentDistance)))
      ReturnFalseEx("current distribution");
   if(bNumericRejected)
      return(true);

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

Исходная раскладка группирует результаты по реализациям, тогда как CalcDistance должен видеть рядом варианты одной группы и одного состояния. Перестановка осей приводит данные к нужному порядку. Нормирование каждой строки по собственному максимуму отключено: Source и Target должны оставаться в одной шкале, которую задаёт параметр τ.

//--- Transform fixed history into checked posterior samples and their distance layout.
   if(!history_transpose.FeedForward(history))
      ReturnFalseEx("history transpose");
   bool rejected = false;
   if(!cResidual.ForwardSamplesBatchChecked(history.getOutput(), raw_samples.getOutput(), capacity,
         GetPointer(cNumericFlags), GetPointer(cNumericOnes),
         GetPointer(cNumericScalar), rejected))
      ReturnFalseEx("checked posterior samples");
   if(rejected)
     {
      bNumericRejected = true;
      return(true);
     }
   if(!samples_transpose.FeedForward(raw_samples))
      ReturnFalseEx("samples transpose");
   if(!CalcDistance(samples_transpose.getOutput(), distance, iDimension, iSamples,
                    iVariables * capacity, false, GetPointer(cNumericFlags)))
      ReturnFalseEx("checked raw grouped distance");
   if(!ReduceNumericFlags(GetPointer(cNumericFlags), GetPointer(cNumericOnes),
                          GetPointer(cNumericScalar), iVariables * capacity * iSamples * iSamples,
                          rejected))
      ReturnFalseEx("distance numeric reduction");
   if(rejected)
      bNumericRejected = true;
   return(true);

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

H̃ᵥ(i)H̃ᵥ(j) = Gᵥ · (ΔHᵥ(i) − ΔHᵥ(j)).

Квадрат расстояния масштабируется через Gᵥ². Почти закрытый затвор слабо передаёт различия внутренних поправок в прогнозный тракт. При большем коэффициенте то же расхождение заметнее меняет выход. Ядро OMPBDisagreement учитывает эту связь и одновременно готовит воздействие Mismatch на параметры затвора. Разницу можно уменьшать как изменением posterior, так и изменением доли поправки.

   uint global_work_offset[2] = {0, 0};
   uint global_work_size[2] = {64, iVariables};
   uint local_work_size[2] = {64, 1};
   setBuffer(def_k_OMPBDisagreement, def_k_ompbd_reference, cReferenceDistance.GetIndex())
   setBuffer(def_k_OMPBDisagreement, def_k_ompbd_current, cCurrentDistance.GetIndex())
   setBuffer(def_k_OMPBDisagreement, def_k_ompbd_output, cDisagreement.GetIndex())
   setArgument(def_k_OMPBDisagreement, def_k_ompbd_source_states, (int)iReferenceCapacity)
   setArgument(def_k_OMPBDisagreement, def_k_ompbd_target_states, (int)iCurrentCapacity)
   setArgument(def_k_OMPBDisagreement, def_k_ompbd_samples, (int)iSamples)
   setArgument(def_k_OMPBDisagreement, def_k_ompbd_inv_tau2, inv_tau2)
   setBuffer(def_k_OMPBDisagreement, def_k_ompbd_gate, cGate.getOutputIndex())
   setBuffer(def_k_OMPBDisagreement, def_k_ompbd_alpha_gradient, cAlphaDisagreement.GetIndex())
   setArgument(def_k_OMPBDisagreement, def_k_ompbd_gate_stride, (int)iDimension)
   setArgument(def_k_OMPBDisagreement, def_k_ompbd_multiplier,
               -m_fDisagreementMult / (float)iVariables)
   setBuffer(def_k_OMPBDisagreement, def_k_ompbd_numeric_flags, cNumericFlags.GetIndex())
   setArgument(def_k_OMPBDisagreement, def_k_ompbd_flags_enabled, 1)
   kernelExecuteLoc(def_k_OMPBDisagreement, global_work_offset, global_work_size, local_work_size)

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

ΔDᵥ = DS(v) − DT(v),    Mismatch = (1 / V) · ∑ᵥ₌₁ⱽ |ΔDᵥ|.

Модуль не позволяет изменениям разных групп взаимно погаситься. При этом малая величина ещё не подтверждает хороший прогноз, поскольку почти закрытый затвор тоже сближает Source и Target на выходе. Градиент Mismatch служит дополнительным ограничением. Ядро OMPBDisagreementGrad рассчитывает воздействия для случайных поправок обеих выборок. Знаки различаются, поскольку Source и Target входят в ΔDᵥ с противоположными коэффициентами.

После обратного транспонирования градиенты возвращаются в раскладку байесовских реализаций и поступают в cResidual. Целевой набор (Target) проходит тот же путь со своими входами. Веса на этом этапе не меняются: байесовский слой накапливает воздействия для последующего объединения с сигналом основной задачи. Сравнение доступных входов готовится без будущих данных, а полезность коррекции будет оценена после появления фактического результата.

   if(!cReferenceSamples.CalcHiddenGradients(cReferenceSamplesTranspose.AsObject()))
      ReturnFalseEx("reference transpose backward");
   if(!cResidual.BackwardSamplesBatch(cReference.getOutput(), cReferenceSamples.getGradient(),
                                      iReferenceCapacity))
      ReturnFalseEx("reference posterior backward");

При численной проблеме прямой проход возвращает исходные признаки и оставляет bForwardAccepted ложным. Уже записанный вход Target сохраняется, но текущая поправка не участвует ни в выходе, ни в обновлении параметров. В журнале такое событие означает защитный отказ конкретного прохода. История объекта при этом не восстанавливается к предыдущему состоянию.

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

Метод calcInputGradients проверяет, участвовала ли коррекция в принятом прямом проходе. При обходе, накоплении Source и защитном отказе выход совпадал со входом. Градиент в этих случаях копируется напрямую. Байесовская ветвь не обучается по результату, в формировании которого её поправка не участвовала.

bool CNeuronOMPBOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      ReturnFalseEx("source layer is null");
   if(eMode == OMPB_BYPASS || eMode == OMPB_REFERENCE)
     {
      if(!CopyBufferRaw(getGradient(), NeuronOCL.getGradient(), iDimension * iVariables))
         ReturnFalseEx("identity gradient copy");
      return(true);
     }
//--- A rejected forward owns only the direct identity gradient path.
   if(!bForwardAccepted)
     {
      if(!CopyBufferRaw(getGradient(), NeuronOCL.getGradient(), iDimension * iVariables))
         ReturnFalseEx("rejected identity gradient copy");
      return(true);
     }
//--- Reset OMPB gradient buffers before propagating the gated residual path.
   if(!cInputGradient.Fill(0.0f))
      ReturnFalseEx("clear input gradient");
   if(!cResidual.getGradient().Fill(0.0f))
      ReturnFalseEx("clear residual gradient");
   if(!cAlpha.getGradient().Fill(0.0f))
      ReturnFalseEx("clear alpha gradient");
   if(!cAlphaGradient.Fill(0.0f))
      ReturnFalseEx("clear alpha feature gradient");

Для принятого прохода градиент распределяется по остаточной формуле. Обозначим входящий сигнал через r. Байесовская поправка получает Gr, и малый затвор ослабляет влияние основной задачи на cResidual. Градиент параметра затвора зависит и от направления ΔH. Полезное изменение создаёт сигнал к увеличению его доли, а усиление ошибки — к снижению G.

rΔH = Gr,    rₐᵥ = Gᵥ(1 − Gᵥ) · ∑d=1D ΔHᵥ,d · rᵥ,d.

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

//--- Differentiate the gate over candidate=H+delta and the direct H branch.
   if(!GateElementMultGrad(&cCandidate, cResidual.getGradient(),
                           NeuronOCL.getOutput(), &cInputGradient,
                           cGate.getOutput(), GetPointer(cAlphaGradient), getGradient(),
                           None, None, SIGMOID))
      ReturnFalseEx("gated gradient");
   if(!MatMul(GetPointer(cAlphaGradient), GetPointer(cAlphaOnes), cAlpha.getGradient(),
              1, iDimension, 1, iVariables, false))
      ReturnFalseEx("reduce alpha feature gradient");
//--- Merge the transient disagreement direction after forecast reduction.
   if(!SumAndNormalize(cAlpha.getGradient(), GetPointer(cAlphaDisagreement), cAlpha.getGradient(),
                       iVariables, false, 0, 0, 0, 1.0f))
      ReturnFalseEx("merge alpha disagreement");

Градиент к входу складывается из нескольких путей. Исходные признаки входят в кандидата H + ΔH и одновременно проходят по неизменённой ветви H. Эти части восстанавливают полный прямой вклад. К ним добавляется зависимость ΔH от входа, рассчитанная байесовским слоем. Потеря любого пути заставила бы предыдущие слои энкодера обучаться по иной формуле, чем та, что использовалась при прогнозе.

//--- Include candidate's H identity branch before the Bayesian backward pass.
   if(!SumAndNormalize(&cInputGradient, cResidual.getGradient(), &cInputGradient,
                       iDimension, false, 0, 0, 0, 1.0f))
      ReturnFalseEx("candidate input gradient");
   if(!NeuronOCL.CalcHiddenGradients(cResidual.AsObject()))
      ReturnFalseEx("residual gradient");
//--- Merge the direct residual identity branch after Bayesian affine propagation.
   if(!SumAndNormalize(&cInputGradient, NeuronOCL.getGradient(), NeuronOCL.getGradient(),
                       iDimension, false, 0, 0, 0, 1.0f))
      ReturnFalseEx("input gradient merge");
   if(!ApplyAlphaPrior())
      ReturnFalseEx("alpha prior");
//--- Finalize the calibrated residual gradient after alpha regularization.
   return(true);
  }

К этому моменту cResidual уже содержит воздействия Source и Target, а прогнозная ошибка дополняет их. Репараметризация распределяет сигнал между средними значениями и логарифмами дисперсий posterior. KL-регуляризация добавляется один раз внутри байесовского слоя. Случайные реализации не умножают её на число K. Увеличение K меняет качество оценки Disagreement, но не усиливает один и тот же штраф.

Затвор получает отдельную опору. Без неё несколько последних примеров могли бы слишком быстро увеличить долю поправки. Метод ApplyAlphaPrior притягивает параметры cAlpha к значению a₀. По умолчанию a₀ = −6 и соответствует почти закрытому затвору. Опора не блокирует адаптацию: прогнозная ошибка и Mismatch могут отвести параметр от неё, если общий сигнал сохраняет нужное направление.

Rᵅ = (λᵅ / 2) · ∑ᵥ₌₁ⱽ (aᵥa₀)².

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

bool CNeuronOMPBOCL::ApplyAlphaPrior(void)
  {
   m_fLastAlphaPrior = 0.0f;
   if(!bTrain || eMode != OMPB_CALIBRATE || bSourceAnchor || m_fAlphaMult <= 0.0f)
      return(true);
   CBufferFloat *gradient = cAlpha.getGradient();
   if(!gradient)
      ReturnFalseEx("alpha gradient is null");
//--- Form alpha-prior, reduce its squared norm, and retain only its scalar metric on host.
   if(!SumVecMatrix(GetPointer(cAlphaPriorOffset), cAlpha.getOutput(), GetPointer(cAlphaDelta),
                    1, 1, 0, 0, 0, 1.0f, iVariables))
      ReturnFalseEx("alpha prior delta");
   if(!MatMul(GetPointer(cAlphaDelta), GetPointer(cAlphaDelta), GetPointer(cAlphaPriorSum),
              1, iVariables, 1, 1, false))
      ReturnFalseEx("alpha prior squared sum");
   if(!cAlphaPriorSum.BufferRead())
      ReturnFalseEx("read alpha prior squared sum");
   if(!MathIsValidNumber(cAlphaPriorSum[0]))
      ReturnFalseEx("nonfinite alpha prior squared sum");
   m_fLastAlphaPrior = 0.5f * m_fAlphaMult * cAlphaPriorSum[0];

Оптимизатор библиотеки прибавляет переданный сигнал к параметрам. Для уменьшения штрафа используется отрицательная производная. В коде один и тот же вектор складывается сам с собой и умножается на −λᵅ/2. Полученное направление −λᵅ(aa₀) добавляется к сигналам прогноза и Mismatch, уже накопленным в градиенте затвора.

//--- The optimizer consumes ascent directions, so add -lambda*(alpha-prior).
   if(!SumAndNormalize(GetPointer(cAlphaDelta), GetPointer(cAlphaDelta), GetPointer(cAlphaDelta),
                       iVariables, false, 0, 0, 0, -0.5f * m_fAlphaMult))
      ReturnFalseEx("alpha prior gradient");
   if(!SumAndNormalize(gradient, GetPointer(cAlphaDelta), gradient,
                       iVariables, false, 0, 0, 0, 1.0f))
      ReturnFalseEx("merge alpha prior gradient");
   return(true);
  }

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

bool CNeuronOMPBOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
   if(!bForwardAccepted)
      return(true);
   if(!bTrain || eMode != OMPB_CALIBRATE || bSourceAnchor)
      return(true);
   if(!NeuronOCL)
      ReturnFalseEx("source layer is null");
   if(!cResidual.UpdateInputWeights(NeuronOCL.AsObject()))
      ReturnFalseEx("residual posterior");
   if(!cAlpha.UpdateInputWeights())
      ReturnFalseEx("alpha parameters");
   return(true);
  }

При разрешённом обновлении отдельно изменяются posterior байесовской поправки и параметры затвора. Source и Target остаются данными для сравнения и не превращаются в веса. Прогнозная ошибка задаёт полезное направление ΔH, Mismatch учитывает различие поведения на двух выборках, а KL вместе с опорой затвора сдерживает масштаб адаптации.

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

Визуализация объекта верхнего уровня



Заключение

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

Калибровочная часть получила завершённый порядок. Source хранит точку отсчёта, Target отражает свежий рынок, а один posterior обрабатывает обе истории. Mismatch показывает изменение его поведения, прогнозная ошибка связывает поправку с качеством основной задачи, а KL и опора затвора ограничивают свободу параметров. Внутренние метрики используются только вместе с целью прогнозирования.

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

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


Ссылки


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

# Имя Тип  Описание
1 NeuroNet_TimeSeries.mqh Библиотека классов Реализация CNeuronOMPBOCL
2 NeuroNet_Declarations.mqh Библиотека классов Объявления классов
3 NeuroNet_Definitions.mqh Библиотека классов Типы, режимы и индексы ядер
4 NeuroNet_Misc.mqh Библиотека классов Байесовский линейный слой
5 NeuroNet.cl Библиотека Вычислительные ядра OpenCL

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

Прикрепленные файлы |
MQL5.zip (4144.76 KB)
Эволюция городских советов — City Councils Evolution (CCE) Эволюция городских советов — City Councils Evolution (CCE)
Выполняем каноническую реализацию CCE в рамках C_AO, воспроизводим тесты и показываем ключевой дефект: аддитивный покоординатный оператор тянет решения к нулю и деградирует на смещенных диапазонах. Предлагаем простой контрольный тест со сдвигом области поиска и модификацию на основе разностей, применяемую по всем координатам. В результате — исходники MQL5, методика проверки и улучшенная версия алгоритма.
Алгоритм оптимизации на основе кровеносной системы — Circulatory System Based Optimization (CSBO) Алгоритм оптимизации на основе кровеносной системы — Circulatory System Based Optimization (CSBO)
CSBO — алгоритм, вдохновлённый кровеносной системой, рассматривается как разностно-ориентированный метод: венозное движение, систематический круг с кроссовером и лёгочная мутация. Приведена реализация в виде класса C_AO_CSBO для MQL5 с переводом в максимизацию, нормировкой шагов и двухфазными эпохами. Представлены итоги, исходники и практические настройки.
Машинное обучение и Data Science (Часть 47): Прогнозирование рынка с помощью модели DeepAR на Python Машинное обучение и Data Science (Часть 47): Прогнозирование рынка с помощью модели DeepAR на Python
В этой статье мы попробуем спрогнозировать движения рынка с помощью достаточно хорошей модели прогнозирования временных рядов под названием DeepAR. Модель представляет собой сочетание глубоких нейронных сетей и авторегрессионных свойств, характерных для таких моделей, как ARIMA и VAR (векторная авторегрессионная модель).
Репликация и моделирование рынка: Большой финал Репликация и моделирование рынка: Большой финал
Я знаю, что многие, возможно, думали, что я опубликую ещё несколько статей, чтобы объяснить другие аспекты этой системы. Недостающие элементы легко реализовать. Тем не менее, её разработка позволит вам проверить, насколько вы действительно готовы.