preview
VQC-фильтр с живым обучением прямо в MQL5 поверх графового fair-value скальпера

VQC-фильтр с живым обучением прямо в MQL5 поверх графового fair-value скальпера

MetaTrader 5Торговые системы |
80 0
Galym Yechshanov
Galym Yechshanov

Задача этой статьи — добавить к готовому торговому сигналу отдельный вероятностный фильтр. Вместо частотных счётчиков используется небольшая параметризованная схема вращений, которую по‑английски называют variational quantum circuit (VQC), которую по-английски называют variational quantum circuit (VQC). Важная оговорка сразу: это не квантовый компьютер и не связь с квантовым железом. Это классическая линейная алгебра — обычные векторы и матрицы вращения, которая считается аналитически на CPU и лишь по структуре скопирована с квантовых схем. Называть её квантовой моделью можно только с этой оговоркой: реального квантового ускорения или квантовых эффектов здесь нет, есть просто удобная параметризация нелинейного преобразования признаков.

Второй слой статьи — graph fair‑value скальпер. Он строит граф курсов по котировкам 28 валютных пар и через DFS находит альтернативные пути пересчёта одной валюты в другую (EURUSD напрямую и через GBP, JPY и т. д.). Справедливая цена пары рассчитывается как средневзвешенное по всем найденным путям. Если рыночная цена сильно отклонилась от этой справедливой оценки, это кандидат на вход. VQC-модель в этой связке не открывает сделки сама — она подтверждает или отклоняет направление, предложенное графом, а также управляет размером позиции через сайзинг по Келли.


Архитектура: два независимых сигнала, один вход

Схема простая на уровне идеи и сложная в деталях реализации:

  1. Графовый движок считает справедливую стоимость пары по мультихоп-путям и определяет отклонение в пунктах.
  2. Если отклонение больше порога — это кандидат на вход, направление задаётся знаком отклонения: рынок дороже справедливой цены → SELL, дешевле → BUY.
  3. VQC-модель для этой же пары независимо считает направленный прогноз в диапазоне [-1, 1] по восьми рыночным признакам, один из которых — то самое отклонение от графовой fair value (значит, два слоя не полностью независимы: VQC в том числе учится, отрабатывает ли графовый сигнал в ближайшем баре).
  4. Сделка открывается только если знак прогноза VQC совпадает с направлением от графа — это фильтр ложных срабатываний, тот же принцип gate-фильтра, что и в статье про ассоциативные правила.
  5. Размер позиции считается либо классическим риск-лотом по стоп-лоссу, либо через сайзинг по Келли на основе силы прогноза VQC.

Ключевое отличие от статических индикаторных фильтров — веса VQC не загружаются из офлайн‑файла и не остаются постоянными. Они меняются прямо во время торговли, на каждом баре, через точный аналитический градиент (parameter-shift rule) и Adam-оптимизатор — без Python, без экспорта весов, без внешних процессов.


Что на самом деле считает квантовая часть

Схема кодирует восемь рыночных признаков в три кубита (2³ = 8 базисных состояний). Кубит — это просто двумерный комплексный вектор; в этой реализации всё вещественное, поэтому по сути это просто единичный вектор в 8-мерном вещественном пространстве, а вращения кубита — это стандартные 2D-повороты, применяемые к парам координат этого вектора.

//+------------------------------------------------------------------+
//| Базовые константы VQC                                            |
//+------------------------------------------------------------------+
#define NUM_QUBITS        3
#define NUM_STATES        8      // 2^NUM_QUBITS
#define FEATURES_COUNT    8      // 7 базовых + 1 графовый residual

Восемь признаков — это нормализованный размах бара, волатильность, моментум, синус/косинус часа суток (циклическое кодирование времени), отклонение цены от SMA, последний return и, отдельным пунктом, отклонение цены от графовой fair value:

//--- Фрагмент ExtractFeatures(): добавляем graph fair-value residual
if(m_fair_value_provider!=NULL)
   features[7]=MathTanh(m_fair_value_provider.GetFairValueResidual(symbol));
else
   features[7]=0.0; //--- без провайдера признак отключён

Каждый признак превращается в угол поворота через обучаемые параметры w[i] и b[i] (по умолчанию w=1, b=0 — тождественное преобразование до начала обучения):

//--- Фрагмент ComputeQuantumStateAnalytic(): кодируем признак в угол
double w=m_weights[feature_idx];
double b=m_biases[feature_idx];
double angle=MathMax(MathMin((raw*w+b)*M_PI,M_PI),-M_PI);

//--- применяем поворот к парам амплитуд выбранного кубита
for(int i=0;i<NUM_STATES;i++)
  {
   if((i&(1<<qubit))==0)
     {
      int i1=i|(1<<qubit);
      double temp=state[i];

      state[i] =MathCos(angle/2.0)*temp-MathSin(angle/2.0)*state[i1];
      state[i1]=MathSin(angle/2.0)*temp+MathCos(angle/2.0)*state[i1];
     }
  }

После кодирования всех трёх кубитов схема добавляет перепутывание — искусственную нелинейную связь между парами признаков через фазовый множитель, зависящий от произведения двух признаков, а затем фиксированный поворот на 45°, который задаёт базовый баланс между состояниями вверх и вниз ещё до нормировки:

