//+------------------------------------------------------------------+
//|                                                   QuantumVQC.mqh |
//|                                Copyright 2026, Evgeniy Koshtenko |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Evgeniy Koshtenko"
#property link      "https://www.mql5.com"
#property strict
#property version   "1.00"
#property description "Graph fair-value scalper + Quantum VQC confirmation & Kelly sizing + live online learning"

//--- Базовые константы
#define NUM_QUBITS        3
#define NUM_STATES        8      // 2^NUM_QUBITS
#define FEATURES_COUNT    8      // было 7, добавлена фича graph fair-value residual (идея 8)
#define HOUR_LOOKBACK     1000
#define WINSORIZE_PCT     0.05

//+------------------------------------------------------------------+
//| Идея 10: Page-Hinkley тест на дрейф концепции (легче ADWIN,      |
//| не требует хранить окно — подходит для MQL5 без доп. памяти)     |
//+------------------------------------------------------------------+
class CPageHinkley
  {
private:
   double            m_mean_error;
   double            m_cumulative_sum;
   double            m_min_cumulative_sum;
   int               m_n;
   double            m_delta;      // допустимое отклонение (нечувствительность к шуму)
   double            m_lambda;     // порог срабатывания
public:
                     CPageHinkley(double delta = 0.005, double lambda = 0.05)
     {
      m_delta = delta;
      m_lambda = lambda;
      Reset();
     }
   void              Reset()
     {
      m_mean_error = 0.0;
      m_cumulative_sum = 0.0;
      m_min_cumulative_sum = 0.0;
      m_n = 0;
     }
   //--- error — абсолютная ошибка прогноза (|prediction_sign*move - actual|), подавай на каждом баре
   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, PH=", DoubleToString(ph, 5),
               " after ", m_n, " observations. Retraining of w[]/b[] weights is required.");
         Reset(); // сбрасываем после срабатывания
         return(true);
        }
      return(false);
     }
  };

//+------------------------------------------------------------------+
//| Идея 5: детекция режима волатильности (упрощённый HMM-подобный   |
//| классификатор на процентилях волатильности + сила тренда).       |
//| Полноценный Baum-Welch EM тяжело держать в реалтайме MQL5,       |
//| поэтому используем скользящие процентили + гистерезис            |
//| переключения состояний, что даёт похожий эффект без runtime ML.  |
//+------------------------------------------------------------------+
enum ENUM_MARKET_REGIME
  {
   REGIME_LOW_VOL = 0,
   REGIME_NORMAL  = 1,
   REGIME_HIGH_VOL= 2,
   REGIME_TREND   = 3
  };

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CRegimeDetector
  {
private:
   double            m_vol_history[];
   int               m_history_size;
   ENUM_MARKET_REGIME m_current_regime;
   int               m_hysteresis_count;   // сколько баров подряд сигнал держится, прежде чем переключить режим
   int               m_confirm_bars;
public:
                     CRegimeDetector(int history_size = 500, int confirm_bars = 3)
     {
      m_history_size = history_size;
      m_confirm_bars = confirm_bars;
      m_hysteresis_count = 0;
      m_current_regime = REGIME_NORMAL;
      ArrayResize(m_vol_history, 0);
     }

   void              PushVolatility(double vol)
     {
      int size = ArraySize(m_vol_history);
      if(size >= m_history_size)
         ArrayRemove(m_vol_history, 0, 1);
      ArrayResize(m_vol_history, MathMin(size + 1, m_history_size));
      m_vol_history[ArraySize(m_vol_history) - 1] = vol;
     }

   //--- trend_strength — например |momentum| нормализованный в [0,1]
   ENUM_MARKET_REGIME Classify(double current_vol, double trend_strength)
     {
      int n = ArraySize(m_vol_history);
      if(n < 30)
         return(REGIME_NORMAL); // недостаточно истории

      double sorted[];
      ArrayResize(sorted, n);
      ArrayCopy(sorted, m_vol_history);
      ArraySort(sorted);

      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;

      //--- Гистерезис: переключаем состояние только если кандидат стабилен confirm_bars раз подряд
      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);
     }

   //--- Множитель порога уверенности по режиму: в HIGH_VOL требуем больше уверенности,
   //--- в TREND — доверяем сигналу сильнее (понижаем порог)
   double            GetConfidenceMultiplier() const
     {
      switch(m_current_regime)
        {
         case REGIME_HIGH_VOL:
            return(2.0);
         case REGIME_LOW_VOL:
            return(1.3);
         case REGIME_TREND:
            return(0.7);
         default:
            return(1.0);
        }
     }

   ENUM_MARKET_REGIME GetCurrentRegime() const { return(m_current_regime); }

   string            RegimeToString() const
     {
      switch(m_current_regime)
        {
         case REGIME_LOW_VOL:
            return("LOW_VOL");
         case REGIME_HIGH_VOL:
            return("HIGH_VOL");
         case REGIME_TREND:
            return("TREND");
         default:
            return("NORMAL");
        }
     }
  };

