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

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

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

Введение

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

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

Калибровка не строится вокруг одной последней ошибки. Мартингальный сертификат PAC-Bayes связывает риск на исходной области Source, расхождение с текущей областью Target и цену изменения апостериорного распределения. Такой подход учитывает зависимость соседних наблюдений. Для финансового ряда это важно: ошибка на одном баре часто связана с ошибкой на следующем. Обновление выполняется по правилу Predict-Then-Update: сначала модель выдаёт прогноз, затем получает фактический результат и только после этого меняет состояние для будущих прогнозов.

В нашей реализации сохраняются фиксированный базовый прогноз, байесовская поправка, контроль расхождения и причинный порядок обработки данных. Меняется точка приложения коррекции. Авторская остаточная голова работает с конечным прогнозом, а наш слой OMPB изменяет латентное описание рынка внутри энкодера состояния рыночной среды. После исторической калибровки слой фиксируется. Поэтому Actor, Critic и банки D2Skill обучаются уже в стабильной системе координат.

В предыдущих статьях мы подготовили все необходимые компоненты. Сначала появился байесовский линейный слой. Затем были реализованы вычисления Disagreement, Mismatch и KL-дивергенции. После этого механизмы были собраны в класс CNeuronOMPBOCL с областями Reference и Current, остаточной ветвью и затвором. Теперь нужно встроить этот объект в рабочую модель и организовать его подготовку так, чтобы базовый энкодер не изменился во время калибровки.

В данной статье мы сначала определим место нового слоя в архитектуре. Затем разделим обучение энкодера на базовый и калибровочный этапы. После этого проследим формирование опорной области, проверку контрольной точки и итоговый тест на отложенном периоде. Подготовка торговой политики, Actor, Critic, банков D2Skill и показателя Utility уже рассмотрена в предыдущем проекте. Здесь этот контур используется как готовое продолжение новой прогнозной модели.


Интеграция OMPB в энкодер состояния рыночной среды

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

Для трейдера здесь важен не сам дополнительный блок, а характер его вмешательства. Допустим, значения индикаторов остаются в привычном диапазоне, но реакция цены на импульс стала слабее. Базовая сеть видит знакомые числа и продолжает использовать прежнее толкование. OMPB добавляет к выходу RankTCM небольшую остаточную поправку Delta H. Её вклад регулирует затвор g. Когда новый режим не подтверждён, затвор остаётся почти закрытым, и ScenarioForecast получает исходное состояние.

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

Архитектура создаётся в методе D2SkillCreateDescriptions. Базовая часть поступает из метода CreateStateDescriptions. Затем прежняя прогнозная оконечность заменяется последовательностью CogDriverData, RankTCM, CNeuronOMPBOCL и ScenarioForecast. Названия рабочих объектов сохраняют префикс D2Skill, поскольку они входят в общий учебный контур. На этом этапе банки навыков ещё не используются. Объект D2SkillMarket хранит энкодер, D2SkillTarget содержит целевую копию, а D2SkillForecast указывает на прогнозный слой.

//--- CreateBuffers already stores state feature-major as
//--- [BarDescr,HistoryBars], which is CogDriverData's required input layout.
if(!market.Delete(layer) || !market.Delete(layer - 1))
   ReturnFalse;
CLayerDescription *descr = new CLayerDescription();
if(!descr)
   ReturnFalse;
descr.type = defNeuronCogDriverData;
descr.window = HistoryBars;
descr.count = BarDescr;
  {
   uint units[] = {StackSize, StackSize, Quantiles};
   if(ArrayCopy(descr.units, units, 0, 0, units.Size()) < int(units.Size()))
      DeleteObjAndFalse(descr);
  }
descr.probability = 1.0f;
descr.activation = None;
descr.optimization = ADAM;
descr.batch = BatchSize;
if(!market.Add(descr))
   DeleteObjAndFalse(descr);
//---
descr = new CLayerDescription();
if(!descr)
   ReturnFalse;
descr.type = defNeuronCogDriverRankTCM;
descr.window = HistoryBars * (2 * Quantiles + 1);
descr.count = EmbeddingSize;
descr.variables = BarDescr;
  {
   uint units[] = {StackSize, NHeads};
   if(ArrayCopy(descr.units, units, 0, 0, units.Size()) < int(units.Size()))
      DeleteObjAndFalse(descr);
  }
descr.activation = None;
descr.optimization = ADAM;
descr.batch = BatchSize;
if(!market.Add(descr))
   DeleteObjAndFalse(descr);
//---
descr = new CLayerDescription();
if(!descr)
   ReturnFalse;
descr.type = defNeuronOMPBOCL;
descr.window = EmbeddingSize;
descr.count = BarDescr;
descr.layers = OMPBSamples;
  {
   uint units[] = {OMPBReferenceSize, OMPBCurrentWindow};
   if(ArrayCopy(descr.units, units, 0, 0, units.Size()) < int(units.Size()))
      DeleteObjAndFalse(descr);
  }
descr.activation = None;
descr.optimization = ADAM;
descr.batch = BatchSize;
if(!market.Add(descr))
   DeleteObjAndFalse(descr);
//---
descr = new CLayerDescription();
if(!descr)
   ReturnFalse;
descr.type = defNeuronScenarioForecast;
descr.count = NScenarios;
descr.variables = TopK;
descr.window_out = NForecast;
descr.window = EmbeddingSize;
descr.layers = BarDescr;
descr.activation = None;
descr.optimization = ADAM;
descr.batch = BatchSize;
if(!market.Add(descr))
   DeleteObjAndFalse(descr);

После сборки графа CogDriverData занимает позицию 3, RankTCM — позицию 4, OMPB — позицию 5, а ScenarioForecast — позицию 6. Эти индексы становятся частью контракта контрольной точки. При загрузке советник проверяет не только число слоёв, но и их типы. Модель со старой архитектурой не попадёт в процедуру калибровки случайно.

Форма основного тензора не меняется. Параметр window задаёт размер EmbeddingSize, count — число переменных BarDescr, а layers — число стохастических реализаций. В массиве units хранятся ёмкость Reference и размер окна Current. Для ScenarioForecast новый слой выглядит как обычный поставщик латентного состояния. Значит, остальную архитектуру перестраивать не требуется.

Простая вставка слоя решает только половину задачи. Если обучать RankTCM, OMPB и ScenarioForecast одновременно, система координат будет постоянно двигаться. Базовый энкодер ещё учится отделять импульс от шума, а адаптер уже пытается исправлять его промежуточные ошибки. На графике функции потерь такое смешение может выглядеть вполне благополучно, но смысл коррекции окажется неясным. Поэтому подготовка энкодера делится на два этапа.