//--- Фрагмент ComputeQuantumStateAnalytic(): связываем пары кубитов
for(int i=0;i<NUM_QUBITS-1;i++)
  {
   for(int j=i+1;j<NUM_QUBITS;j++)
     {
      double fi=active_mask[i] ? features[i] : 0.0;
      double fj=active_mask[j] ? features[j] : 0.0;
      double phase=M_PI/2.0*(fi*fj);

      //--- изменяем только состояния, где оба выбранных бита равны единице
      for(int k=0;k<NUM_STATES;k++)
        {
         if(((k&(1<<i))!=0) && ((k&(1<<j))!=0))
            state[k]*=MathCos(phase);
        }
     }
  }

Финальный вектор нормируется. Вероятности восьми состояний считаются напрямую как квадраты координат, без сэмплирования. На реальном квантовом железе это потребовало бы тысяч измерений (шотов), здесь — одна аналитическая формула.

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

//+------------------------------------------------------------------+
//| Рассчитывает направленный голос по вероятностям состояний        |
//| Возвращает значение в диапазоне [-1,1]                           |
//+------------------------------------------------------------------+
double GetWeightedVote(const double &state_probs[])
  {
   double up_votes=0.0;
   double down_votes=0.0;
   double total_weight=0.0;

   //--- суммируем вероятности состояний с положительным и отрицательным голосом
   for(int i=0;i<ArraySize(state_probs);i++)
     {
      int num_ones=CountBits(i);
      double normalized_vote=2.0*(double)num_ones/(double)NUM_QUBITS-1.0;
      double vote_weight=state_probs[i];

      if(normalized_vote>0.0)
         up_votes+=vote_weight;
      else if(normalized_vote<0.0)
         down_votes+=vote_weight;

      total_weight+=vote_weight;
     }

   //--- при вырожденном распределении возвращаем нейтральный прогноз
   if(total_weight<=1e-9)
      return(0.0);

   return((up_votes-down_votes)/total_weight);
  }

По сути, вся эта конструкция — красиво параметризованная нелинейная функция от восьми входов, дающая число в [-1, 1]. Её честное название — не квантовая нейросеть, а классически симулированная параметризованная схема вращений, и в статье дальше используется термин VQC именно в этом узком, буквальном смысле.


Живое обучение весов прямо в терминале: parameter-shift rule

Ключевая инженерная деталь: веса w[i]/b[i] обучаются прямо во время работы советника. Обновление выполняется на каждом баре, когда известен результат предыдущего прогноза. Обычный backpropagation тут напрямую не применить: угол поворота участвует в тригонометрических функциях, а не в линейных слоях. Но для однокубитных поворотов вида RY(θ) существует точная (не приближённая) формула градиента — parameter-shift rule:

dE/dθ = 0.5 * (E(θ + π/2) - E(θ - π/2))

Это не численная аппроксимация конечными разностями с маленьким шагом — это математически точный результат для гейтов с генератором собственных значений ±1/2 (Mitarai et al. 2018, Schuld et al. 2019). Чтобы получить градиент по обучаемому весу w[i], а не напрямую по углу θ, добавляется обычное цепное правило: сдвигаем не сам угол, а w[i] на величину, которая через θ = (feat·w + b)·π даёт ровно сдвиг на ±π/2:

//--- Фрагмент TrainStep(): вычисляем градиент веса по parameter-shift rule
//--- θ=(feat*w+b)*π, поэтому dθ/dw=feat*π
double grad_w=0.0;

if(MathAbs(feat)>1e-9)
  {
   //--- сдвиг веса изменяет угол ровно на +π/2 и -π/2
   double shift_w=1.0/(2.0*feat);

   model.SetWeight(i,w+shift_w);
   double pred_plus=model.ComputeRawPrediction(features,active_mask);

   model.SetWeight(i,w-shift_w);
   double pred_minus=model.ComputeRawPrediction(features,active_mask);

   //--- восстанавливаем исходный параметр до обновления оптимизатором
   model.SetWeight(i,w);

   double d_pred_d_theta=0.5*(pred_plus-pred_minus);
   grad_w=d_pred_d_theta*feat*M_PI;
  }

Если признак близок к нулю, угол вообще не зависит от веса — тогда градиент честно принимается за 0, а не превращается в NaN от деления на почти нулевой feat. Для смещения b[i] всё проще: dθ/db = π всегда, поэтому сдвиг на ±0.5 гарантированно даёт сдвиг угла ровно на ±π/2 независимо от значения признака.

На каждый обучаемый параметр уходит по два дополнительных прогона схемы (plus/minus сдвиг), то есть на 16 параметров (8 весов + 8 смещений) — 32 дополнительных вызова ComputeRawPrediction на один шаг обучения. Для аналитической формулы без сэмплирования это остаётся дёшево, но при увеличении числа кубитов или признаков стоимость шага обучения растёт линейно от числа параметров — это стоит держать в уме при расширении схемы.

Полученный точный градиент дальше обновляется обычным Adam-оптимизатором с bias-correction — тем же алгоритмом, что используется в PyTorch/TensorFlow, только вручную реализованным на MQL5:

//--- Фрагмент TrainStep(): обновляем вес w[i] оптимизатором Adam
m_m_w[i]=m_beta1*m_m_w[i]+(1.0-m_beta1)*loss_grad_w;
m_v_w[i]=m_beta2*m_v_w[i]+(1.0-m_beta2)*loss_grad_w*loss_grad_w;