//+------------------------------------------------------------------+
//| Идея 4: Conformal prediction calibration.                        |
//| Держим окно nonconformity scores (|prediction - realized_move|)  |
//| и берём эмпирический квантиль как порог — вместо статичной       |
//| MIN_CONFIDENCE, порог адаптируется к реальному распределению     |
//| ошибок и даёт формальное покрытие (coverage guarantee).          |
//+------------------------------------------------------------------+
class CConformalCalibrator
  {
private:
   double            m_scores[];
   int               m_max_size;
   double            m_confidence_level; // например 0.90 => 90% покрытие
public:
                     CConformalCalibrator(int max_size = 500, double confidence_level = 0.90)
     {
      m_max_size = max_size;
      m_confidence_level = confidence_level;
      ArrayResize(m_scores, 0);
     }

   void              AddScore(double nonconformity_score)
     {
      int size = ArraySize(m_scores);
      if(size >= m_max_size)
         ArrayRemove(m_scores, 0, 1);
      ArrayResize(m_scores, MathMin(size + 1, m_max_size));
      m_scores[ArraySize(m_scores) - 1] = MathAbs(nonconformity_score);
     }

   //--- Возвращает динамический порог уверенности вместо статичного MIN_CONFIDENCE
   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]);
     }

   int               GetSampleCount() const { return(ArraySize(m_scores)); }
  };

//+------------------------------------------------------------------+
//| Идея 7: Информационно-теоретический отбор фичей.                 |
//| Держим скользящую корреляционную матрицу и подавляем (обнуляем   |
//| вес) фичи, которые сильно коррелируют с уже "включённой" фичой — |
//| простой greedy аналог VIF-фильтрации в реальном времени.         |
//+------------------------------------------------------------------+
class CFeatureSelector
  {
private:
   double            m_feature_history[][FEATURES_COUNT];
   int               m_count;
   int               m_max_history;
   double            m_corr_threshold;
public:
                     CFeatureSelector(int max_history = 300, double corr_threshold = 0.85)
     {
      m_max_history = max_history;
      m_corr_threshold = corr_threshold;
      m_count = 0;
      ArrayResize(m_feature_history, 0);
     }

   void              PushFeatures(const double &features[])
     {
      int size = ArrayRange(m_feature_history, 0);
      if(size >= m_max_history)
        {
         //--- сдвигаем окно
         for(int i = 0; i < size - 1; i++)
            for(int j = 0; j < FEATURES_COUNT; j++)
               m_feature_history[i][j] = m_feature_history[i+1][j];
         for(int j = 0; j < FEATURES_COUNT; j++)
            m_feature_history[size-1][j] = features[j];
        }
      else
        {
         ArrayResize(m_feature_history, size + 1);
         for(int j = 0; j < FEATURES_COUNT; j++)
            m_feature_history[size][j] = features[j];
        }
      m_count = ArrayRange(m_feature_history, 0);
     }

   double            PearsonCorr(int idx_a, int idx_b) const
     {
      if(m_count < 20)
         return(0.0);
      double mean_a = 0.0, mean_b = 0.0;
      for(int i = 0; i < m_count; i++)
        {
         mean_a += m_feature_history[i][idx_a];
         mean_b += m_feature_history[i][idx_b];
        }
      mean_a /= m_count;
      mean_b /= m_count;

      double cov = 0.0, var_a = 0.0, var_b = 0.0;
      for(int i = 0; i < m_count; i++)
        {
         double da = m_feature_history[i][idx_a] - mean_a;
         double db = m_feature_history[i][idx_b] - mean_b;
         cov += da * db;
         var_a += da * da;
         var_b += db * db;
        }
      if(var_a < 1e-12 || var_b < 1e-12)
         return(0.0);
      return(cov / MathSqrt(var_a * var_b));
     }

   //--- Возвращает маску: true = фича активна, false = подавлена как избыточная
   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)
              {
               //--- подавляем вторую фичу из коррелирующей пары (greedy)
               active_mask[j] = false;
              }
           }
        }
     }
  };

//+------------------------------------------------------------------+
//| Идея 9: Kelly-based position sizing на основе величины prediction|
//+------------------------------------------------------------------+
class CKellySizer
  {
private:
   double            m_fraction;      // доля от полного Kelly (обычно 0.25-0.5 для снижения риска)
   double            m_max_lot;
   double            m_min_lot;
public:
                     CKellySizer(double kelly_fraction = 0.25, double min_lot = 0.01, double max_lot = 10.0)
     {
      m_fraction = kelly_fraction;
      m_min_lot = min_lot;
      m_max_lot = max_lot;
     }

   //--- prediction в [-1,1], win_loss_ratio — средний профит/средний убыток по истории,
   //--- base_lot — базовый лот при полной уверенности
   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));
     }
  };

//+------------------------------------------------------------------+
//| Идея 3: Purge/Embargo helper для walk-forward валидации.         |
//| Не позволяет тренировочному и тестовому окну соприкасаться,      |
//| убирая look-ahead и serial-correlation утечку (Lopez de Prado).  |
//+------------------------------------------------------------------+
class CPurgeEmbargo
  {
private:
   int               m_embargo_bars;
public:
                     CPurgeEmbargo(int embargo_bars = 24) { m_embargo_bars = embargo_bars; }

   //--- 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);
     }
  };

//+------------------------------------------------------------------+
//| Идея 8: Интерфейс для внешнего graph fair-value residual         |
//| (подключается из GraphCycleFairValueScalper — DFS multi-hop граф)|
//+------------------------------------------------------------------+
class IFairValueProvider
  {
public:
   virtual double    GetFairValueResidual(string symbol) = 0; // (price - fair_value)/fair_value
  };

//--- Forward declaration — CQuantumForex объявлен полностью ниже, а CVQCOnlineTrainer
//--- нужно знать о его существовании уже сейчас (для указателя в параметрах методов).
//--- Тела методов, которые реально ВЫЗЫВАЮТ методы CQuantumForex, вынесены за пределы
//--- класса (реализация после полного объявления CQuantumForex) — иначе компилятор
//--- MQL5 не сможет разрешить вызовы model.GetWeight()/model.SetWeight() и т.п.
class CQuantumForex;

