preview
Автоматическое принятие и отклонение изменений ML-советника в MIDAS/MQL5: защита от переобучения через проверку устойчивости

Автоматическое принятие и отклонение изменений ML-советника в MIDAS/MQL5: защита от переобучения через проверку устойчивости

MetaTrader 5 — Эксперты |
22 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Что такое MIDAS

В этой статье MIDAS — это не отдельная ML-модель, а весь исследовательский и торговый контур, в котором модели обучаются, сравниваются, отбираются и затем используются в MetaTrader 5.

У системы есть текущая рабочая версия — champion, которая считается базовой. Любое изменение создаёт нового кандидата: это может быть другая конфигурация CatBoost, новый порог входа, изменённая калибровка вероятности или набор связанных параметров. Кандидат не заменяет рабочую модель автоматически, даже если показывает более высокий Profit Factor или лучший итоговый score.

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

Таким образом, MIDAS объединяет несколько уровней:

  • исследовательский эксперимент и сравнение кандидатов;

  • фиксацию текущей рабочей модели;

  • многокритериальный Robustness Guard;

  • ACCEPT/ROLLBACK для обновлений;

  • ансамбль CatBoost и дополнительный stack-фильтр;

  • контроль неопределённости прогноза;

  • расчёт допустимого размера позиции;

  • self-test защитной логики при запуске MQL5;

  • shadow-режим, в котором весь торговый контур работает без отправки реальных заявок.

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

Именно поэтому кандидат в MIDAS оценивается не по одной красивой итоговой метрике, а по заранее зафиксированной процедуре отбора.


Введение

Хороший результат на истории ещё не означает, что модель стала лучше. Например, Profit Factor может вырасти с 6 до 8, но одновременно ухудшиться просадка, сократиться число сделок или появиться сильная зависимость от одного участка истории. Поэтому в MIDAS кандидат оценивается не по одной итоговой метрике, а по заранее заданной процедуре отбора. Новая версия заменяет текущую только после прохождения всех проверок. Иначе базовая модель остаётся без изменений. В статье разберём:

  • как фиксировать текущую рабочую версию;
  • почему одного Profit Factor недостаточно;<
  • как сравнивать модели на нескольких участках истории;
  • как формализовать решение «принять или отклонить»;
  • как перенести этот механизм в MQL5;
  • что такая проверка действительно доказывает, а чего она доказать не может.

Основной вопрос:

как отличить полезное изменение торговой модели от случайно удачного результата на истории?

Фиксируем базовую модель

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

  • версию модели;
  • контрольную сумму параметров;
  • версию признаков;

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

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

по сравнению с какой именно моделью мы получили улучшение?

Правило:

Пока кандидат не принят, базовая версия остаётся неизменной.

Если эксперимент отклонён, работа продолжается с той же зафиксированной версией.

Один эксперимент — один вопрос

Для интерпретируемого эксперимента желательно менять один параметр за раз. Например:

  • увеличить глубину деревьев;

  • изменить регуляризацию;

  • поднять минимальную вероятность для входа;

  • изменить допустимое расхождение моделей ансамбля.

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

meta_depth      = 6
meta_iterations = 110
meta_l2         = 24
meta_lr         = 0.035


Такой тест допустим. Корректный вывод: эта комбинация настроек прошла проверку. Некорректный вывод: улучшение вызвано именно увеличением глубины деревьев. Вклад отдельного параметра требует отдельного эксперимента.

Почему одной метрики недостаточно

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

Показатель Назначение
Research score Сводная исследовательская оценка кандидата
Profit Factor Соотношение валовой прибыли и валового убытка
Maximum Drawdown Контроль глубины неблагоприятного режима
Positive months Проверка распределения результата во времени
Trade count Защита от слишком малой выборки сделок
Max symbol share Контроль концентрации количества сделок на одном инструменте
Worst-fold degradation Проверка поведения в худшем временном блоке
Stress PF Проверка устойчивости к ухудшению торговых издержек

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