double m_hat_w=m_m_w[i]/(1.0-MathPow(m_beta1,m_t));
double v_hat_w=m_v_w[i]/(1.0-MathPow(m_beta2,m_t));

//--- применяем bias-correction и защищаем знаменатель малой константой
model.SetWeight(i,w-m_lr*m_hat_w/(MathSqrt(v_hat_w)+m_eps));

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

//+------------------------------------------------------------------+
//| Регистрирует фактический исход предыдущего прогноза              |
//| Обновляет калибровку, детектор дрейфа и параметры модели         |
//+------------------------------------------------------------------+
void RegisterOutcome(double prediction,double actual_move_normalized)
  {
   //--- сохраняем абсолютную ошибку для конформной калибровки
   double nonconformity=MathAbs(prediction-actual_move_normalized);
   m_conformal.AddScore(nonconformity);

   //--- проверяем, не изменилось ли распределение ошибок
   bool drift=m_drift_detector.Update(nonconformity);
   if(drift)
      Print("!!! Concept drift detected by Page-Hinkley test...");

   //--- обучаемся только при наличии сохранённого прогноза прошлого бара
   if(m_has_pending_prediction)
     {
      double loss=m_trainer.TrainStep(GetPointer(this),
                                      m_last_features,
                                      m_last_active_mask,
                                      actual_move_normalized);
      m_has_pending_prediction=false;
     }
  }

Важная оговорка о заглядывании в будущее: прогноз на баре N делается по данным, доступным на этот момент, запоминается в m_last_features/m_last_active_mask, и только когда открывается следующий бар N+1 и фактическое движение уже известно, вызывается RegisterOutcome. Обучение всегда onpolicy, с задержкой в один бар — это защищает от классической ошибки модель училась на будущем баре в живой торговле. В офлайн-бэктесте на истории эта же схема требует по-прежнему честной валидации: обучение в реальном времени не отменяет необходимости в purge/embargo при оценке качества (см. ниже).


Идея 4: конформная калибровка порога уверенности

Статичный порог уверенности (когда-то просто торговать, если |prediction| > 0.3) плохо переживает смену режима рынка: на спокойном рынке такой порог редко достижим, на волатильном — достигается слишком часто и даёт мусорные сигналы. CConformalCalibrator держит скользящее окно ошибок прогноза (nonconformity scores) и берёт эмпирический квантиль этого распределения как порог:

//+------------------------------------------------------------------+
//| Возвращает динамический порог уверенности                        |
//| Порог определяется квантилем накопленных ошибок                  |
//+------------------------------------------------------------------+
double GetThreshold(double fallback_threshold) const
  {
   int n=ArraySize(m_scores);

   //--- до накопления минимальной выборки используем резервный порог
   if(n<30)
      return(fallback_threshold);

   double sorted[];
   ArrayResize(sorted,n);
   ArrayCopy(sorted,m_scores);
   ArraySort(sorted);

   //--- выбираем эмпирический квантиль заданного уровня доверия
   int idx=(int)MathCeil(m_confidence_level*(n+1))-1;
   idx=MathMax(0,MathMin(idx,n-1));

   return(sorted[idx]);
  }

При confidence_level = 0.90 это формально даёт покрытие: порог подобран так, что около 90% исторических ошибок прогноза были меньше этого значения. Это стандартный приём conformal prediction — в отличие от статичного порога, он адаптируется к реальному распределению ошибок конкретной пары, а не к произвольной константе, угаданной на глаз.


Идея 5: детекция режима волатильности без полноценного HMM

Полноценный Baum-Welch EM для скрытой марковской модели тяжело держать в реальном времени внутри терминала, поэтому CRegimeDetector использует упрощённый классификатор: скользящие процентили волатильности плюс гистерезис переключения состояний, чтобы не дёргаться между режимами на каждом баре из-за шума.

//--- Фрагмент CRegimeDetector::Classify(): выбираем кандидата режима
double p25=sorted[(int)(n*0.25)];
double p75=sorted[(int)(n*0.75)];

ENUM_MARKET_REGIME candidate;
if(trend_strength>0.6)
   candidate=REGIME_TREND;
else if(current_vol>p75)
   candidate=REGIME_HIGH_VOL;
else if(current_vol<p25)
   candidate=REGIME_LOW_VOL;
else
   candidate=REGIME_NORMAL;

//--- переключаем режим только после подтверждения на нескольких барах
if(candidate==m_current_regime)
   m_hysteresis_count=0;
else
  {
   m_hysteresis_count++;

   if(m_hysteresis_count>=m_confirm_bars)
     {
      m_current_regime=candidate;
      m_hysteresis_count=0;
     }
  }

return(m_current_regime);

Режим используется как множитель к порогу уверенности из conformal-калибровки: в REGIME_HIGH_VOL требуется вдвое больше уверенности (2.0×), в REGIME_TREND — порог, наоборот, понижается (0.7×), потому что на устойчивом тренде даже умеренно уверенный прогноз статистически чаще отрабатывает.


Идея 7: отбор фичей через корреляционный фильтр

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