//+------------------------------------------------------------------+
//| РЕАЛЬНОЕ ОБУЧЕНИЕ ПРЯМО В MQL5 — без Python, без offline экспорта|
//|                                                                  |
//| Используем parameter-shift rule — ТОЧНУЮ (не приближённую!)      |
//| формулу градиента для гейтов вида RY(θ), которые как раз и стоят |
//| в энкодинге фичей:                                               |
//|                                                                  |
//|     dE/dθ = 0.5 * ( E(θ + π/2) - E(θ - π/2) )                    |
//|                                                                  |
//| Это стандартный результат из quantum ML (Mitarai et al. 2018,    |
//| Schuld et al. 2019) — для однокубитных поворотов с генератором   |
//| собственных значений ±1/2 эта формула даёт РОВНО тот же градиент,|
//| что и аналитическое дифференцирование, без всякой аппроксимации. |
//| Дальше — обычный Adam поверх этого точного градиента.            |
//+------------------------------------------------------------------+
class CVQCOnlineTrainer
  {
private:
   double            m_m_w[FEATURES_COUNT], m_v_w[FEATURES_COUNT];   // Adam моменты для весов w[i]
   double            m_m_b[FEATURES_COUNT], m_v_b[FEATURES_COUNT];   // Adam моменты для смещений b[i]
   int               m_t;              // шаг Adam (для bias-correction)
   double            m_lr;
   double            m_beta1, m_beta2, m_eps;
   double            m_loss_history[];
   int               m_max_loss_history;
   double            m_grad_clip;      // защита от взрыва градиента на малых feat

public:
                     CVQCOnlineTrainer(double lr = 0.03, double beta1 = 0.9, double beta2 = 0.999,
                     double eps = 1e-8, double grad_clip = 5.0)
     {
      m_lr = lr;
      m_beta1 = beta1;
      m_beta2 = beta2;
      m_eps = eps;
      m_t = 0;
      m_grad_clip = grad_clip;
      m_max_loss_history = 300;
      ArrayInitialize(m_m_w, 0.0);
      ArrayInitialize(m_v_w, 0.0);
      ArrayInitialize(m_m_b, 0.0);
      ArrayInitialize(m_v_b, 0.0);
      ArrayResize(m_loss_history, 0);
     }

   double            Clip(double x) { return(MathMax(-m_grad_clip, MathMin(m_grad_clip, x))); }

   //--- Один шаг НАСТОЯЩЕГО онлайн-обучения. Вызывается на каждом баре, когда
   //--- становится известен фактический исход (target). model — указатель на
   //--- CQuantumForex, у которого прямо здесь будут изменены m_weights/m_biases.
   //--- target — нормализованное фактическое движение цены в [-1,1]
   //--- (например MathTanh(actual_return * scale)).
   //--- Реализация — ниже в файле, после полного объявления класса CQuantumForex.
   double            TrainStep(CQuantumForex *model, double &features[], bool &active_mask[], double target);

   double            GetAverageLoss() const
     {
      int n = ArraySize(m_loss_history);
      if(n == 0)
         return(0.0);
      double s = 0;
      for(int i = 0; i < n; i++)
         s += m_loss_history[i];
      return(s / n);
     }

   int               GetStepCount() const { return(m_t); }

   //--- Сохранить текущее состояние обученных весов на диск (можно перезапускать
   //--- терминал без потери обучения — при старте вызови model.LoadWeights())
   //--- Реализация — ниже в файле, после полного объявления класса CQuantumForex.
   bool              SaveWeights(CQuantumForex *model, string filename);
  };