Проверка на нескольких временных блоках

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

  • первый период дал PF 10;

  • второй - PF 1.1;

  • общий PF - 5.

Общий PF=5 выглядит приемлемо, но разница между PF 10 и PF 1.1 показывает сильную зависимость от периода. Поэтому внутренняя выборка MIDAS разбивается как минимум на два последовательных блока. Схема проверки:

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

  • Profit Factor;

  • просадка;

  • число сделок;

  • распределение результата;

  • ухудшение относительно текущей модели.


Final audit используется только для независимой проверки. Он не участвует в обучении и подборе параметров. После просмотра его результатов изменение параметров делает последующий запуск уже не независимым аудитом.

Два уровня решения: улучшение и защита

Решение состоит из двух проверок. Первая — research score. Она показывает изменение относительно текущей версии. Вторая — Guard. Guard проверяет ограничения, которые не компенсируются высоким score. Например, рост score не компенсирует недопустимую просадку. Такой кандидат отклоняется. Логика:

есть улучшение?
        |
       да
        |
прошли защитные ограничения?
        |
    да / нет
     |     |
 принять  отклонить

В коде:

if(score_delta > 0 && guard_pass)
   ACCEPT;
else
   ROLLBACK;

ROLLBACK здесь означает отказ от замены текущей версии.


Что проверяет защитный фильтр

В опубликованной версии MIDAS используются следующие ограничения:

combined_pf             >= MIDAS_MIN_COMBINED_PF
abs(combined_dd)         <= MIDAS_MAX_ABS_COMBINED_DD
trade_count              >= MIDAS_MIN_TRADES
positive_month_fraction  >= MIDAS_MIN_POSITIVE_MONTHS
max_symbol_trade_share   <= MIDAS_MAX_SYMBOL_SHARE
worst_fold_degradation   >= MIDAS_MIN_WORST_FOLD_DEGRADATION

Кандидат должен:

  1. сохранить приемлемый Profit Factor;

  2. не выйти за предел допустимой просадки;

  3. иметь достаточное число сделок;

  4. показывать прибыль не только в небольшом числе месяцев;

  5. не концентрировать почти всю торговлю на одном инструменте;

  6. не проваливаться слишком сильно на худшем временном участке.

Высокое значение одной метрики не компенсирует нарушение защитного ограничения.

Пример: ACCEPT

Рассмотрим принятый внутренний эксперимент. В нём изменялась группа связанных параметров meta-модели:

meta_depth            = 6
meta_iterations       = 110
meta_l2               = 24
meta_lr               = 0.035
meta_random_strength  = 0.05

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

Метрика Значение
Research score 252.4621
Fold 1 PF 8.1698
Fold 1 DD −76.3
Fold 2 PF 6.2524
Fold 2 DD −79.3
Combined PF 7.0179
Combined DD −79.3
Positive months 100%
Max symbol share 0.3245

Score delta = 9.9613. Кандидат получает ACCEPT и Guard PASS. Решение определяется не отдельным PF, а совокупностью условий. Кандидат:

  • улучшила итоговую оценку;

  • не нарушила защитные ограничения.

Пример: ROLLBACK

Рассмотрим отклонённый кандидат. В нём изменялась следующая конфигурация:

meta_rsm        = 0.55
meta_subsample  = 0.94
p_shrink        = 0.95

Research score снизился до 232.3257, просадка на одном блоке выросла до −343.5, Combined PF составил 5.8387, positive months — 95.83%.

Метрика Значение
Research score 232.3257
Fold 1 PF 5.3856
Fold 1 DD −343.5
Fold 2 PF 6.2406
Fold 2 DD −73.1
Combined PF 5.8387
Combined DD −343.5
Positive months 95.83%
Max symbol share 0.3208

Score delta = −20.1364. Решение: ROLLBACK.

Текущая версия сохраняется. Положительный PF сам по себе не является основанием для обновления модели.

Сохраняем отклонённые эксперименты

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