//+------------------------------------------------------------------+
//| Формирует маску активных признаков                               |
//| Коррелирующие признаки подавляются жадным фильтром               |
//+------------------------------------------------------------------+
void GetActiveMask(bool &active_mask[])
  {
   ArrayResize(active_mask,FEATURES_COUNT);
   ArrayInitialize(active_mask,true);

   //--- до накопления минимальной истории оставляем все признаки
   if(m_count<30)
      return;

   //--- сравниваем каждую активную пару признаков
   for(int i=0;i<FEATURES_COUNT;i++)
     {
      if(!active_mask[i])
         continue;

      for(int j=i+1;j<FEATURES_COUNT;j++)
        {
         if(!active_mask[j])
            continue;

         double corr=PearsonCorr(i,j);

         //--- подавляем второй признак при превышении порога корреляции
         if(MathAbs(corr)>m_corr_threshold)
            active_mask[j]=false;
        }
     }
  }

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


Идея 9: Сайзинг по Келли по силе прогноза

Вместо фиксированного лота или классического риск-лота по стоп-лоссу, лот можно считать через долю от критерия Келли, где вместо истинной вероятности победы используется псевдовероятность, полученная из силы прогноза VQC:

//+------------------------------------------------------------------+
//| Рассчитывает объём позиции по дробному критерию Келли            |
//| Значение prediction должно находиться в диапазоне [-1,1]         |
//+------------------------------------------------------------------+
double GetLotSize(double prediction,double win_loss_ratio,double base_lot)
  {
   //--- преобразуем модуль прогноза в эвристическую вероятность выигрыша
   double p=0.5+0.5*MathAbs(prediction);
   double q=1.0-p;
   double b=MathMax(win_loss_ratio,0.01);

   //--- отрицательное значение Келли запрещает увеличение позиции
   double kelly_pct=(b*p-q)/b;
   kelly_pct=MathMax(0.0,kelly_pct)*m_fraction;

   //--- ограничиваем итоговый объём допустимым диапазоном
   double lot=base_lot*kelly_pct*MathAbs(prediction)*2.0;
   lot=MathMax(m_min_lot,MathMin(m_max_lot,lot));

   return(NormalizeDouble(lot,2));
  }

Здесь стоит быть честным насчёт слабого места: критерий Келли по определению требует настоящую вероятность выигрыша и настоящее отношение выигрыш/проигрыш, оценённые на достаточно большой статистике. Здесь p — это не вероятность в строгом смысле, а эвристическое преобразование |prediction| в [0.5, 1.0], а win_loss_ratio считается по скользящему среднему прибыльных/убыточных сделок конкретного советника в реальном времени, на малой выборке. Использование доли Kelly (0.25) — разумная защита от переоценки, но при малом числе сделок win_loss_ratio может быть нестабильным, и лот будет скакать вслед за этой нестабильностью. Это стоит проверять отдельно, прежде чем доверять сайзингу по Келли на реальном капитале


Идея 10: детекция дрейфа концепции через тест Пейджа — Хинкли

ADWIN и другие оконные детекторы дрейфа концепции требуют хранить историю наблюдений в памяти. Тест Пейджа — Хинкли (Page-Hinkley) даёт похожий эффект без хранения окна — только несколько скалярных аккумуляторов, что удобно для MQL5:

//+------------------------------------------------------------------+
//| Проверяет поток ошибок тестом Пейджа-Хинкли                      |
//| Возвращает true при обнаружении дрейфа концепции                 |
//+------------------------------------------------------------------+
bool Update(double error)
  {
   //--- обновляем среднее и накопленную центрированную сумму
   m_n++;
   m_mean_error+=(error-m_mean_error)/m_n;
   m_cumulative_sum+=(error-m_mean_error-m_delta);

   if(m_cumulative_sum<m_min_cumulative_sum)
      m_min_cumulative_sum=m_cumulative_sum;

   //--- сравниваем отклонение суммы с порогом детектора
   double ph=m_cumulative_sum-m_min_cumulative_sum;
   if(ph>m_lambda)
     {
      Print("CPageHinkley: DRIFT DETECTED...");
      Reset();
      return(true);
     }

   return(false);
  }

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


Идея 3: purge/embargo — то, чего часто не хватает в бэктестах

При валидации нельзя перемешивать старые и новые бары. CPurgeEmbargo формализует эту защиту для полноценного walk-forward: тестовый бар, оказавшийся слишком близко к границе тренировочного окна, исключается из тестовой выборки, чтобы через автокорреляцию цены не произошла скрытая утечка информации из обучения в тест (подход из работы Lopez de Prado про финансовое ML):

//+------------------------------------------------------------------+
//| Проверяет попадание тестового бара в embargo-интервал            |
//| Возвращает true, если наблюдение нужно исключить                 |
//+------------------------------------------------------------------+
bool IsEmbargoed(datetime test_bar_time,
                  datetime train_end_time,
                  int bar_seconds)
  {
   //--- переводим временной разрыв в число баров текущего периода
   long diff_bars=(long)((test_bar_time-train_end_time)/bar_seconds);

   return(diff_bars>=0 && diff_bars<m_embargo_bars);
  }