На первом этапе OMPB уже находится в графе, но работает как прозрачный переход. Базовая модель обучает RankTCM и ScenarioForecast. На втором этапе загружается полученная контрольная точка, её параметры фиксируются, и право на обновление получает только OMPB. Оба этапа выполняет один советник StudyForecast.mq5. Благодаря этому адаптер видит реальные латентные состояния и реальный градиент прогнозной задачи, а не заранее выгруженный набор векторов.

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

enum ENUM_OMPB_STAGE
  {
   OMPB_STAGE_MARKET_ENCODER = 1,
   OMPB_STAGE_CALIBRATION,
   OMPB_STAGE_BASE_POLICY,
   OMPB_STAGE_D2SKILL,
   OMPB_STAGE_OPTIMIZATION,
   OMPB_STAGE_INFERENCE
  };

Этап советника не следует смешивать с режимом самого слоя. Этап определяет весь алгоритм запуска. Режим управляет поведением CNeuronOMPBOCL внутри выбранного алгоритма. Во время базового обучения используется OMPB_BYPASS. Калибровочный запуск последовательно включает OMPB_REFERENCE, OMPB_BYPASS, OMPB_CALIBRATE и OMPB_INFERENCE.

Параметры обоих этапов находятся в StudyForecast.mq5. Здесь задаются общий период обучения, границы Reference и Calibration, число эпох и веса регуляризаторов. Значения в исходном коде служат настройками по умолчанию. Рабочий эксперимент запускается с пресетами 01MarketEncoder.set и 02OMPB.set, поэтому фактические даты и коэффициенты всегда нужно сверять с файлом запуска и журналом.

input group                       "---- Forecast training ----"
input datetime                    InpStart                 = D'2024.01.01'; //Training period start
input datetime                    InpEnd                   = D'2026.01.01'; //Training period end
input int                         InpEpochs                = 100; //Training epochs
//---
input group                       "---- OMPB stage ----"
input ENUM_OMPB_STAGE             InpOMPBStage             = OMPB_STAGE_MARKET_ENCODER;
input bool                        InpOMPBSmoke             = false; //Stage smoke; never writes a production checkpoint
//--- Stage 02 uses these values for strict checkpoint preflight, causal
//--- partitioning, Reference collection and frozen BYPASS baselines.  The
//--- calibration/update transaction remains in the following block.
input group                       "---- OMPB calibration ----"
input uint                        InpOMPBEpochs            = 3; //Calibration epochs
input datetime                    InpOMPBReferenceStart    = D'2024.01.01';
input datetime                    InpOMPBReferenceEnd      = D'2024.10.01';
input datetime                    InpOMPBCalibrationStart  = D'2024.10.01';
input datetime                    InpOMPBCalibrationEnd    = D'2026.01.01';
input uint                        InpOMPBSourcePeriod      = 0;
input float                       InpOMPBTau               = 1.0f;
input float                       InpOMPBLambdaDis         = 0.0f;
input float                       InpOMPBLambdaKL          = 0.0f;
input float                       InpOMPBLambdaAlpha       = 0.0f;
input float                       InpOMPBAlphaPrior        = -6.0f;
input float                       InpOMPBMaxKL             = 0.0f;

Метод OnInit разделяет два сценария запуска. Для OMPB_STAGE_MARKET_ENCODER выполняется проверка параметров и вызывается CreateD2SkillForecastStudy. Для OMPB_STAGE_CALIBRATION проверяется число эпох и вызывается CreateOMPBStage02ReferenceStudy. Калибровка не является продолжением того же цикла. Она получает отдельную инициализацию и отдельный набор проверок.

int OnInit(void)
  {
   Start = InpStart;
   End = InpEnd;
   Epochs = InpEpochs;
   const bool smoke = (InpRecoverySmoke || InpOMPBSmoke);
   if(InpOMPBStage == OMPB_STAGE_CALIBRATION)
     {
      if(InpOMPBEpochs == 0)
         return(INIT_PARAMETERS_INCORRECT);
      if(!D2SkillConfigureForecastRun(smoke, InpRecoverySmokeBatches,
                                      InpRecoverySmokeAge) ||
         !CreateOMPBStage02ReferenceStudy(InpOMPBReferenceStart, InpOMPBReferenceEnd,
                                          InpOMPBCalibrationStart, InpOMPBCalibrationEnd,
                                          InpOMPBSourcePeriod, InpOMPBTau, InpOMPBLambdaDis,
                                          InpOMPBLambdaKL, InpOMPBLambdaAlpha, InpOMPBAlphaPrior,
                                          InpOMPBMaxKL, smoke, InpRecoverySmokeBatches, InpOMPBEpochs))
         return(INIT_PARAMETERS_INCORRECT);
      return(INIT_SUCCEEDED);
     }
   if(InpOMPBStage != OMPB_STAGE_MARKET_ENCODER)
     {
      PrintFormat("OMPB_STAGE_UNSUPPORTED stage=%d", InpOMPBStage);
      return(INIT_PARAMETERS_INCORRECT);
     }
   if(Start >= End || Epochs <= 0 ||
      !D2SkillConfigureForecastRun(smoke, InpRecoverySmokeBatches,
                                   InpRecoverySmokeAge))
      return(INIT_PARAMETERS_INCORRECT);
   return(CreateD2SkillForecastStudy() ? INIT_SUCCEEDED : INIT_FAILED);
  }

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

void OnChartEvent(const int id, const long &lparam, const double &dparam,
                  const string &sparam)
  {
   if(id == 1001)
     {
      if(InpOMPBStage == OMPB_STAGE_CALIBRATION)
         RunOMPBStage02ReferenceStudy();
      else
         TrainD2SkillForecast();
     }
  }

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

bool SetOMPBMode(const ENUM_OMPB_MODE mode)
  {
   //--- Clear source-anchor state before selecting the requested OMPB runtime mode.
   CNeuronOMPBOCL *layer = GetOMPB();
   //--- Finalize the mode change only for a validated OMPB layer.
   return(layer && layer.SetSourceAnchor(false) && layer.SetMode(mode));
  }
bool ConfigureOMPB(const ENUM_OMPB_MODE mode)
  {
   //--- Resolve the layer and install the requested OMPB runtime mode.
   CNeuronOMPBOCL *layer = GetOMPB();
   if(!layer || !SetOMPBMode(mode))
      ReturnFalse;
   //--- BYPASS must not retain a trainable OMPB layer during MarketEncoder training.
   if(mode == OMPB_BYPASS)
      layer.TrainMode(false);
   //--- Finalize the OMPB configuration after the runtime safeguards are applied.
   return(true);
  }