//+------------------------------------------------------------------+
//| Основной класс: аналитический (без Monte-Carlo) VQC с обучаемыми |
//| весами, которые крутятся ЖИВЬЁМ в терминале через                |
//| CVQCOnlineTrainer (идеи 1 и 2, теперь по-настоящему обучаемые)   |
//+------------------------------------------------------------------+
class CQuantumForex
  {
private:
   int               m_lookback_bars;
   double            m_features[];
   double            m_returns[];
   datetime          m_dates[];

   //--- Идея 1: обучаемые параметры энкодинга (по одному w,b на фичу).
   //--- Дефолты w=1,b=0 — стартовая точка, ДАЛЬШЕ они реально крутятся
   //--- в реальном времени через CVQCOnlineTrainer (parameter-shift + Adam),
   //--- никакого Python/offline экспорта не требуется.
   double            m_weights[FEATURES_COUNT];
   double            m_biases[FEATURES_COUNT];
   bool              m_weights_loaded;

   //--- Живое онлайн-обучение прямо в терминале
   CVQCOnlineTrainer *m_trainer;
   double            m_last_features[FEATURES_COUNT];
   bool              m_last_active_mask[FEATURES_COUNT];
   bool              m_has_pending_prediction; // есть ли прогноз, ждущий подтверждения исходом

   //--- Идея 9,7,5,4,10 — модули
   CFeatureSelector    *m_feature_selector;
   CRegimeDetector     *m_regime_detector;
   CConformalCalibrator*m_conformal;
   CPageHinkley        *m_drift_detector;
   CKellySizer         *m_kelly;

   IFairValueProvider  *m_fair_value_provider; // идея 8, может быть NULL

   double            MIN_CONFIDENCE_FALLBACK;

   double            CalculateVolatility(const double &returns[], int period)
     {
      if(ArraySize(returns) < period || period <= 0)
         return(0);
      double mean = 0;
      for(int i = 0; i < period; i++)
         mean += returns[i];
      mean /= period;
      double variance = 0;
      for(int i = 0; i < period; i++)
         variance += MathPow(returns[i] - mean, 2);
      variance /= period;
      return(MathSqrt(variance * 252.0));
     }

   double            CalculateMomentum(const double &returns[], int period)
     {
      if(ArraySize(returns) < period || period <= 0)
         return(0);
      double sum = 0;
      for(int i = 0; i < period; i++)
         sum += returns[i];
      return(sum / period);
     }

   double            GetWeightedVote(const double &state_probs[])
     {
      double up_votes = 0.0, down_votes = 0.0, 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);
     }

   double            RobustNormalize(double value, const double &data[], int period)
     {
      if(ArraySize(data) < period || period <= 0)
         return(0.0);
      double temp[];
      ArrayResize(temp, period);
      ArrayCopy(temp, data, 0, 0, period);
      ArraySort(temp);
      int lower_idx = (int)(period * WINSORIZE_PCT);
      int upper_idx = MathMin(period - 1, (int)(period * (1.0 - WINSORIZE_PCT)));
      double lower_bound = temp[lower_idx];
      double upper_bound = temp[upper_idx];
      value = MathMax(MathMin(value, upper_bound), lower_bound);
      double mean = 0.0;
      for(int i = 0; i < period; i++)
         mean += temp[i];
      mean /= period;
      double variance = 0.0;
      for(int i = 0; i < period; i++)
         variance += MathPow(temp[i] - mean, 2);
      variance /= period;
      double std_dev = MathSqrt(variance);
      if(std_dev < 1e-6)
         return(0.0);
      double z = (value - mean) / std_dev;
      return(MathTanh(z));
     }

   //+------------------------------------------------------------------+
   //| GetFeatures — как в v4, плюс фича 8 (graph fair value residual)   |
   //+------------------------------------------------------------------+
   void              GetFeatures(string symbol, double &features[])
     {
      MqlRates rates[];
      ArraySetAsSeries(rates, true);
      int copied = CopyRates(symbol, PERIOD_CURRENT, 0, m_lookback_bars + 1, rates);

      ArrayResize(features, FEATURES_COUNT);
      ArrayInitialize(features, 0.0);

      if(copied <= 0 || copied < m_lookback_bars + 1)
        {
         Print("ERROR/WARNING: not enough data for ", symbol, " copied=", copied);
         return;
        }

      ArrayResize(m_returns, m_lookback_bars);
      int max_returns = MathMin(m_lookback_bars, copied - 1);
      for(int i = 0; i < max_returns; i++)
        {
         if(rates[i+1].close <= 0)
           {
            m_returns[i] = 0.0;
            continue;
           }
         m_returns[i] = (rates[i].close - rates[i+1].close) / rates[i+1].close;
        }
      for(int i = max_returns; i < m_lookback_bars; i++)
         m_returns[i] = 0.0;

      ArrayResize(m_dates, m_lookback_bars);
      int max_dates = MathMin(m_lookback_bars, copied);
      for(int i = 0; i < max_dates; i++)
         m_dates[i] = rates[i].time;
      for(int i = max_dates; i < m_lookback_bars; i++)
         m_dates[i] = (max_dates > 0) ? rates[max_dates-1].time : TimeCurrent();

      //--- 1. Price range
      double price_ranges[];
      ArrayResize(price_ranges, m_lookback_bars);
      for(int i = 0; i < max_dates; i++)
         price_ranges[i] = (rates[i].close > 0) ? (rates[i].high - rates[i].low) / rates[i].close : 0.0;
      for(int i = max_dates; i < m_lookback_bars; i++)
         price_ranges[i] = 0.0;
      features[0] = RobustNormalize(price_ranges[0], price_ranges, m_lookback_bars);

      //--- 2. Volatility
      double volatilities[];
      ArrayResize(volatilities, m_lookback_bars);
      for(int i = 0; i < m_lookback_bars; i++)
        {
         int window_size = MathMin(12, m_lookback_bars - i);
         if(window_size <= 0)
           {
            volatilities[i] = 0.0;
            continue;
           }
         double temp_returns[];
         ArrayResize(temp_returns, window_size);
         int copy_size = MathMin(window_size, ArraySize(m_returns) - i);
         if(copy_size > 0)
           {
            ArrayCopy(temp_returns, m_returns, 0, i, copy_size);
            for(int j = copy_size; j < window_size; j++)
               temp_returns[j] = 0.0;
           }
         else
            ArrayInitialize(temp_returns, 0.0);
         volatilities[i] = CalculateVolatility(temp_returns, window_size);
        }
      features[1] = RobustNormalize(volatilities[0], volatilities, m_lookback_bars);

      //--- 3. Momentum
      double momenta[];
      ArrayResize(momenta, m_lookback_bars);
      for(int i = 0; i < m_lookback_bars; i++)
        {
         int window_size = MathMin(24, m_lookback_bars - i);
         if(window_size <= 0)
           {
            momenta[i] = 0.0;
            continue;
           }
         double temp_returns[];
         ArrayResize(temp_returns, window_size);
         int copy_size = MathMin(window_size, ArraySize(m_returns) - i);
         if(copy_size > 0)
           {
            ArrayCopy(temp_returns, m_returns, 0, i, copy_size);
            for(int j = copy_size; j < window_size; j++)
               temp_returns[j] = 0.0;
           }
         else
            ArrayInitialize(temp_returns, 0.0);
         momenta[i] = CalculateMomentum(temp_returns, window_size);
        }
      features[2] = RobustNormalize(momenta[0], momenta, m_lookback_bars);

      //--- 4-5. Time
      MqlDateTime dt;
      TimeToStruct(rates[0].time, dt);
      features[3] = MathSin(2 * M_PI * dt.hour / 24.0);
      features[4] = MathCos(2 * M_PI * dt.hour / 24.0);

      //--- 6. Price deviation from SMA
      double price_deviations[];
      ArrayResize(price_deviations, m_lookback_bars);
      for(int i = 0; i < m_lookback_bars; i++)
        {
         int window_size = MathMin(24, m_lookback_bars - i);
         if(window_size <= 0 || i >= max_dates)
           {
            price_deviations[i] = 0.0;
            continue;
           }
         double prices[];
         ArrayResize(prices, window_size);
         int valid_prices = 0;
         for(int j = 0; j < window_size && (i + j) < max_dates; j++)
            if(rates[i+j].close > 0)
              {
               prices[valid_prices] = rates[i+j].close;
               valid_prices++;
              }
         if(valid_prices == 0)
           {
            price_deviations[i] = 0.0;
            continue;
           }
         ArrayResize(prices, valid_prices);
         double sma = 0;
         for(int j = 0; j < valid_prices; j++)
            sma += prices[j];
         sma /= valid_prices;
         price_deviations[i] = (sma > 0 && rates[i].close > 0) ? (rates[i].close - sma) / sma : 0.0;
        }
      for(int i = max_dates; i < m_lookback_bars; i++)
         price_deviations[i] = 0.0;
      features[5] = RobustNormalize(price_deviations[0], price_deviations, m_lookback_bars);

      //--- 7. Latest return
      features[6] = RobustNormalize(m_returns[0], m_returns, m_lookback_bars);

      //--- 8. (Идея 8) Graph fair-value residual — из внешнего DFS-графа кросс-валютных циклов
      if(m_fair_value_provider != NULL)
         features[7] = MathTanh(m_fair_value_provider.GetFairValueResidual(symbol));
      else
         features[7] = 0.0; // без провайдера фича неактивна
     }

   //+------------------------------------------------------------------+
   //| Идеи 1+2: аналитический расчёт квантового состояния,             |
   //| обучаемые углы поворота, без MathRand()/shots                    |
   //+------------------------------------------------------------------+
   double            ComputeQuantumStateAnalytic(double &features[], bool &active_mask[])
     {
      double state[NUM_STATES];
      ArrayInitialize(state, 0.0);
      state[0] = 1.0; // |000>

      //--- Encoding: используем обучаемые w[i], b[i]; фичи, подавленные фильтром
      //--- корреляции (идея 7), пропускаются (angle = 0 => identity rotation).
      //--- ИСПРАВЛЕНО: раньше цикл шёл по qubit (0..NUM_QUBITS-1=0..2), и
      //--- feature_idx = qubit % ArraySize(features) физически мог быть только
      //--- 0, 1 или 2 — фичи 3..7 (sin/cos часа, отклонение от SMA, последний
      //--- return и, что важнее всего, фича 8 = graph fair-value residual)
      //--- вообще НЕ участвовали в схеме, хотя вычислялись и упоминались как
      //--- используемые. Теперь цикл идёт по всем FEATURES_COUNT фичам, каждая
      //--- крутит "свой" кубит (feature_idx % NUM_QUBITS) — при NUM_QUBITS=3 на
      //--- кубит 0 приходятся фичи 0,3,6; на кубит 1 — фичи 1,4,7 (в т.ч. graph
      //--- residual); на кубит 2 — фичи 2,5. Parameter-shift по каждому w[i]/b[i]
      //--- остаётся точным: угол конкретной фичи всё ещё θ_i=(feat_i*w_i+b_i)*π,
      //--- сдвиг w_i двигает только гейт этой фичи на её кубите.
      int n_features = ArraySize(features);
      for(int feature_idx = 0; feature_idx < n_features; feature_idx++)
        {
         int qubit = feature_idx % NUM_QUBITS;
         double raw = features[feature_idx];
         if(!active_mask[feature_idx])
            raw = 0.0; // фича подавлена как избыточная

         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];
              }
           }
        }

      //--- Entanglement. ИСПРАВЛЕНО: раньше fi/fj = features[i]/features[j] с
      //--- i,j — индексами кубитов (0..2), то есть тоже неявно смотрели только
      //--- на фичи 0..2. Теперь берём агрегат (сумму активных фичей) по каждому
      //--- кубиту — тем самым перепутывание тоже учитывает все 8 фичей, а не
      //--- только первые три.
      double qubit_feature[NUM_QUBITS];
      ArrayInitialize(qubit_feature, 0.0);
      for(int feature_idx = 0; feature_idx < n_features; feature_idx++)
        {
         if(!active_mask[feature_idx])
            continue;
         qubit_feature[feature_idx % NUM_QUBITS] += features[feature_idx];
        }
      for(int i = 0; i < NUM_QUBITS - 1; i++)
        {
         for(int j = i + 1; j < NUM_QUBITS; j++)
           {
            double phase = M_PI/2.0 * (qubit_feature[i] * qubit_feature[j]);
            for(int k = 0; k < NUM_STATES; k++)
               if(((k & (1 << i)) != 0) && ((k & (1 << j)) != 0))
                  state[k] *= MathCos(phase);
           }
        }

      //--- Final rotations
      double balance_angle = M_PI/4.0;
      for(int qubit = 0; qubit < NUM_QUBITS; qubit++)
        {
         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(balance_angle) * temp - MathSin(balance_angle) * state[i1];
               state[i1] = MathSin(balance_angle) * temp + MathCos(balance_angle) * state[i1];
              }
           }
        }

      //--- Нормировка
      double total_prob = 0.0;
      for(int i = 0; i < NUM_STATES; i++)
         total_prob += state[i] * state[i];
      double norm_factor = MathSqrt(total_prob);
      if(norm_factor > 0.0)
         for(int i = 0; i < NUM_STATES; i++)
            state[i] /= norm_factor;

      //--- Идея 2: аналитические вероятности state[i]^2 напрямую, без сэмплирования
      double state_probs[NUM_STATES];
      for(int i = 0; i < NUM_STATES; i++)
         state_probs[i] = state[i] * state[i];

      return(GetWeightedVote(state_probs));
     }