Важно: класс объявлен, но не используется ни в QuantumVQC.mqh, ни в GraphQuantumScalper.mq5. Модель обучается одним непрерывным потоком на живых и исторических барах, без разделения train/test и без embargo. Это нормально для live-трейдинга (там нет отдельного теста — каждый новый бар и есть тест для весов, обученных до него), но при офлайн-валидации на истории именно этот механизм нужно подключить вручную, иначе оценка качества модели рискует получиться оптимистичнее, чем окажется в реальности.


Идея 6: мультитаймфрейм стекинг — заявлено, но не реализовано до конца

CMultiTimeframeStacker должен держать по одной VQC-модели на каждый таймфрейм и комбинировать их прогнозы через простую логистическую мета-модель с обучаемыми (офлайн) коэффициентами:

//+------------------------------------------------------------------+
//| Объединяет прогнозы моделей нескольких таймфреймов               |
//| Возвращает итоговый сигнал в диапазоне [-1,1]                    |
//+------------------------------------------------------------------+
double GetStackedPrediction(string symbol)
  {
   double weighted_sum=m_meta_bias;

   //--- добавляем прогноз и уверенность каждой модели
   for(int i=0;i<m_count;i++)
     {
      double confidence;
      ENUM_MARKET_REGIME regime;
      double pred=m_models[i].PredictNextMove(symbol,confidence,regime);

      weighted_sum+=m_meta_weights[i*2]*pred;
      weighted_sum+=m_meta_weights[i*2+1]*confidence;
     }

   return(MathTanh(weighted_sum));
  }

Но здесь честно обозначена незакрытая часть: PredictNextMove внутри всегда читает PERIOD_CURRENT через GetFeatures, то есть даже если создать несколько моделей на разные ENUM_TIMEFRAMES, все они физически будут смотреть на один и тот же (текущий) таймфрейм графика. Это отмечено прямо в коде как TODO — GetFeatures нужно расширить явным параметром timeframe и получать данные через CopyRates(symbol, m_timeframes[i], ...) для каждой модели отдельно. Пока это не сделано, CMultiTimeframeStacker не даёт настоящего мультитаймфреймового сигнала, а фактически считает N раз один и тот же прогноз на одном таймфрейме — использовать его в текущем виде не стоит.


Как это всё связано в GraphQuantumScalper.mq5

Советник держит по одной независимо обучаемой VQC-модели на каждую из 28 валютных пар — веса не шарятся между парами, у каждой своя динамика и свой файл сохранения (vqc_weights_SYMBOL.csv). Фильтр согласования сигналов — намеренно простой: сверяется только знак прогноза VQC, без проверки на конформный порог уверенности:

//+------------------------------------------------------------------+
//| Сверяет направление VQC с сигналом валютного графа               |
//| Возвращает true, если фильтр отключён или направления совпали    |
//+------------------------------------------------------------------+
bool QuantumConfirms(int idx,int graphDirection)
  {
   //--- отключённый фильтр не блокирует исходный графовый сигнал
   if(!InpUseQuantumFilter)
      return(true);

   double pred=g_LastPrediction[idx];
   int quantumDirection=(pred>=0.0) ? 1 : -1;

   return(quantumDirection==graphDirection);
  }

Это стоит отметить отдельно как осознанный компромисс: если VQC-прогноз близок к нулю (модель фактически не уверена ни в одном направлении), знак всё равно определён и фильтр формально подтвердит сделку. Более строгий вариант — требовать не только совпадения знака, но и |prediction| выше конформного порога из m_conformal, что уже посчитано внутри PredictNextMove, но наружу для этой проверки не используется.

Обновление моделей и обучение происходит один раз на новый бар основного таймфрейма (не на каждый тик — предсказывать движение внутри ещё не закрытого бара бессмысленно), а закрытие позиций по развороту — на каждом тике, потому что это дёшево и должно реагировать быстро на возврат цены к fair value:

//+------------------------------------------------------------------+
//| Обрабатывает тики и запуск логики нового бара                    |
//| Закрытия проверяются чаще, чем обучение и новые входы            |
//+------------------------------------------------------------------+
void OnTick()
  {
   //--- обновляем граф и проверяем условия закрытия на каждом тике
   BuildRateMatrix(g_R,g_OK);
   ManageClosures(g_R,g_OK);

   //--- обучение и поиск входов выполняем один раз на новом баре
   datetime bar=iTime(Symbol(),InpTF,0);
   if(bar>0 && bar!=LastBarTime)
     {
      LastBarTime=bar;
      UpdateQuantumModels();
      FindEntries(g_R,g_OK);
     }
  }

Обучение и прогноз объединены в один проход по всем 28 парам: сначала для каждой модели регистрируется фактический исход прошлого прогноза (и делается шаг обучения), затем сразу же считается новый прогноз на следующий бар:

//--- Фрагмент UpdateQuantumModels(): цикл по торговым символам
for(int s=0;s<SYM_COUNT;s++)
  {
   ...

   //--- обучаем модель по фактическому исходу прошлого прогноза
   if(g_LastPredClose[s]>0.0)
     {
      double actual_return=(market-g_LastPredClose[s])/g_LastPredClose[s];
      double actual_move_normalized=MathTanh(actual_return*InpReturnScaleForTraining);

      QModels[s].RegisterOutcome(g_LastPrediction[s],
                                 actual_move_normalized);
     }

   //--- рассчитываем прогноз и режим для следующего бара
   double confidence;
   ENUM_MARKET_REGIME regime;
   double pred=QModels[s].PredictNextMove(sym,confidence,regime);
   ...
  }