Для режима OMPB_BYPASS метод дополнительно выключает обучение слоя. В результате объект остаётся между RankTCM и ScenarioForecast, но передаёт сигнал без поправки. Его апостериорное распределение, затвор и внутренние истории не изменяются. Это состояние используется на всём первом этапе.

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

if(!D2SkillValidateShapes())
  {
   PrintFormat("%s init: shapes=FAIL", OMPB_LOG_PREFIX);
   ReturnFalse;
  }
//--- Stage 01 keeps OMPB a non-trainable identity bridge. Its posterior,
//--- alpha and both histories remain untouched while ScenarioForecast trains.
CNeuronOMPBOCL *ompb = GetOMPB();
if(!ompb || !ConfigureOMPB(OMPB_BYPASS))
   ReturnFalse;
ompb.TrainMode(false);
if(!D2SkillInitIndicators())
  {
   PrintFormat("%s init: indicators=FAIL error=%d", OMPB_LOG_PREFIX, GetLastError());
   ReturnFalse;
  }
PrintFormat("%s init: forecast=%s completed_epochs=%u batches=%I64u invalid=%I64u", OMPB_LOG_PREFIX,
            (resumed ? "RESUMED" : "NEW"), D2SkillCompletedEpochs, D2SkillBatches, D2SkillInvalidBatches);
D2SkillReady = true;
//---
if(!EventChartCustom(ChartID(), 1, 0, 0, "Init"))
  {
   PrintFormat("%s init: chart event=FAIL error=%d", OMPB_LOG_PREFIX, GetLastError());
   ReturnFalse;
  }
return(true);

Дальше работает уже знакомый цикл TrainD2SkillForecast. Он формирует рыночное состояние, строит прогнозные сценарии и распространяет ошибку назад. Обновляются базовый энкодер и ScenarioForecast. Слой OMPB только переносит данные между ними. После каждой эпохи метод D2SkillCheckOMPBBypassInvariant сравнивает размеры Reference и Current, значения Disagreement, KL и защитные счётчики. Любое изменение означает, что адаптер вмешался в базовое обучение, и такой запуск прекращается.

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

Калибровочный запуск обязан загрузить именно эту контрольную точку. Метод CreateOMPBStage02ReferenceStudy не создаёт случайную модель при ошибке загрузки. Он проверяет позиции RankTCM, OMPB и ScenarioForecast, а затем блокирует штатное обновление энкодера и целевой модели.

bool CreateOMPBStage02ReferenceStudy(const datetime reference_start,
                                     const datetime reference_end,
                                     const datetime calibration_start,
                                     const datetime calibration_end,
                                     const uint anchor_period,
                                     const float tau_value,
                                     const float lambda_dis,
                                     const float lambda_kl,
                                     const float lambda_alpha,
                                     const float alpha_prior,
                                     const float max_kl,
                                     const bool smoke,
                                     const uint smoke_limit,
                                     const uint calibration_epochs = 1)
  {
   if(calibration_epochs == 0)
      ReturnFalseEx("calibration epochs is zero");
   ExtOMPBCalibrationEpochs = (smoke ? 1 : calibration_epochs);
   ExtOMPBCalibrationEpoch = 0;
   PrintFormat("OMPB_STAGE02_EPOCH_CONFIG requested=%u effective=%u smoke=%s",
               calibration_epochs, ExtOMPBCalibrationEpochs,
               (smoke ? "true" : "false"));
   ResetLastError();
   D2SkillReady = false;
   D2SkillForecast = NULL;
   D2SkillCompletedEpochs = 0;
   D2SkillBatches = 0;
   D2SkillInvalidBatches = 0;
   if(!D2SkillLoadForecastTraining())
     {
      PrintFormat("OMPB_STAGE02_PREFLIGHT_FAIL reason=stage01_checkpoint error=%d",
                  GetLastError());
      ReturnFalse;
     }
   CNeuronBaseOCL *rank_tcm = D2SkillMarket.Layer(4);
   CNeuronBaseOCL *ompb_layer = D2SkillMarket.Layer(5);
   CNeuronBaseOCL *forecast_layer = D2SkillMarket.Layer(6);
   if(!D2SkillForecast || !rank_tcm || !ompb_layer || !forecast_layer ||
      rank_tcm.Type() != defNeuronCogDriverRankTCM ||
      ompb_layer.Type() != defNeuronOMPBOCL ||
      forecast_layer.Type() != defNeuronScenarioForecast ||
      !GetOMPB() || !D2SkillValidateShapes() || !D2SkillInitIndicators() ||
      !D2SkillMarket.SetWeightsUpdate(false) ||
      !D2SkillTarget.SetWeightsUpdate(false))
     {
      Print("OMPB_STAGE02_PREFLIGHT_FAIL reason=loaded_contract");
      ReturnFalse;
     }
   if(!OMPBStage02Configure(reference_start, reference_end,
                            calibration_start, calibration_end,
                            anchor_period, tau_value, lambda_dis,
                            lambda_kl, lambda_alpha, alpha_prior,
                            max_kl, smoke, smoke_limit))
      ReturnFalse;
   D2SkillTarget.TrainMode(false);
   D2SkillMarket.TrainMode(true);
   GetOMPB().TrainMode(false);
   D2SkillReady = true;
   if(!EventChartCustom(ChartID(), 1, 0, 0, "OMPB_STAGE02_INIT"))
     {
      PrintFormat("OMPB_STAGE02_PREFLIGHT_FAIL reason=chart_event error=%d",
                  GetLastError());
      D2SkillReady = false;
      ReturnFalse;
     }
   Print("OMPB_STAGE02_CHECKPOINT_PASS mode=load_only production_save=forbidden");
   return(true);
  }

Здесь важно различать методы TrainMode и SetWeightsUpdate. Первый сохраняет полный прямой и обратный проход. Второй запрещает запись новых весов. Градиент по-прежнему идёт от ошибки ScenarioForecast через OMPB к выходу RankTCM, но базовые параметры остаются неподвижными. Такая схема позволяет обучать адаптер внутри реальной модели и одновременно удерживать систему координат.

Отдельный пакет калибровки обрабатывает метод OMPBStage02RunBatch. Он включает режим OMPB_CALIBRATE, выполняет общий учебный проход D2SkillTrainBatch и лишь затем вызывает UpdateInputWeights для слоя OMPB. Это единственная явная операция изменения параметров на втором этапе.