Кандидат 01 -> принят
Кандидат 02 -> отклонён
Кандидат 03 -> отклонён
Кандидат 04 -> принят

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

Не подгоняем правила отбора

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

Поэтому пороги Guard фиксируются до начала серии экспериментов и входят в версию policy. Если возникает обоснованная необходимость изменить ограничения, это следует оформлять как новую версию правил и новую серию проверок, а не как способ «спасти» уже понравившийся кандидат. Такой подход сохраняет интерпретируемость эксперимента: всегда понятно, по каким именно условиям была принята или отклонена модель.

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


Реализация Guard в MQL5

В MQL5 метрики кандидата передаются через структуру MidasGuardMetrics. Функция Guard последовательно проверяет ограничения:

//+------------------------------------------------------------------+
//| Проверка защитных ограничений кандидата                          |
//+------------------------------------------------------------------+
bool MidasGuardPass(const MidasGuardMetrics &m,string &reason)
  {
   reason="";
   //---
   if(m.combined_pf<MIDAS_MIN_COMBINED_PF)
     {
      reason="combined_pf";
      return(false);
     }
   //---
   if(MathAbs(m.combined_dd)>MIDAS_MAX_ABS_COMBINED_DD)
     {
      reason="combined_dd";
      return(false);
     }
   //---
   if(m.trades<MIDAS_MIN_TRADES)
     {
      reason="trades";
      return(false);
     }
   //---
   if(m.positive_month_fraction<MIDAS_MIN_POSITIVE_MONTHS)
     {
      reason="positive_month_fraction";
      return(false);
     }
   //---
   if(m.max_symbol_share>MIDAS_MAX_SYMBOL_SHARE)
     {
      reason="max_symbol_share";
      return(false);
     }
   //---
   if(m.worst_fold_degradation<MIDAS_MIN_WORST_FOLD_DEGRADATION)
     {
      reason="worst_fold_degradation";
      return(false);
     }
   //---
   return(true);
  }

Проверка завершается при первом нарушении. В reason сохраняется имя сработавшего ограничения:

combined_dd
trades
positive_month_fraction
max_symbol_share

Итоговое решение

//+------------------------------------------------------------------+
//| Итоговая проверка кандидата                                      |
//+------------------------------------------------------------------+
bool MidasAcceptCandidate(const MidasGuardMetrics &m,string &reason)
  {
   if(m.score_delta<=MIDAS_MIN_SCORE_DELTA)
     {
      reason="score_delta";
      return(false);
     }
   //---
   return(MidasGuardPass(m,reason));
  }

Итоговое правило: кандидат должен улучшить score и пройти Guard.


Self-test Guard при запуске

Guard проверяется не только во время исследовательского отбора, но и при запуске советника. Это защищает от ошибок, которые могут появиться после изменения MQL5-кода: перепутанного знака сравнения, случайно изменённого порога, удалённого условия или неверной логики возврата результата.

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

Нормальные метрики        -> принять
Отрицательное улучшение   -> отклонить
Слишком большая просадка  -> отклонить
Слишком высокая доля
одного инструмента        -> отклонить

Самопроверка вызывается из OnInit() до запуска основной торговой логики. Если хотя бы один контрольный случай даёт неожиданный результат, инициализация завершается с INIT_FAILED . Это не оценка качества стратегии и не проверка прибыльности модели; self-test подтверждает только то, что реализованная логика допуска ведёт себя в контрольных сценариях так, как было предусмотрено.

Код:

//+------------------------------------------------------------------+
//| Самопроверка защитной политики MIDAS                             |
//+------------------------------------------------------------------+
bool MidasRobustSelfTest()
  {
   MidasGuardMetrics m;
   string            reason;
   //--- Нормальный кандидат должен пройти
   m.score_delta=1.0;
   m.combined_pf=1.7;
   m.combined_dd=-700.0;
   m.trades=700;
   m.positive_month_fraction=0.90;
   m.max_symbol_share=0.25;
   m.worst_fold_degradation=0.0;
   //---
   if(!MidasAcceptCandidate(m,reason))
      return(false);
   //--- Отрицательное улучшение должно блокироваться
   m.score_delta=-0.1;
   //---
   if(MidasAcceptCandidate(m,reason))
      return(false);
   //--- Недопустимая просадка должна блокироваться
   m.score_delta=2.0;
   m.combined_dd=-3000.0;
   //---
   if(MidasAcceptCandidate(m,reason))
      return(false);
   //--- Слишком высокая концентрация сделок также блокируется
   m.combined_dd=-600.0;
   m.max_symbol_share=0.60;
   //---
   if(MidasAcceptCandidate(m,reason))
      return(false);
   //---
   return(true);
  }

Самопроверка вызывается в OnInit() до запуска основной логики:

//--- Проверка защитной политики MIDAS
if(InpRequireRobustGuardSelfTest)
  {
   if(!MidasRobustSelfTest())
     {
      Print("MIDAS ROBUST POLICY SELFTEST: FAIL");
      return(INIT_FAILED);
     }
   //---
   PrintFormat("MIDAS ROBUST POLICY SELFTEST: PASS | guard=%s | champion=%s",
               MIDAS_GUARD_VERSION,
               MIDAS_CHAMPION_HASH);
  }


От нового H1-бара до stack-фильтра

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

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

//--- выполняем расчёт только один раз на новом H1-баре
string symbol_name=g_symbols[symbol_index];
datetime bar_time=iTime(symbol_name,PERIOD_H1,0);

if(bar_time<=0 || bar_time==g_last_bar[symbol_index])
   return;

if(g_last_bar[symbol_index]==0 && !InpTradeOnFirstTimerRun)
  {
   g_last_bar[symbol_index]=bar_time;
   return;
  }