Параметры системы
Параметр По умолчанию Комментарий
InpUseQuantumFilter true Требовать совпадения знака VQC-прогноза с направлением графа
InpUseQuantumKellySizing true Лот через Kelly вместо фикс/риск-лота
InpQuantumLookbackBars 24 Окно для расчёта признаков VQC
InpReturnScaleForTraining 80.0 Масштаб tanh(return*scale) при формировании обучающей метки
InpKellyFraction 0.25 Доля от полного критерия Келли
InpMaxHops 4 Максимальная длина пути в графовом движке fair value
InpEntryDeviationPts / InpExitDeviationPts 18 / 4 Пороги входа и выхода по отклонению от fair value
InpStopLossPts / InpTakeProfitPts 120 / 80 Защитные SL/TP — в отличие от ассоциативных правил, здесь они есть с самого начала
InpMaxOpenTrades 8 Лимит одновременных позиций советника
InpSaveWeightsEveryBars 200 Как часто сохранять обученные веса на диск















Бэктест: что реально получилось на истории

Ниже — результат прогона на EURUSD, M1, за два месяца (2026.05.01–2026.07.01) в MetaTrader 5 Strategy Tester, брокер RoboForex Ltd. Советник торгует мультивалютно: график открыт на EURUSD, но по факту в тесте участвовало 27 символов из графового движка.

Параметр запуска Значение
Символ графика / таймфрейм EURUSD, M1
Период теста 2026.05.01 – 2026.07.01
InpEntryDeviationPts / InpExitDeviationPts 35 / 5
InpStopLossPts / InpTakeProfitPts 2000 / 2000
InpUseRiskLot / InpRiskPercent true / 1%
InpUseQuantumFilter / InpUseQuantumKellySizing true / true
InpQuantumLookbackBars 1000
InpMaxOpenTrades / InpMaxSpreadPts 80 / 40
Депозит / плечо 10 000 USD / 1:500










Итоговые метрики тестера:

Метрика Значение
Всего сделок / входов (Total Trades / Deals) 614 позиций, 1228 сделок
Короткие / длинные позиции (won %) 20 sell (40.00%), 594 buy (71.21%)
Прибыльные / убыточные 431 из 614 (70.20%) прибыльных, 183 (29.80%) убыточных
Чистая прибыль 172.01 USD
Gross Profit / Gross Loss 256.92 USD / -84.91 USD
Profit Factor 3.03
Expected Payoff 0.28 USD на сделку
Recovery Factor / Sharpe Ratio 19.91 / 69.88
Z-Score -6.51 (99.74%)
Максимальная просадка по эквити / балансу 8.64 USD (0.09%) / 6.56 USD (0.07%)
Крупнейшая прибыльная / убыточная сделка 9.75 USD / -6.51 USD
Средняя прибыльная / убыточная сделка 0.60 USD / -0.46 USD
Макс. серия побед / поражений (count, $) 28 (14.24 USD) / 9 (-3.69 USD)
Время удержания позиции (мин / сред. / макс.) 0:00:20 / 0:07:52 / 11:07:40














Что сразу стоит сказать: это уже осмысленная выборка — 614 позиций за два месяца на M1, не девять и не пятьдесят одна сделка, как в статье про ассоциативные правила. Profit Factor 3.03 и распределение прибыльных/убыточных сделок (70.20% / 29.80%) выглядят правдоподобно для скальпера с узким средним удержанием в 8 минут. Но дальше начинаются вещи, которые нужно разобрать честно, а не взять на веру.

Первое: Sharpe Ratio 69.88 — это артефакт масштаба таймфрейма, а не признак выдающейся системы. MetaTrader 5 считает Sharpe по доходностям на баре текущего таймфрейма и аннуализирует его, домножая на корень из числа периодов в году. На M1 таких периодов формально сотни тысяч в год, поэтому даже скромное по факту соотношение доходность/волатильность на баре превращается в отчёте в трёхзначные величины. Sharpe выше 3 у живых квант-фондов уже считается поводом для отдельной проверки на переобучение — 69.88 нужно читать не как система в 20 раз лучше топовых фондов, а как побочный эффект того, что метрика на этом таймфрейме считается некорректно сопоставимо с годовыми Sharpe других систем. Сравнивать это число с Sharpe дневных или часовых стратегий нельзя.

Второе: Z-Score -6.51 (99.74%) сигнализирует, что последовательность выигрышей/проигрышей статистически не случайна. Этот тест в MetaTrader 5 проверяет, не выглядит ли чередование прибыльных и убыточных сделок подозрительно неслучайным (например, длинные серии побед/поражений подряд чаще, чем дал бы честный подбрасываемый результат с той же общей вероятностью выигрыша). Здесь это может быть следствием архитектуры: 27 валютных пар не независимы (USD входит почти во все), а VQC-модели по разным парам используют один и тот же признак графового residual — то есть сделки по коррелирующим парам могут открываться и закрываться скоплениями, а не независимо друг от друга. Это не обязательно плохо, но означает, что 614 сделок по факту дают меньше независимой статистической информации, чем 614 сделок по одному инструменту.