bool OMPBStage02RunBatch(const int position, const bool source_anchor)
  {
   CNeuronOMPBOCL *ompb = GetOMPB();
   CNeuronBaseOCL *rank_tcm = GetRankTCM();
   if(!ompb || !rank_tcm || !ConfigureOMPB(OMPB_CALIBRATE) ||
      !ompb.SetSourceAnchor(source_anchor))
      ReturnFalse;
   ompb.TrainMode(true);
   if(!D2SkillTrainBatch(position))
      ReturnFalse;
//--- A source anchor has already created its mean Forecast gradient. Its
//--- forward/backward phase must retain the anchor flag so Current and both
//--- regularizers remain untouched. Clear only the update guard afterwards
//--- to apply that already-computed mean posterior gradient.
   if(source_anchor && !ompb.SetSourceAnchor(false))
      ReturnFalse;
   return(ompb.UpdateInputWeights(rank_tcm));
  }

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


Калибровка OMPB

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

Разделение относится именно к OMPB. Базовый прогнозный блок ранее обучался на более широком диапазоне, поэтому Calibration нельзя называть новым периодом для всей модели. Задача этого этапа уже. Мы сравниваем одну и ту же замороженную сеть до и после настройки адаптера. Полноценный период вне обучения появится позже, при торговом тесте.

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

Метод OMPBStage02Configure проверяет календарные границы и коэффициенты. Затем каждая область делится в пропорции 80/20. Рабочая часть Reference наполняет опорную память, а её остаток образует контрольный участок Source. Рабочая часть Calibration используется для обновлений, а последние 20% становятся контрольным участком Target.

bool OMPBStage02Configure(const datetime reference_start, const datetime reference_end,
                          const datetime calibration_start, const datetime calibration_end,
                          const uint anchor_period, const float tau_value, const float lambda_dis,
                          const float lambda_kl, const float lambda_alpha, const float alpha_prior,
                          const float max_kl, const bool smoke, const uint smoke_limit)
  {
   if(reference_start >= reference_end || reference_end > calibration_start ||
      calibration_start >= calibration_end || tau_value < 0.0f || tau_value > 1.0f ||
      lambda_dis < 0.0f || lambda_kl < 0.0f || lambda_alpha < 0.0f ||
      max_kl < 0.0f ||
      !OMPBStage02Finite(tau_value) || !OMPBStage02Finite(lambda_dis) ||
      !OMPBStage02Finite(lambda_kl) || !OMPBStage02Finite(lambda_alpha) ||
      !OMPBStage02Finite(alpha_prior) || !OMPBStage02Finite(max_kl))
     {
      Print("OMPB_STAGE02_PREFLIGHT_FAIL reason=input_contract");
      ReturnFalse;
     }
   const long reference_span = long(reference_end) - long(reference_start);
   const long calibration_span = long(calibration_end) - long(calibration_start);
   const datetime reference_split = reference_start + int(reference_span * 4 / 5);
   const datetime calibration_split = calibration_start + int(calibration_span * 4 / 5);
   if(reference_span <= 0 || calibration_span <= 0 || reference_split <= reference_start ||
      reference_split >= reference_end || calibration_split <= calibration_start ||
      calibration_split >= calibration_end)
     {
      Print("OMPB_STAGE02_PREFLIGHT_FAIL reason=empty_80_20_partition");
      ReturnFalse;
     }
   CNeuronOMPBOCL *ompb = GetOMPB();
   if(!ompb || !ompb.SetTau(tau_value) || !ompb.SetDisagreementMult(lambda_dis) ||
      !ompb.SetKLDMult(lambda_kl) || !ompb.SetAlphaMult(lambda_alpha) ||
      !ompb.SetAlphaPrior(alpha_prior) || !ompb.SetMaxKL(max_kl) ||
      !ConfigureOMPB(OMPB_BYPASS))
     {
      Print("OMPB_STAGE02_PREFLIGHT_FAIL reason=ompb_configuration");
      ReturnFalse;
     }
   ompb.TrainMode(false);
   OMPBStage02ReferenceStart = reference_start;
   OMPBStage02ReferenceSplit = reference_split;
   OMPBStage02ReferenceEnd = reference_end;
   OMPBStage02CalibrationStart = calibration_start;
   OMPBStage02CalibrationSplit = calibration_split;
   OMPBStage02CalibrationEnd = calibration_end;
   OMPBStage02Smoke = smoke;
   OMPBStage02SmokeLimit = (smoke ? MathMax(1, int(smoke_limit)) : 0);
   OMPBStage02SourcePeriod = anchor_period;
   PrintFormat("OMPB_STAGE02_PREFLIGHT_CONFIG reference=%s..%s split=%s calibration=%s..%s",
               TimeToString(reference_start, TIME_DATE), TimeToString(reference_end, TIME_DATE),
               TimeToString(reference_split, TIME_DATE), TimeToString(calibration_start, TIME_DATE),
               TimeToString(calibration_end, TIME_DATE));
   PrintFormat("OMPB_STAGE02_PREFLIGHT_CONFIG split=%s source_period=%u smoke=%s",
               TimeToString(calibration_split, TIME_DATE), anchor_period, (smoke ? "true" : "false"));
   PrintFormat("OMPB_STAGE02_OBJECTIVE tau=%.8g lambda_dis=%.8g lambda_kl=%.8g " +
               "lambda_alpha=%.8g alpha_prior=%.8g max_kl=%.8g",
               tau_value, lambda_dis, lambda_kl, lambda_alpha, alpha_prior, max_kl);
   return(true);
  }

Такое разбиение даёт четыре последовательных отрезка одной истории. Опорная память участвует в вычислении расхождения, но не используется для оценки качества. Контрольный участок Source показывает, сохранила ли модель поведение на знакомом режиме. Рабочий участок Calibration изменяет параметры адаптера. Контрольный участок Target проверяет, переносится ли результат на более поздние бары, которые не участвовали в обновлении.

В том же методе задаются порог , веса Disagreement, KL-дивергенции и штрафа затвора. Порог ограничивает вклад отдельных расстояний в оценку расхождения. Память Reference после заполнения остаётся неподвижной, а окно Current движется по позднему участку. До первого прохода слой сохраняет режим OMPB_BYPASS, поэтому одна настройка параметров ещё не меняет прогноз.

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