public:
                     CQuantumForex(int lookback = 24)
     {
      m_lookback_bars = lookback;
      m_weights_loaded = false;
      m_fair_value_provider = NULL;
      MIN_CONFIDENCE_FALLBACK = 0.0005;

      //--- Дефолтные веса эквивалентны исходному поведению (w=1, b=0)
      for(int i = 0; i < FEATURES_COUNT; i++)
        {
         m_weights[i] = 1.0;
         m_biases[i] = 0.0;
        }

      m_feature_selector = new CFeatureSelector();
      m_regime_detector  = new CRegimeDetector();
      m_conformal        = new CConformalCalibrator();
      m_drift_detector   = new CPageHinkley();
      m_kelly            = new CKellySizer();
      m_trainer          = new CVQCOnlineTrainer(); // реальное обучение прямо тут

      m_has_pending_prediction = false;
      ArrayInitialize(m_last_features, 0.0);
      for(int i = 0; i < FEATURES_COUNT; i++)
         m_last_active_mask[i] = true;
     }

                    ~CQuantumForex()
     {
      ArrayFree(m_features);
      ArrayFree(m_returns);
      ArrayFree(m_dates);
      delete m_feature_selector;
      delete m_regime_detector;
      delete m_conformal;
      delete m_drift_detector;
      delete m_kelly;
      delete m_trainer;
     }

   //+------------------------------------------------------------------+
   //| Публичный доступ к весам — нужен тренеру для parameter-shift:     |
   //| он сдвигает вес, считает предсказание, сдвигает в другую сторону, |
   //| считает снова, восстанавливает — так добывается точный градиент. |
   //+------------------------------------------------------------------+
   double            GetWeight(int idx) const { return((idx >= 0 && idx < FEATURES_COUNT) ? m_weights[idx] : 0.0); }
   double            GetBias(int idx)   const { return((idx >= 0 && idx < FEATURES_COUNT) ? m_biases[idx]  : 0.0); }
   void              SetWeight(int idx, double val) { if(idx >= 0 && idx < FEATURES_COUNT) m_weights[idx] = val; }
   void              SetBias(int idx, double val)   { if(idx >= 0 && idx < FEATURES_COUNT) m_biases[idx]  = val; }

   //--- Сырое предсказание по уже готовым фичам — без регимов/калибровки/логов,
   //--- именно то, что нужно тренеру для многократных вызовов при вычислении градиента
   double            ComputeRawPrediction(double &features[], bool &active_mask[])
     {
      return(ComputeQuantumStateAnalytic(features, active_mask));
     }

   //--- Идея 8: подключить внешний провайдер fair-value (например обёртку над
   //--- GraphCycleFairValueScalper). Может не вызываться — тогда фича 8 = 0.
   void              SetFairValueProvider(IFairValueProvider *provider) { m_fair_value_provider = provider; }

   //--- Идея 1: загрузка обученных offline (Python/PennyLane) весов из CSV:
   //--- строки "index,weight,bias", по одной на фичу (0..FEATURES_COUNT-1)
   bool              LoadWeights(string filename)
     {
      int handle = FileOpen(filename, FILE_READ | FILE_CSV | FILE_ANSI, ',');
      if(handle == INVALID_HANDLE)
        {
         Print("LoadWeights: cannot open ", filename, ", default weights are used (w=1,b=0)");
         return(false);
        }
      while(!FileIsEnding(handle))
        {
         int idx = (int)FileReadNumber(handle);
         double w = FileReadNumber(handle);
         double b = FileReadNumber(handle);
         if(idx >= 0 && idx < FEATURES_COUNT)
           {
            m_weights[idx] = w;
            m_biases[idx] = b;
           }
        }
      FileClose(handle);
      m_weights_loaded = true;
      Print("LoadWeights: trained VQC weights loaded from ", filename);
      return(true);
     }

   bool              Initialize() { return(true); }

   //--- Основной прогноз: собирает фичи, применяет отбор фичей, режим,
   //--- аналитический VQC, конформную калибровку
   double            PredictNextMove(string symbol, double &out_confidence, ENUM_MARKET_REGIME &out_regime)
     {
      double features[];
      GetFeatures(symbol, features);

      //--- Идея 7: обновляем историю и получаем маску активных фичей
      m_feature_selector.PushFeatures(features);
      bool active_mask[];
      m_feature_selector.GetActiveMask(active_mask);

      //--- Идея 5: обновляем регим волатильности
      m_regime_detector.PushVolatility(MathAbs(features[1]));
      ENUM_MARKET_REGIME regime = m_regime_detector.Classify(MathAbs(features[1]), MathAbs(features[2]));
      out_regime = regime;

      double prediction = ComputeQuantumStateAnalytic(features, active_mask);
      double confidence = MathAbs(prediction);
      out_confidence = confidence;

      //--- Запоминаем фичи и маску этого прогноза — понадобятся для реального
      //--- онлайн-обучения весов, как только станет известен фактический исход
      for(int i = 0; i < FEATURES_COUNT; i++)
        {
         m_last_features[i] = features[i];
         m_last_active_mask[i] = active_mask[i];
        }
      m_has_pending_prediction = true;

      //--- Идея 4: динамический порог через conformal calibration вместо статичного MIN_CONFIDENCE
      double base_threshold = m_conformal.GetThreshold(MIN_CONFIDENCE_FALLBACK);
      double adjusted_threshold = base_threshold * m_regime_detector.GetConfidenceMultiplier();

      if(confidence < adjusted_threshold)
        {
         Print("Not enough confidence (", DoubleToString(confidence,5),
               " < ", DoubleToString(adjusted_threshold,5), "), regime=", m_regime_detector.RegimeToString());
         return(0.0);
        }
      return(prediction);
     }

   //--- Вызывать после того, как известен реальный исход (закрылся бар и видно
   //--- фактическое движение). Делает СРАЗУ ТРИ вещи:
   //---   1) калибрует conformal-порог (идея 4)
   //---   2) проверяет concept drift через Page-Hinkley (идея 10)
   //---   3) РЕАЛЬНО обучает веса w[]/b[] прямо здесь, через parameter-shift + Adam —
   //---      никакого Python, никакого offline экспорта, обучение живёт в терминале
   //--- actual_move_normalized — фактическое движение, приведённое к [-1,1]
   //--- тем же способом, что и prediction (например MathTanh(actual_return*scale))
   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 the Page-Hinkley test. Online learning "
               "keeps updating the weights, learning rate of CVQCOnlineTrainer "
               "can be increased for faster adaptation.");
        }

      if(m_has_pending_prediction)
        {
         double loss = m_trainer.TrainStep(GetPointer(this), m_last_features, m_last_active_mask,
                                           actual_move_normalized);
         if(m_trainer.GetStepCount() % 50 == 0)
           {
            Print("VQC online training: step=", m_trainer.GetStepCount(),
                  " current loss=", DoubleToString(loss, 6),
                  " average loss(300)=", DoubleToString(m_trainer.GetAverageLoss(), 6));
           }
         m_has_pending_prediction = false;
        }
     }

   //--- Периодически сохраняй состояние обученных весов на диск, чтобы не терять
   //--- прогресс обучения при перезапуске терминала (грузится обратно LoadWeights())
   bool              SaveTrainedWeights(string filename)
     {
      return(m_trainer.SaveWeights(GetPointer(this), filename));
     }

   double            GetTrainingAverageLoss() const { return(m_trainer.GetAverageLoss()); }
   int               GetTrainingStepCount()   const { return(m_trainer.GetStepCount()); }

   //--- Идея 9: получить размер позиции по Kelly на основе последнего прогноза
   double            GetKellyLotSize(double prediction, double historical_win_loss_ratio, double base_lot)
     {
      return(m_kelly.GetLotSize(prediction, historical_win_loss_ratio, base_lot));
     }

   void              SetLookbackBars(int bars)
     {
      if(bars >= 10 && bars <= 1000)
        {
         m_lookback_bars = bars;
         Print("Lookback updated to ", bars, " bars");
        }
     }
   int               GetLookbackBars() const { return(m_lookback_bars); }
   bool              AreWeightsLoaded() const { return(m_weights_loaded); }
  };