Третье: результат почти целиком держится на BUY-стороне. 594 длинные сделки против всего 20 коротких — SELL-сторона (40% из 20 сделок) статистически ничего не доказывает ни в плюс, ни в минус, выборка слишком мала. По сути этот прогон проверяет систему в условиях, где рынок за май-июнь 2026 давал в основном покупки по паттерну графового fair-value + VQC. Прежде чем считать edge устойчивым, стоит явно проверить период с обратным трендом, где SELL-сторона получит сопоставимую с BUY статистику.

Четвёртое: реальные выходы происходят почти всегда по развороту, а не по SL/TP. InpStopLossPts/InpTakeProfitPts выставлены в этом прогоне на 2000 пунктов — по сути это аварийный ограничитель, а не рабочий уровень. При среднем удержании 7-8 минут и максимальном убытке всего -6.51 USD видно, что почти все закрытия идут через ManageClosures по возврату к fair value (InpExitDeviationPts=5), а не через стоп. Это ожидаемо для скальпера такого типа, но означает, что заявленная в параметрах защита через SL пока ни разу не была протестирована на реальном хвостовом сценарии — если revert-логика перестанет срабатывать (например, на резком гэпе), это будет первый раз, когда 2000-пунктовый стоп реально определит размер убытка.

Пятое: InpQuantumLookbackBars=1000 в этом прогоне — это не то же самое, что дефолт 24 из таблицы параметров. При лукбэке в 1000 баров признаки волатильности и моментума VQC усредняются по гораздо более широкому окну, чем предполагает архитектура по умолчанию. Это стоит зафиксировать явно при воспроизведении: результат получен не на конфигурации по умолчанию, а на её изменённой версии, и сравнение с другими прогонами имеет смысл только при одинаковом лукбэке.

Итог по этому прогону: советник торгует, статистика набралась не игрушечная (614 позиций), просадка небольшая (0.09% по эквити), profit factor правдоподобный. Но Sharpe и Z-Score из отчёта нельзя цитировать буквально без пояснений выше, SELL-сторона пока не проверена, а параметры теста (лукбэк 1000, SL/TP 2000 пунктов как чисто аварийные) отличаются от дефолтных значений из таблицы. Прежде чем считать эту конфигурацию рабочей, стоит: прогнать более длинный период с разными рыночными режимами, отдельно посмотреть на статистику SELL-сделок при достаточной выборке и проверить, не создаёт ли межвалютная корреляция скрытую концентрацию риска, на которую намекает отрицательный Z-Score.


Чего в этой версии ещё не хватает

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

  1. Purge/Embargo объявлен, но не подключён. На истории обучение и тест идут одним непрерывным потоком без разделения. Для честной офлайн-валидации нужно вручную развести train/test с embargo, иначе метрики будут оптимистичнее реальности.
  2. Мультитаймфрейм стекинг не читает разные таймфреймы. Пока GetFeatures жёстко привязан к PERIOD_CURRENT, CMultiTimeframeStacker бесполезен — это открытый TODO, а не готовая к использованию идея.
  3. Реакция на обнаруженный дрейф концепции ограничена логированием. Page-Hinkley детектор сообщает о дрейфе, но не меняет ни learning rate, ни поведение входов — эффект защиты пока чисто информационный.
  4. Сайзинг по Келли опирается на нестабильную online статистику. win_loss_ratio считается по скользящему среднему прибылей/убытков конкретного советника в реальном времени — на малом числе сделок это может давать резкие скачки размера позиции.
  5. Фильтр согласования сигналов не использует конформный порог. QuantumConfirms сверяет только знак прогноза, даже если |prediction| близко к нулю — более строгая проверка через уже посчитанный conformal-порог была бы логичнее.

Ни один из этих пунктов не означает, что архитектура нерабочая — все базовые механизмы (аналитический расчёт схемы, точный parameter-shift градиент, Adam, конформная калибровка, детекция режима, purge/embargo как класс) реализованы корректно и независимо тестируемы. Но эксплуатационная связка между ними местами не закончена, и прежде чем публиковать какие-либо цифры бэктеста, эти пробелы стоит явно закрыть или явно учитывать при интерпретации результатов — ровно по той же причине, по которой на девяти, а потом на пятидесяти одной сделках наш бэктест разбирался настолько придирчиво.


Подводные камни

Переобучение квантового слова. Термин VQC здесь используется в узком, буквальном смысле — параметризованная схема вращений, которая честно считается на CPU. Никакого квантового ускорения, никакой связи с реальным квантовым железом в этой реализации нет, и выдавать её за что-то большее было бы недобросовестно.

Обучение в реальном времени — не панацея от переобучения. Онлайн-обучение с задержкой в один бар защищает от заглядывания в будущее внутри live-цикла, но не защищает от того, что модель может настроиться на короткий локальный участок рынка и забыть через несколько сотен баров то, что работало раньше — особенно с learning rate 0.03 и Adam, которые довольно агрессивно подстраиваются.

Сайзинг по Келли без стабильной статистики опасен. Полный Kelly уже сам по себе агрессивен; Kelly, посчитанный на плохо оценённых p и b, может давать лот, не соответствующий реальному риску сделки. Доля 0.25 — разумная подстраховка, но не гарантия.