bool OMPBStage02PrepareData(void)
  {
   const int reference_start = iBarShift(Symb.Name(), TimeFrame, OMPBStage02ReferenceStart);
   const int reference_split = iBarShift(Symb.Name(), TimeFrame, OMPBStage02ReferenceSplit);
   const int reference_end = iBarShift(Symb.Name(), TimeFrame, OMPBStage02ReferenceEnd);
   const int calibration_start = iBarShift(Symb.Name(), TimeFrame, OMPBStage02CalibrationStart);
   const int calibration_split = iBarShift(Symb.Name(), TimeFrame, OMPBStage02CalibrationSplit);
   const int calibration_end = iBarShift(Symb.Name(), TimeFrame, OMPBStage02CalibrationEnd);
   if(reference_start <= 0 || reference_split <= 0 || reference_end <= 0 ||
      calibration_start <= 0 || calibration_split <= 0 || calibration_end <= 0 ||
      reference_start <= reference_split || reference_split <= reference_end ||
      reference_end < calibration_start || calibration_start <= calibration_split ||
      calibration_split <= calibration_end)
     {
      PrintFormat("OMPB_STAGE02_PREFLIGHT_FAIL reason=bar_order shifts=(%d,%d,%d,%d,%d,%d)",
                  reference_start, reference_split, reference_end, calibration_start,
                  calibration_split, calibration_end);
      ReturnFalse;
     }
   const int bars = CopyRates(Symb.Name(), TimeFrame, 0, reference_start, Rates);
   if(bars <= 0 || !RSI.BufferResize(bars) || !CCI.BufferResize(bars) ||
      !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
     {
      PrintFormat("OMPB_STAGE02_PREFLIGHT_FAIL reason=rates bars=%d error=%d", bars, GetLastError());
      ReturnFalse;
     }
   int wait = -1;
   bool calculated = false;
   do
     {
      calculated = (RSI.BarsCalculated() >= bars && CCI.BarsCalculated() >= bars &&
                    ATR.BarsCalculated() >= bars && MACD.BarsCalculated() >= bars);
      Sleep(100);
      wait++;
     }
   while(!calculated && wait < 100);
   if(!calculated)
     {
      PrintFormat("OMPB_STAGE02_PREFLIGHT_FAIL reason=indicators bars=%d error=%d", bars, GetLastError());
      ReturnFalse;
     }
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();
   if(!ArraySetAsSeries(Rates, true))
     {
      PrintFormat("OMPB_STAGE02_PREFLIGHT_FAIL reason=rates_series error=%d", GetLastError());
      ReturnFalse;
     }
   OMPBStage02ReferenceFirst = reference_start - HistoryBars - NForecast - 1;
   OMPBStage02ReferenceLast = reference_split;
   OMPBStage02SourceEvalFirst = reference_split - 1;
   OMPBStage02SourceEvalLast = reference_end;
   OMPBStage02TargetCalibrationFirst = calibration_start - HistoryBars - NForecast - 1;
   OMPBStage02TargetCalibrationLast = calibration_split;
   OMPBStage02TargetEvalFirst = calibration_split - 1;
   OMPBStage02TargetEvalLast = calibration_end;
   const int reference_rows = OMPBStage02ReferenceFirst - OMPBStage02ReferenceLast + 1;
   const int source_eval_rows = OMPBStage02SourceEvalFirst - OMPBStage02SourceEvalLast + 1;
   const int target_calibration_rows = OMPBStage02TargetCalibrationFirst - OMPBStage02TargetCalibrationLast + 1;
   const int target_eval_rows = OMPBStage02TargetEvalFirst - OMPBStage02TargetEvalLast + 1;
   if(OMPBStage02ReferenceFirst < OMPBStage02ReferenceLast ||
      OMPBStage02SourceEvalFirst < OMPBStage02SourceEvalLast ||
      OMPBStage02TargetCalibrationFirst < OMPBStage02TargetCalibrationLast ||
      OMPBStage02TargetEvalFirst < OMPBStage02TargetEvalLast ||
      reference_rows < int(OMPBReferenceSize) || source_eval_rows <= 0 ||
      target_calibration_rows < int(OMPBCurrentWindow) || target_eval_rows <= 0)
     {
      PrintFormat("OMPB_STAGE02_PREFLIGHT_FAIL reason=bars ref=%d source=%d calibration=%d target=%d",
                  reference_rows, source_eval_rows, target_calibration_rows, target_eval_rows);
      PrintFormat("OMPB_STAGE02_PREFLIGHT_REQUIRED reference=%u target_calibration=%u",
                  OMPBReferenceSize, OMPBCurrentWindow);
      ReturnFalse;
     }
   PrintFormat("OMPB_STAGE02_PREFLIGHT_PASS bars=%d reference=%d source_eval=%d target_calibration=%d target_eval=%d",
               bars, reference_rows, source_eval_rows, target_calibration_rows, target_eval_rows);
   return(true);
  }

Перед запуском проверяется и размер каждого участка. Опорная память должна получить 256 состояний, а рабочая часть Calibration — не менее 64. Контрольные интервалы обязаны оставаться непустыми. Эти требования поддерживают сопоставимость запусков. Если сегодня Reference заполнена полностью, а завтра содержит лишь несколько десятков состояний, изменение Disagreement будет связано не только с рынком, но и с другой статистической основой.

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

bool OMPBStage02CollectReference(void)
  {
   CNeuronOMPBOCL *ompb = GetOMPB();
   if(!ompb || !ResetOMPBReference() || !ConfigureOMPB(OMPB_REFERENCE))
     {
      Print("OMPB_STAGE02_REFERENCE_FAIL reason=setup");
      ReturnFalse;
     }
   ompb.TrainMode(false);
   uint collected = 0;
   const uint rows = uint(OMPBStage02ReferenceFirst - OMPBStage02ReferenceLast + 1);
   const uint quota = OMPBStage02ProgressQuota(rows, OMPBReferenceSize, OMPBStage02Smoke,
                                                OMPBStage02SmokeLimit);
   OMPBStage02ShowProgress("reference", collected, quota, collected, 0, false, false, false,
                           false,
                           0, 0, 0, "");
   for(int position = OMPBStage02ReferenceFirst;
       position >= OMPBStage02ReferenceLast && !IsStopped(); position--)
     {
      if(!CreateBuffers(position + NForecast, GetPointer(D2SkillState),
                        GetPointer(D2SkillTime), NULL) ||
         !D2SkillMarket.feedForward(GetPointer(D2SkillState), 1, false, (CBufferFloat *)NULL))
        {
         PrintFormat("OMPB_STAGE02_REFERENCE_FAIL reason=forward position=%d line=%d", position, __LINE__);
         ReturnFalse;
        }
      collected++;
      OMPBStage02ShowProgress("reference", collected, quota, collected, 0, false, false, false,
                              false,
                              0, 0, 0, "");
      if(OMPBStage02Smoke && collected >= OMPBStage02SmokeLimit)
         break;
      if(ompb.ReferenceCount() >= OMPBReferenceSize)
         break;
     }
   if(collected == 0 || (!OMPBStage02Smoke && ompb.ReferenceCount() != OMPBReferenceSize))
     {
      PrintFormat("OMPB_STAGE02_REFERENCE_FAIL reason=count collected=%u reference=%u required=%u",
                  collected, ompb.ReferenceCount(), OMPBReferenceSize);
      OMPBStage02ShowProgress("reference", collected, quota, collected, 0, true, false, false,
                              false,
                              0, 0, 0, "result=failed");
      ReturnFalse;
     }
   OMPBStage02ShowProgress("reference", collected, quota, collected, 0, true, true, false,
                           false,
                           0, 0, 0, "");
   PrintFormat("OMPB_STAGE02_REFERENCE_PASS collected=%u reference=%u capacity=%u smoke=%s",
               collected, ompb.ReferenceCount(), OMPBReferenceSize, (OMPBStage02Smoke ? "true" : "false"));
   return(ConfigureOMPB(OMPB_BYPASS));
  }

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

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

void RunOMPBStage02ReferenceStudy(void)
  {
   OMPBStage02ShowProgress("preparing", 0, 1, 0, 0, false, false, false, false,
                           0, 0, 0, "");
   if(!D2SkillReady || !OMPBStage02PrepareData() || !OMPBStage02CollectReference())
     {
      OMPBStage02ShowProgress("preparing", 0, 1, 0, 0, true, false, false, false,
                              0, 0, 0,
                              "result=failed");
      ExpertRemove();
      return;
     }
   if(!D2SkillForecast.SetCodebookUpdate(false))
     {
      Print("OMPB_STAGE02_REFERENCE_FAIL reason=codebook_freeze");
      ExpertRemove();
      return;
     }
   if(!D2SkillCaptureFrozenForecastBaseline())
     {
      Print("OMPB_STAGE02_REFERENCE_FAIL reason=frozen_baseline");
      ExpertRemove();
      return;
     }
   double source_loss = 0.0, target_loss = 0.0;
   uint source_valid, source_invalid, target_valid, target_invalid;
   const bool source_ok = OMPBStage02Baseline(OMPBStage02SourceEvalFirst,
                          OMPBStage02SourceEvalLast, "source_eval",
                          source_loss, source_valid, source_invalid);
   const bool target_ok = (source_ok &&
                           OMPBStage02Baseline(OMPBStage02TargetEvalFirst,
                                 OMPBStage02TargetEvalLast, "target_eval",
                                 target_loss, target_valid, target_invalid));
   bool calibration_ok = false;
   bool evaluation_ok = false;
   bool acceptance_ok = false;
   bool checkpoint_saved = false;
   double source_inference_loss = 0.0, target_inference_loss = 0.0;
   uint source_inference_valid = 0, source_inference_invalid = 0;
   uint target_inference_valid = 0, target_inference_invalid = 0;
   if(target_ok && D2SkillVerifyFrozenForecastExact() && OMPBStage02CaptureSignatures())
     {
      PrintFormat("OMPB_STAGE02_REFERENCE_READY source_loss=%.9f target_loss=%.9f source_valid=%u target_valid=%u",
                  source_loss, target_loss, source_valid, target_valid);
      calibration_ok = OMPBStage02Calibrate();
     }
   else
      Print("OMPB_STAGE02_REFERENCE_FAIL reason=baseline_frozen_or_signatures");

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

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

CNeuronOMPBOCL *ompb = GetOMPB();
if(!ompb || !D2SkillForecast ||
   !D2SkillMarket.SetWeightsUpdate(false) ||
   !D2SkillTarget.SetWeightsUpdate(false) ||
   !D2SkillForecast.SetCodebookUpdate(false) ||
   !D2SkillMarket.Clear() ||
   !D2SkillTarget.Clear() ||
   !D2SkillForecast.ResetEpochDiagnostics() ||
   !ConfigureOMPB(OMPB_CALIBRATE))
  {
   Print("OMPB_STAGE02_CALIBRATION_FAIL reason=setup");
   ReturnFalse;
  }
D2SkillMarket.TrainMode(true);
D2SkillTarget.TrainMode(false);
ompb.TrainMode(true);

Основной цикл проходит рабочую часть Calibration строго по времени. Текущее состояние попадает в окно Current, затем рассчитываются Disagreement, KL-дивергенция и штраф затвора. После заданного числа успешных обновлений модель возвращается к рабочей части Reference и выполняет опорный проход Source. Он формирует градиент на прежнем режиме, но не меняет внутренние окна.

for(int position = OMPBStage02TargetCalibrationFirst;
    position >= OMPBStage02TargetCalibrationLast && !IsStopped(); position--)
  {
   target_attempts++;
   if(!OMPBStage02RunBatch(position, false))
     {
      target_invalid++;
     }
   else
     {
      uint reference_count, current_count, invalid_fallbacks, kl_rejects;
      float disagreement, kl, alpha_prior;
      if(!ReadOMPBDiagnostics(reference_count, current_count, disagreement, kl,
                              alpha_prior, invalid_fallbacks, kl_rejects) ||
         !OMPBStage02Finite(disagreement) || !OMPBStage02Finite(kl) ||
         !OMPBStage02Finite(alpha_prior))
        {
         PrintFormat("OMPB_STAGE02_CALIBRATION_FAIL reason=target_diagnostics position=%d", position);
         ReturnFalse;
        }
      target_updates++;
      if(OMPBStage02SourcePeriod > 0 && target_updates % OMPBStage02SourcePeriod == 0)
        {
         const int source_position = OMPBStage02ReferenceFirst - int(source_updates % source_rows);
         uint reference_before, current_before, invalid_before, kl_rejects_before;
         float disagreement_before, kl_before, alpha_before;
         if(!ReadOMPBDiagnostics(reference_before, current_before, disagreement_before, kl_before,
                                 alpha_before, invalid_before, kl_rejects_before))
           {
            Print("OMPB_STAGE02_CALIBRATION_FAIL reason=anchor_diagnostics_before");
            ReturnFalse;
           }
         source_attempts++;
         if(!OMPBStage02RunBatch(source_position, true))
           {
            source_invalid++;
           }
         else
           {
            uint reference_after, current_after, invalid_after, kl_rejects_after;
            float disagreement_after, kl_after, alpha_after;
            if(!ReadOMPBDiagnostics(reference_after, current_after, disagreement_after, kl_after,
                                    alpha_after, invalid_after, kl_rejects_after) ||
               current_after != current_before || reference_after != reference_before ||
               !OMPBStage02Finite(disagreement_after) || !OMPBStage02Finite(kl_after) ||
               !OMPBStage02Finite(alpha_after))
              {
               PrintFormat("OMPB_STAGE02_CALIBRATION_FAIL reason=anchor_contract position=%d", source_position);
               ReturnFalse;
              }
            source_updates++;
           }
        }
     }
   OMPBStage02ShowProgress("calibration", target_attempts, target_quota, target_attempts,
                           target_invalid, false, false, true, true, source_updates, source_attempts,
                           source_invalid, StringFormat("target_updates=%u", target_updates));
   if(OMPBStage02Smoke && target_attempts >= OMPBStage02SmokeLimit)
      break;
  }

Такое чередование имеет прямой рыночный смысл. Поздний участок может содержать короткий всплеск волатильности или одно сильное направленное движение. Без опоры адаптер легко примет временный эпизод за новый постоянный режим. Проход Source напоминает ему о прежнем поведении и сдерживает одностороннюю подгонку. Код сравнивает размеры Reference и Current до и после опорного пакета. Любое изменение памяти означает нарушение контракта и останавливает калибровку.

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

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

bool OMPBStage02Calibrate(void)
  {
   //--- Clear in the epoch setup retains posterior, alpha and optimizer state.
   for(uint epoch = 0; epoch < ExtOMPBCalibrationEpochs; epoch++)
     {
      if(IsStopped())
         return(false);
      ExtOMPBCalibrationEpoch = epoch + 1;
      PrintFormat("OMPB_STAGE02_EPOCH_BEGIN epoch=%u epochs=%u",
                  ExtOMPBCalibrationEpoch, ExtOMPBCalibrationEpochs);
      OMPBStage02ShowProgress("epoch setup", 0, 1, 0, 0, false, false, false, false,
                              0, 0, 0, StringFormat("epoch=%u/%u", ExtOMPBCalibrationEpoch,
                                                    ExtOMPBCalibrationEpochs));
      if(!OMPBStage02CalibrateEpoch())
         return(false);
     }
   //--- Only a complete run may proceed to held-out evaluation and publication.
   return(!IsStopped() && ExtOMPBCalibrationEpoch == ExtOMPBCalibrationEpochs);
  }

После последней эпохи слой переводится в OMPB_INFERENCE. Рабочая поправка теперь строится по средним параметрам апостериорного распределения и становится детерминированной. На контрольных участках Source и Target снова рассчитывается ошибка, а сохранённые отпечатки проверяются повторно.

if(calibration_ok && ConfigureOMPB(OMPB_INFERENCE))
  {
   const bool source_inference_ok =
      OMPBStage02InferenceEvaluation(OMPBStage02SourceEvalFirst, OMPBStage02SourceEvalLast,
                                     "source_inference", source_inference_loss,
                                     source_inference_valid, source_inference_invalid);
   const bool target_inference_ok =
      (source_inference_ok &&
       OMPBStage02InferenceEvaluation(OMPBStage02TargetEvalFirst, OMPBStage02TargetEvalLast,
                                      "target_inference", target_inference_loss,
                                      target_inference_valid, target_inference_invalid));
   evaluation_ok = (target_inference_ok && OMPBStage02VerifySignatures());
   if(evaluation_ok)
      acceptance_ok = OMPBStage02Accept(source_loss, target_loss, source_inference_loss,
                                        target_inference_loss, source_inference_valid,
                                        target_inference_valid);
   else
      Print("OMPB_STAGE02_ACCEPTANCE_FAIL reason=evaluation_or_signatures");
  }
else
   Print("OMPB_STAGE02_ACCEPTANCE_FAIL reason=calibration");
//--- Switching to deterministic inference and clearing OMPB drops only Current
//--- and transient buffers.  Clear deliberately preserves posterior parameters.
CNeuronOMPBOCL *ompb = GetOMPB();
const bool finalized = (ompb && ConfigureOMPB(OMPB_INFERENCE) && ompb.Clear());
if(!finalized)
   Print("OMPB_STAGE02_CALIBRATION_FAIL reason=finalize");
else
  {
   ompb.TrainMode(false);
//--- Quality acceptance is diagnostic; unchanged frozen parameters remain mandatory.
   const bool checkpoint_integrity = OMPBStage02VerifySignatures();
   OMPBStage02ShowProgress("saving", 0, 1, 0, 0, false, false, false, false, 0, 0, 0,
                           StringFormat("accepted=%s smoke=%s",
                                        (acceptance_ok ? "true" : "false"),
                                        (OMPBStage02Smoke ? "true" : "false")));
   if(checkpoint_integrity && !OMPBStage02Smoke)
      checkpoint_saved = OMPBStage02SaveAcceptedCheckpoint();
   else
      if(OMPBStage02Smoke)
         PrintFormat("OMPB_STAGE02_TRANSACTION_SKIPPED reason=smoke accepted=%s",
                     (acceptance_ok ? "true" : "false"));
      else
         Print("OMPB_STAGE02_TRANSACTION_SKIPPED reason=integrity");

Метод OMPBStage02Accept рассчитывает показатели TargetImprovement и SourceDegradation. Первый отражает относительное снижение ошибки на позднем контрольном участке. Второй показывает, какой ценой это улучшение получено на опорном режиме. Порог качества служит диагностикой. Он помогает сравнивать эксперименты, но не заменяет проверку целостности графа.

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

bool OMPBStage02SaveAcceptedCheckpoint(void)
  {
   ulong market = 0, target = 0, posterior = ulong(1469598103934665603);
   CNeuronOMPBOCL *ompb = GetOMPB();
   OMPBStage02ReloadProofPassed = false;
   OMPBStage02PreviousProofReady = false;
   OMPBStage02SelectorActivated = false;
   if(!ompb || !D2SkillMarket.ParameterFingerprint(market) ||
      !D2SkillTarget.ParameterFingerprint(target) || !ompb.AppendParameterFingerprint(posterior) ||
      !OMPBStage02StageCandidate() || !OMPBStage02PublishCandidate())
      return(false);
   if(!OMPBStage02ReloadSmoke(market, target, posterior) || !OMPBStage02FinalizeSteadySelector())
     {
      const bool restored = OMPBStage02RestorePreviousSelector();
      PrintFormat("OMPB_STAGE02_CANONICAL_FAIL reason=reload_or_finalize restored=%s",
                  (restored ? "true" : "false"));
      return(false);
     }
   OMPBStage02CheckpointPublished = true;
   Print("OMPB_STAGE02_CANONICAL_PASS checkpoint=published");
   return(true);
  }

Кандидат сначала записывается во временные файлы. Затем он публикуется, повторно загружается и проходит проверку отпечатков вместе с детерминированным выводом. При ошибке советник возвращает предыдущую контрольную точку. На выходе второго этапа мы получаем не просто обновлённый слой, а проверенный энкодер состояния рыночной среды. Его базовые параметры сохранены, окно Current очищено, а OMPB готов работать в режиме OMPB_INFERENCE.


Тестирование

После калибровки прогнозный блок был передан в уже опубликованный контур подготовки торговой политики. Actor, Critic, банки D2Skill и показатель Utility обучались по прежней схеме. Изменился источник рыночного представления: энкодер содержал откалиброванный слой OMPB, зафиксированный в режиме OMPB_INFERENCE. Итоговый прогон выполнял советник Test.mq5 с пресетом 06Inference.set. Обратный проход и сохранение параметров в этом режиме отключены.

На контрольной области Target ошибка снизилась на 0,6056%. Показатель SourceDegradation составил −0,5892%. Отрицательное значение означает, что ошибка на опорной области тоже немного уменьшилась. Изменение невелико, но оно соответствует роли остаточного адаптера. Он не должен заново строить латентное пространство, его задача — мягко уточнить уже обученное представление.

Торговый тест проводился на EURUSD с таймфреймом H1. Период тестирования составил первое полугодие 2026 года. Эти бары не участвовали в калибровке слоя или обучении торговой политики. Начальный депозит равнялся 1000 USD.

За период тестирования модель получила 1075,89 USD чистой прибыли. Конечный баланс достиг 2075,89 USD, что соответствует доходности 107,59%. Кривая росла неравномерно. Быстрый подъём пришёлся на январь, февраль и начало марта, затем последовала весенняя коррекция. Максимальная просадка баланса составила 460,40 USD, или 21,19%, а просадка по средствам достигла 507,80 USD, или 23,02%.

Динамика баланса и средств модели OMPB

Результаты тестирования модели OMPB

Показатель Profit Factor равен 2,00, Expected Payoff составил 22,41 USD, а Recovery Factor достиг 2,12. Значение Sharpe Ratio равно 3,40. Эти цифры выглядят уверенно, но выборка включает только 48 сделок. Поэтому важна не только итоговая доходность, но и структура результата. Валовая прибыль составила 2156,56 USD, валовый убыток — 1080,67 USD. Максимальная серия из шести убыточных сделок принесла −297,50 USD, после чего баланс смог восстановиться.

Прибыльными стали 25 сделок, или 52,08%. Средняя прибыльная операция принесла 86,26 USD, а средний убыток составил 46,99 USD. Значит, система заработала не за счёт исключительно высокой точности входов. Ей помогло соотношение среднего выигрыша и среднего проигрыша, которое приблизилось к 1,84. Максимальная серия из пяти прибыльных сделок дала 391,10 USD.

При этом результат нельзя считать равномерным. Наибольшая прибыльная сделка принесла 491,34 USD и сформировала около 45,7% чистой прибыли. Кроме того, модель открыла 45 коротких и только 3 длинные позиции. Такой перекос хорошо соответствует нисходящим возможностям выбранного участка, но почти ничего не говорит о поведении на устойчивом росте EURUSD. Для более надёжного вывода нужны другие периоды и инструменты с иной структурой волатильности.

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


Заключение

Мы завершили работу по адаптации фреймворка OMPB к задачам финансовых временных рядов. В первой части был реализован байесовский линейный слой. Затем появились OpenCL-примитивы для расчёта Disagreement, Mismatch и KL-дивергенции. На следующем этапе они были объединены в класс CNeuronOMPBOCL. В завершение готовый слой встроен в энкодер состояния рыночной среды между RankTCM и ScenarioForecast.

Обучение энкодера рыночного состояния было разделено на два этапа. Сначала базовый Market Encoder обучался при прозрачном режиме OMPB_BYPASS. Затем его параметры фиксировались, а OMPB проходил отдельную калибровку на областях Reference и Calibration. После этого слой переводился в детерминированный режим OMPB_INFERENCE. Такая схема сохранила причинный порядок Predict-Then-Update и не допустила изменения замороженных компонентов модели.

Поставленная в начале серии задача решена. Фреймворк OMPB адаптирован к архитектуре торговой модели, реализован средствами MQL5 и OpenCL. Новый слой включён в полный цикл обучения модели. На контрольной области Target ошибка снизилась на 0,6056%, а качество на Source не ухудшилось. Итоговый тест подтвердил работоспособность всей системы на данных первой половины 2026 года.


Ссылки


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

# Имя Тип Описание
1 Trajectory.mqh Общий модуль Построение моделей и учебные функции OMPB
2 StudyForecast.mq5 Советник Подготовка прогнозной модели
3 Study.mq5 Советник Обучение базовой политики и формирование навыков
4 StudyOnline.mq5 Советник Калибровка Utility
5 Test.mq5 Советник Тест готовой модели на отложенном периоде
6 01MarketEncoder.set Пресет Обучение базовой прогнозной модели
7 02OMPB.set Пресет Калибровка слоя OMPB
8 03BasePolicy.set Пресет Обучение базовой политики
9 04D2Skill.set Пресет Формирование банков Task Skills и Step Skills
10 05Optimization.set Пресет Калибровка Utility
11 06Inference.set Пресет Итоговый режим вывода
12 NeuroNet.mqh Библиотека классов Библиотека классов для создания нейронной сети
13 NeuroNet.cl OpenCL-программа Библиотека кода OpenCL-программы

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

Прикрепленные файлы |
MQL5.zip (7350.3 KB)
Создание и использование Скиллов для создания артефактов MQL. Часть 2 Создание и использование Скиллов для создания артефактов MQL. Часть 2
Развиваем скилл MQL-TUTOR для генерации артефактов MQL5: добавляем логирование, файл проекта, единый стиль и аккуратный режим Edit. Подключаем опциональный поиск документации через MCP, вводим проверку «здоровья» скилла и поддержку служебных скриптов. В итоге получаем воспроизводимый процесс создания и редактирования скриптов с контролем ошибок, прозрачной историей изменений и меньшими трудозатратами.
Мультиброкерский и мультивалютный граф: симуляция арбитражной торговли Мультиброкерский и мультивалютный граф: симуляция арбитражной торговли
Статья описывает воспроизводимый графовый арбитражный сканер для MetaTrader 5 и Python. Шесть M1‑потоков по EURGBP, EURUSD и GBPUSD объединяются во временной мультиграф, где прямые конвертации считаются по Bid, обратные — по 1/Ask, а параллельные источники конкурируют на каждой ноге. Проверяются пять фиксированных циклов, цены синхронизируются по минуте без forward fill, после издержек выбирается один лучший маршрут на минуту для однозначного исторического результата.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
От одиночного CatBoost к Cross-Fitted Stack в MQL5: ансамбль без утечки будущего и отдельный audit-forward 2026 От одиночного CatBoost к Cross-Fitted Stack в MQL5: ансамбль без утечки будущего и отдельный audit-forward 2026
Статья показывает, как построить stack-ансамбль для MetaTrader 5 без утечки данных между уровнями и расхождений инференса. Реализован purged expanding walk-forward с 72-часовым разрывом, три разнородных CatBoost-классификатора, калибраторы вероятности и размера позиции, а также встраивание 31 модели в советник MQL5. Отдельный аудит 2026 года демонстрирует границы применимости профилей и сравнение с неизменяемой базовой линией.