//+------------------------------------------------------------------+
//| Реализация методов CVQCOnlineTrainer, которые вызывают методы    |
//| CQuantumForex. Вынесены сюда (за пределы тела класса             |
//| CVQCOnlineTrainer), потому что на момент объявления самого класса|
//| CQuantumForex был известен только как forward declaration —      |
//| компилятор не мог видеть его методы (GetWeight/SetWeight/        |
//| ComputeRawPrediction). Теперь, когда CQuantumForex полностью     |
//| объявлен выше, это компилируется без проблем — обычная практика  |
//| разруливания циклической зависимости двух классов в MQL5/C++.    |
//+------------------------------------------------------------------+
double CVQCOnlineTrainer::TrainStep(CQuantumForex *model, double &features[], bool &active_mask[], double target)
  {
   m_t++;
   double pred_before = model.ComputeRawPrediction(features, active_mask);
   double error = pred_before - target;      // dL/dPred для L=(pred-target)^2 (множитель 2 свёрнут в lr)
   double loss  = error * error;

   for(int i = 0; i < FEATURES_COUNT; i++)
     {
      double feat = active_mask[i] ? features[i] : 0.0;
      double w = model.GetWeight(i);
      double b = model.GetBias(i);

      //--- --- Градиент по w[i] через parameter-shift ---
      //--- θ = (feat*w + b)*π  =>  dθ/dw = feat*π
      //--- Чтобы сдвинуть θ ровно на ±π/2, сдвигаем w на ±1/(2*feat)
      double grad_w = 0.0;
      if(MathAbs(feat) > 1e-9)
        {
         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 dPred_dTheta = 0.5 * (pred_plus - pred_minus); // ТОЧНЫЙ градиент, не приближение
         grad_w = dPred_dTheta * feat * M_PI;                  // цепное правило dθ/dw = feat*π
        }
      //--- если feat≈0, угол не зависит от w вообще — градиент честно 0, а не NaN

      //--- --- Градиент по b[i] через parameter-shift ---
      //--- dθ/db = π всегда (не зависит от feat) => сдвиг b на ±0.5 даёт ровно ±π/2 по θ
      double shift_b = 0.5;
      model.SetBias(i, b + shift_b);
      double pred_plus_b = model.ComputeRawPrediction(features, active_mask);
      model.SetBias(i, b - shift_b);
      double pred_minus_b = model.ComputeRawPrediction(features, active_mask);
      model.SetBias(i, b); // восстановили

      double dPred_dTheta_b = 0.5 * (pred_plus_b - pred_minus_b);
      double grad_b = dPred_dTheta_b * M_PI;

      double loss_grad_w = Clip(error * grad_w);
      double loss_grad_b = Clip(error * grad_b);

      //--- --- Adam update для w[i] ---
      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));
      model.SetWeight(i, w - m_lr * m_hat_w / (MathSqrt(v_hat_w) + m_eps));

      //--- --- Adam update для b[i] ---
      m_m_b[i] = m_beta1 * m_m_b[i] + (1.0 - m_beta1) * loss_grad_b;
      m_v_b[i] = m_beta2 * m_v_b[i] + (1.0 - m_beta2) * loss_grad_b * loss_grad_b;
      double m_hat_b = m_m_b[i] / (1.0 - MathPow(m_beta1, m_t));
      double v_hat_b = m_v_b[i] / (1.0 - MathPow(m_beta2, m_t));
      model.SetBias(i, b - m_lr * m_hat_b / (MathSqrt(v_hat_b) + m_eps));
     }

   int size = ArraySize(m_loss_history);
   if(size >= m_max_loss_history)
      ArrayRemove(m_loss_history, 0, 1);
   ArrayResize(m_loss_history, MathMin(size + 1, m_max_loss_history));
   m_loss_history[ArraySize(m_loss_history) - 1] = loss;

   return(loss);
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CVQCOnlineTrainer::SaveWeights(CQuantumForex *model, string filename)
  {
   int handle = FileOpen(filename, FILE_WRITE | FILE_CSV | FILE_ANSI, ',');
   if(handle == INVALID_HANDLE)
      return(false);
   for(int i = 0; i < FEATURES_COUNT; i++)
      FileWrite(handle, i, model.GetWeight(i), model.GetBias(i));
   FileClose(handle);
   return(true);
  }