Связанность признаков графа и VQC. Раз признак 8 — это отклонение от той же fair value, что используется для входа, VQC отчасти учится предсказывать отработает ли графовый сигнал, а не независимую вещь. Это осознанное архитектурное решение (два слоя усиливают друг друга), но его стоит учитывать при интерпретации: это не два независимых голоса, а один сигнал плюс его самопроверка.


Куда расширять

Подключить Purge/Embargo к офлайн-валидации. Реализовать полноценный walk-forward скрипт для Strategy Tester или отдельный офлайн-раннер, который использует CPurgeEmbargo при разбиении истории на train/test.

Достроить мультитаймфрейм. Добавить параметр timeframe в GetFeatures и CopyRates, чтобы CMultiTimeframeStacker действительно агрегировал независимые прогнозы с разных таймфреймов, а не дублировал один и тот же.

Дать Page-Hinkley реальные полномочия. При срабатывании дрейфа — временно увеличивать learning rate тренера, снижать Kelly-фракцию или приостанавливать новые входы до нескольких подтверждающих баров без дрейфа.

Ужесточить QuantumConfirms. Требовать не только совпадения знака прогноза с графом, но и превышения conformal-порога уверенности — благо он уже считается внутри PredictNextMove.


Что в итоге

Это уже не про счёт частот, как в статье с ассоциативными правилами, а про параметризованную нелинейную функцию, обучаемую прямо в терминале точным аналитическим градиентом без какого-либо офлайн-этапа. Сильная сторона — честная математика: parameter-shift rule даёт точный, не приближённый градиент, а Adam поверх него — тот же алгоритм, что используется в серьёзных ML-фреймворках. Слабая сторона — то, что часть заявленных в комментариях защит (purge/embargo, реакция на дрейф, мультитаймфрейм) реализована как готовый класс, но не доведена до работающей связки внутри советника.

Первый прогон на 614 сделках (EURUSD M1, май–июнь 2026) показал правдоподобный Profit Factor 3.03 при просадке 0.09% — но Sharpe 69.88 и Z-Score -6.51 из отчёта нельзя принимать на веру буквально: первое — артефакт масштаба M1-таймфрейма в формуле MetaTrader 5, второе — сигнал возможной статистической зависимости между сделками по коррелирующим валютным парам. Прежде чем доверять этой системе реальные деньги, эти пробелы стоит закрыть, а результат — как и в случае с ассоциативными правилами — проверять на длинной истории с честным walk-forward и отдельной проверкой SELL-стороны, а не на первом же прогоне, который выглядит красиво.

Название файла Описание файла
QuantumVQC.mqh Библиотека: аналитическая VQC-модель, онлайн-обучение через parameter-shift + Adam, конформная калибровка, детекция режима, отбор фичей, Kelly sizing, детекция дрейфа, purge/embargo, мультитаймфрейм стекинг
QuantumVQC2.mq5 Советник: графовый fair-value скальпер на 28 парах с VQC-подтверждением направления и сайзингом по Келли
Прикрепленные файлы |
QuantumVQC2.mq5 (31.91 KB)
QuantumVQC.mqh (55.39 KB)
Автоматизация торговых стратегий в MQL5 (Часть 43): Адаптивная стратегия на основе канала линейной регрессии Автоматизация торговых стратегий в MQL5 (Часть 43): Адаптивная стратегия на основе канала линейной регрессии
В этой статье мы реализуем в MQL5 адаптивную систему на основе канала линейной регрессии. Она автоматически рассчитывает линию регрессии и канал стандартного отклонения за заданный пользователем период, активируется только тогда, когда абсолютное значение наклона превышает минимальный порог, что подтверждает наличие выраженного тренда, а также динамически перестраивает или продлевает канал, когда цена выходит за его границы на настраиваемую долю ширины.
Алгоритм направления потока — Flow Direction Algorithm (FDA) Алгоритм направления потока — Flow Direction Algorithm (FDA)
Подробно разбираем механику FDA и демонстрируем, как центровые и диагональные эффекты первоисточника завышают показатели. Честная реализация фиксирует уровень порядка 39.7% на стенде серии. Модификация FDAm с тяжелохвостым распределением зондов и покоординатным шагом повышает итог до 54.7% и доступна как готовый компонент C_AO для настройки параметров советников.
Моделирование рынка: Position View (XII) Моделирование рынка: Position View (XII)
В этой статье вы научитесь создавать визуальный сигнал на своей торговой платформе, чтобы прямо на графике определять, является ли позиция длинной или короткой, без необходимости обращаться к терминалу. Кроме того, в тексте рассматривается реализация функциональности, которая улучшает отображение при перемещении линий тейк-профита и стоп-лосса, скрывая горизонтальную линию, следующую за курсором мыши, во время перемещения этих линий во избежание путаницы. Чтение предлагает практические знания для настройки систем моделирования рынка.
Автоматизация торговых стратегий в MQL5 (Часть 42): Сессионная система пробоя начального диапазона (ORB) Автоматизация торговых стратегий в MQL5 (Часть 42): Сессионная система пробоя начального диапазона (ORB)
В статье мы создадим гибко настраиваемую сессионную систему Opening Range Breakout (ORB) на MQL5. Она позволяет задавать время начала сессии и продолжительность начального диапазона, автоматически рассчитывает его максимум и минимум и открывает сделки только после подтверждённого пробоя в направлении движения.