После этого формируется базовый сигнал. Для профилей V5 дополнительно рассчитывается контекст связанных инструментов и строится набор признаков для stack-фильтра; в полной версии он содержит 39 признаков.

   //--- после базового кандидата применяем фильтр V5 Stack
   if(signal!=0 && IsV5Profile())
     {
      double triangle_count;
      double agreement;
      double probability_mean;
      double probability_max;
      double probability_min;
      double probability_std;
      double lot_mean;
      double lot_max;
      double lot_min;
      double lot_std;
      double direction_mean;
      double direction_max;
      double confidence_mean;
      double confidence_max;

      //--- для внутреннего треугольного кандидата контекст уже рассчитан
      if(native_signal)
        {
         triangle_count=1.0;
         agreement=1.0;
         probability_mean=triangle_probability;
         probability_max=triangle_probability;
         probability_min=triangle_probability;
         probability_std=0.0;
         lot_mean=triangle_lot;
         lot_max=triangle_lot;
         lot_min=triangle_lot;
         lot_std=0.0;

         //--- сохраняем направление и оценку уверенности для полного набора из 39 признаков
         direction_mean=MathAbs(native_direction_raw);
         direction_max=MathAbs(native_direction_raw);
         confidence_mean=MathAbs(native_direction_probability-0.5);
         confidence_max=MathAbs(native_direction_probability-0.5);
        }
      else
        {
         TriangleAggregate(symbol_index,signal,
                           triangle_count,agreement,
                           probability_mean,probability_max,
                           probability_min,probability_std,
                           lot_mean,lot_max,lot_min,lot_std,
                           direction_mean,direction_max,
                           confidence_mean,confidence_max);
        }

      double stack_features[];
      BuildStackFeatures(symbol_index,signal,score,native_signal,
                         triangle_count,agreement,
                         probability_mean,probability_max,
                         probability_min,probability_std,
                         lot_mean,lot_max,lot_min,lot_std,
                         direction_mean,direction_max,
                         confidence_mean,confidence_max,
                         stack_features);

      if(!StackPredict(stack_features,model_probability,model_lot,uncertainty)
         || model_probability<ProfileThreshold()
         || uncertainty>InpMaximumStackUncertainty)
         signal=0;

Здесь важно разделять три разных величины.

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

Такое разделение сделано намеренно. Одна и та же вероятность не должна одновременно определять направление сделки, разрешать вход и напрямую задавать его объём.


Как работает StackPredict()

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

StackPredict() получает уже сформированный сигнал и выполняет дополнительную модельную проверку. Все CatBoost-модели встроены непосредственно в советник, поэтому во время торговли не требуется внешний Python-процесс или загрузка файлов .cbm . Три модели ансамбля независимо оценивают один набор признаков, после чего их среднее используется как агрегированный отклик.

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

Управление существующей позицией и новый вход

После фильтрации советник проверяет наличие открытой корзины по инструменту. Для существующей позиции доступны:

  • продолжить удержание;

  • закрыть позицию после достижения максимального времени удержания;

  • закрыть её при появлении противоположного сигнала;

  • выполнить разрешённое добавление позиции.

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

//--- новый вход разрешён только без действующей корзины и после всех фильтров
if(signal==0 || have_basket || CountOurBaskets()>=InpMaximumPortfolioPositions)
      return;

   double virtual_risk=MathMax(InpVirtualRiskATR*atr,InpMinimumStopPips*pip);
   double lot_multiplier=(InpUseCalibratedLots
                          ? Clamp(model_lot,InpLotMultiplierFloor,InpLotMultiplierCap)
                          : 1.0);
   double lot=RiskLot(symbol_name,virtual_risk,lot_multiplier);

   bool opened=OpenOrder(symbol_index,signal,lot,atr,
                         (native_signal ? "V5 TRI NATIVE" : "V5 BASE"),true);

Размер позиции рассчитывается из базового риска. К нему применяется ограниченный модельный множитель. Множитель ограничен диапазоном InpLotMultiplierFloor...InpLotMultiplierCap. Soft Grid относится к управлению существующей позицией и в опубликованном профиле отключён.

Отправка торгового приказа

Торговая заявка отправляется из OpenOrder(). До вызова сигнал проходит:

  • проверку нового бара;

  • проверку торговых условий;

  • первоначальную модель;

  • дополнительный фильтр V5;

  • контроль неопределённости;

  • ограничение числа портфельных позиций;

  • расчёт допустимого объёма.

Параметр InpShadowOnly отключает реальную отправку заявки. В shadow-режиме решение рассчитывается и журналируется, но приказ брокеру не отправляется. При InpShadowOnly=false используются CTrade.Buy() или CTrade.Sell().

//+------------------------------------------------------------------+
//| Открывает рыночную позицию с защитными уровнями                  |
//+------------------------------------------------------------------+
bool OpenOrder(int si,
               int signal,
               double lot,
               double atr,
               string comment,
               bool initial)
  {
   string symbol_name=g_symbols[si];

   //--- в теневом режиме решение журналируется без отправки заявки
   if(InpShadowOnly)
     {
      if(InpVerboseLog)
         PrintFormat("MIDAS SHADOW %s signal=%d lot=%.2f comment=%s",
                     symbol_name,signal,lot,comment);
      return(false);
     }

   double ask=SymbolInfoDouble(symbol_name,SYMBOL_ASK);
   double bid=SymbolInfoDouble(symbol_name,SYMBOL_BID);
   double point=SymbolInfoDouble(symbol_name,SYMBOL_POINT);
   double pip=(StringFind(symbol_name,"JPY")>=0 ? 0.01 : 0.0001);
   double sl=0.0;
   double tp=0.0;

   //--- для торгового профиля рассчитываем ATR-защиту до отправки заявки
   if(InpExitMode==MIDAS_LIVE_ATR_PROTECTED)
     {
      double min_stop=((double)SymbolInfoInteger(symbol_name,SYMBOL_TRADE_STOPS_LEVEL)+1)*point;
      double stop_distance=MathMax(MathMax(InpStopATR*atr,InpMinimumStopPips*pip),min_stop);
      double take_distance=MathMax(InpTakeATR*atr,min_stop);
      int digits=(int)SymbolInfoInteger(symbol_name,SYMBOL_DIGITS);

      if(signal>0)
        {
         sl=NormalizeDouble(ask-stop_distance,digits);
         tp=NormalizeDouble(ask+take_distance,digits);
        }
      else
        {
         sl=NormalizeDouble(bid+stop_distance,digits);
         tp=NormalizeDouble(bid-take_distance,digits);
        }
     }

   //--- передаём идентификатор советника и допустимое отклонение цены
   g_trade.SetExpertMagicNumber(InpMagicBase+si);
   g_trade.SetDeviationInPoints(InpDeviationPoints);

   //--- после всех фильтров отправляем единственную рыночную заявку
   bool opened=(signal>0
                ? g_trade.Buy(lot,symbol_name,ask,sl,tp,comment)
                : g_trade.Sell(lot,symbol_name,bid,sl,tp,comment));

   if(opened)
     {
      double execution_price=(signal>0 ? ask : bid);

      //--- сохраняем состояние корзины только после успешного исполнения
      if(initial)
        {
         g_grid_level[si]=1;
         g_grid_base_lot[si]=lot;
        }
      else
         g_grid_level[si]++;

      g_grid_last_price[si]=execution_price;
      g_grid_last_add_bar[si]=iTime(symbol_name,PERIOD_H1,0);
      SaveGridState(si);
     }

   return(opened);
  }

В режиме MIDAS_LIVE_ATR_PROTECTED уровни SL и TP рассчитываются до отправки заявки. Расстояние определяется ATR, настройками советника и минимальным stop level символа. В показанной реализации есть ограничение: pip определяется по наличию JPY в имени символа: double pip=(StringFind(symbol_name,"JPY")>=0 ? 0.01 : 0.0001); Этот вариант рассчитан на стандартные валютные пары. Для металлов, CFD и других инструментов шаг цены следует получать из свойств символа.

Поэтому текущая реализация не является универсальной для всех классов активов.

Что self-test не проверяет

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

  • model hash

  • feature hash или описание порядка признаков

  • reference predictions

  • версия policy

  • сравнение MQL5-инференса с эталонным результатом исследовательской среды

В показанной версии эти runtime-проверки ещё не реализованы.

Shadow-режим

После внутреннего отбора кандидата можно проверить в shadow-режиме. Для этого используется InpShadowOnly. Расчёт признаков, прогноз и решение выполняются штатно. Брокеру заявка не отправляется. В журнале можно сохранять:

  • время принятия решения (timestamp);

  • торговый инструмент (symbol);

  • контрольную сумму признаков (features_hash);

  • прогноз модели (prediction);

  • итоговое решение (decision);

  • решение базовой версии (baseline_decision);

  • версию правил допуска (policy_version);

  • контрольную сумму модели (model_hash).

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



Запуск советника

К статье приложен минимальный набор файлов для запуска MIDAS в MQL5.

Распаковка файлов

MQL5.zip распаковывается в каталог данных терминала. Структура каталогов:

MQL5/Experts
MQL5/Include
MQL5/Profiles/Tester
После этого советник можно скомпилировать и запустить.

Ограничения метода

Robustness Guard формализует veto-решение: кандидат не заменяет текущую модель при нарушении заданных ограничений. Это инженерный механизм отбора, а не доказательство будущей live-устойчивости. Примеры ACCEPT и ROLLBACK показывают работу этого правила на исторических метриках. Для оценки способности Guard отбирать более устойчивые версии нужны отдельные untouched audit, ablation и seed-stability проверки.


Заключение

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

Приложенные файлы

К статье прикладывается один архив MQL5.zip.

Файл внутри MQL5.zip Назначение
MQL5/Experts/MIDAS_V5/MIDAS_SINERGY_V5_CROSSFIT_STACK_SAFE_GRID.mq5 Основной торгующий советник с 31 встроенной CatBoost-моделью и полным execution-контуром.
MQL5/Include/MIDAS_RobustGuard/MIDAS_ROBUST_GUARD.mqh Veto-критерии, ACCEPT/ROLLBACK contract и Self-test.
MQL5/Include/MIDAS_RobustGuard/MIDAS_ROBUST_POLICY.mqh Замороженные пороги Guard, версия policy и hash принятой конфигурации.
MQL5/Profiles/Tester/MIDAS_V5/V5_AUDIT_ROBUST_GUARD_TRADING.set Готовый профиль запуска с включённым Guard Self-test, выключенным Shadow-режимом и выключенным Soft Grid.

Литература

  1. Wolpert D. H. Stacked Generalization. Neural Networks, 1992.
  2. Huber P. J. Robust Estimation of a Location Parameter. Annals of Mathematical Statistics, 1964.
  3. Prokhorenkova L. et al. CatBoost: Unbiased Boosting with Categorical Features. NeurIPS, 2018.
  4. Lopez de Prado M. Advances in Financial Machine Learning. Wiley, 2018.
  5. Bergmeir C., Benitez J. M. On the Use of Cross-Validation for Time Series Predictor Evaluation. Information Sciences, 2012.
Прикрепленные файлы |
MQL5.zip (589.33 KB)
Алгоритм оптимизации скопы — Osprey Optimization Algorithm(OOA) Алгоритм оптимизации скопы — Osprey Optimization Algorithm(OOA)
Алгоритм оптимизации скопы (OOA) — ещё один представитель метаэвристик с красивой метафорой и первыми местами на бенчмарках. Проверка на стенде показывает другое. В статье разбираем, какие две формулы ломают канон, исправляем их по одной с измерением вклада каждой правки и доводим алгоритм с 30% до 60%. На выходе — готовая реализация в C_AO с тремя параметрами для оптимизации торговых систем и понятный способ проверить любой новый алгоритм, прежде чем ему доверять.
От начального к среднему уровню: Технические индикаторы (II) От начального к среднему уровню: Технические индикаторы (II)
В этой статье мы покажем, как создать индикатор на MQL5, который рисует несколько скользящих средних на одном графике, сокращая объём дублирующегося кода. Используются iMA, буферы индикатора, CopyBuffer, PlotIndexSetInteger/String и постоянная структура, объединяющая периоды, методы и цвета. Размеры indicatorbuffers и indicatorplots определяются на основе Averange.Size(). Такой подход упрощает сопровождение и позволяет добавлять или удалять скользящие средние, изменяя только один список.
Свинговые экстремумы и откаты в MQL5 (Часть 3): Определение структурной валидности за рамками простых максимумов и минимумов Свинговые экстремумы и откаты в MQL5 (Часть 3): Определение структурной валидности за рамками простых максимумов и минимумов
В статье представлен советник MQL5, который переводит первичное обнаружение свингов на уровень основанного на правилах модуля структурной валидации. Свинги подтверждаются сломом структуры (break of structure), импульсным смещением, снятием ликвидности или удержанием уровня во времени (time-based respect), после чего связываются с картой ликвидности (liquidity map) и структурной машиной состояний (structural state machine). В результате получаются учитывающие контекст точки входа и стопы, привязанные к подтверждённым уровням, что помогает фильтровать шум и систематизировать исполнение сделок.
Разработка индикатора рыночной энтропии: торговая система, основанная на теории информации Разработка индикатора рыночной энтропии: торговая система, основанная на теории информации
В статье рассматривается разработка индикатора рыночной энтропии, основанного на принципах теории информации, для измерения неопределённости и информационного содержания финансовых рынков. Применяя к ценовым движениям такие понятия, как энтропия Шеннона, индикатор количественно определяет, находится ли рынок в структурированном (трендовом), переходном или хаотичном состоянии.