//+------------------------------------------------------------------+
//| Идея 6: мультитаймфрейм стекинг.                                 |
//| Держит по одному CQuantumForex на каждый ТФ, комбинирует прогнозы|
//| через мета-модель (логрегрессия с обучаемыми offline коэфф-ами). |
//+------------------------------------------------------------------+
class CMultiTimeframeStacker
  {
private:
   CQuantumForex     *m_models[];
   ENUM_TIMEFRAMES   m_timeframes[];
   int               m_count;

   //--- Мета-модель: логрегрессия. Коэффициенты обучаются offline на
   //--- истории (prediction_tf1..N, confidence_tf1..N) -> realized_outcome.
   double            m_meta_weights[];   // размер = 2*m_count (pred + confidence на каждый ТФ)
   double            m_meta_bias;

   double            Sigmoid(double x) { return(1.0 / (1.0 + MathExp(-x))); }

public:
                     CMultiTimeframeStacker() { m_count = 0; m_meta_bias = 0.0; }

                    ~CMultiTimeframeStacker()
     {
      for(int i = 0; i < m_count; i++)
         delete m_models[i];
     }

   void              AddTimeframe(ENUM_TIMEFRAMES tf, int lookback = 24)
     {
      ArrayResize(m_models, m_count + 1);
      ArrayResize(m_timeframes, m_count + 1);
      m_models[m_count] = new CQuantumForex(lookback);
      m_models[m_count].Initialize();
      m_timeframes[m_count] = tf;
      m_count++;

      ArrayResize(m_meta_weights, m_count * 2);
      //--- дефолт: равновзвешенное среднее прогнозов, confidence как модификатор
      for(int i = 0; i < m_count * 2; i++)
         m_meta_weights[i] = 1.0 / m_count;
     }

   //--- Загрузка обученных offline коэффициентов мета-модели (логрегрессия / GBM-linearized)
   bool              LoadMetaWeights(string filename)
     {
      int handle = FileOpen(filename, FILE_READ | FILE_CSV | FILE_ANSI, ',');
      if(handle == INVALID_HANDLE)
         return(false);
      int i = 0;
      while(!FileIsEnding(handle) && i < ArraySize(m_meta_weights))
        {
         m_meta_weights[i] = FileReadNumber(handle);
         i++;
        }
      if(!FileIsEnding(handle))
         m_meta_bias = FileReadNumber(handle);
      FileClose(handle);
      return(true);
     }

   //--- Возвращает финальный стекнутый сигнал в [-1,1]
   double            GetStackedPrediction(string symbol)
     {
      double weighted_sum = m_meta_bias;
      for(int i = 0; i < m_count; i++)
        {
         //--- Примечание: PredictNextMove читает PERIOD_CURRENT внутри GetFeatures —
         //--- для полноценной мультитаймфреймовости нужно расширить GetFeatures
         //--- явным параметром timeframe (оставлено как TODO интеграции с CopyRates(symbol, m_timeframes[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;
        }
      //--- Приводим к [-1,1] через tanh(sigmoid-подобное сжатие)
      return(MathTanh(weighted_sum));
     }

   int               GetTimeframeCount() const { return(m_count); }
  };

//+------------------------------------------------------------------+
//| Вспомогательные функции                                          |
//+------------------------------------------------------------------+
int CountBits(int n)
  {
   int count = 0;
   while(n != 0)
     {
      count += n & 1;
      n >>= 1;
     }
   return(count);
  }

//+------------------------------------------------------------------+
//| PrintStateDistribution                                           |
//+------------------------------------------------------------------+
void PrintStateDistribution(const double &state_probs[])
  {
   Print("\n=== Final Quantum State (analytic) ===");
   double total_prob = 0;
   for(int i = 0; i < NUM_STATES; i++)
     {
      string binary = "";
      for(int b = 2; b >= 0; b--)
         binary += ((i >> b) & 1) ? "1" : "0";
      Print(StringFormat("State |%s>: %.4f", binary, state_probs[i]));
      total_prob += state_probs[i];
     }
   Print("Total Probability:", DoubleToString(total_prob, 4));
  }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
