//+------------------------------------------------------------------+
//|                                                 SignalHMMGRU.mqh |
//|                Hidden Markov regime signal with an optional GRU |
//+------------------------------------------------------------------+
#include <Expert\ExpertSignal.mqh>
#include <Indicators\Trend.mqh>
#include <Indicators\Oscilators.mqh>

// wizard description start
//+------------------------------------------------------------------+
//| Description of the class                                         |
//| Title=Signals of Hidden Markov regimes with optional GRU direction|
//| Type=SignalAdvanced                                              |
//| Name=HMM GRU                                                     |
//| ShortName=HMMGRU                                                 |
//| Class=CSignalHMMGRU                                              |
//| Page=signal_hmm_gru                                              |
//| Parameter=ADXPeriod,int,14,ADX averaging period                  |
//| Parameter=ATRPeriod,int,14,ATR averaging period                  |
//| Parameter=SignalMode,int,0,Signal mode from 0 through 6          |
//| Parameter=UseGRU,bool,true,Confirm algorithm entries with GRU    |
//| Parameter=HMMWindow,int,80,HMM rolling observation window        |
//| Parameter=HMMIterations,int,6,Baum-Welch iterations              |
//| Parameter=MinRegimeProbability,double,0.50,Minimum regime posterior |
//| Parameter=TrendADXLevel,double,25.0,ADX trend threshold          |
//| Parameter=VolatilityZLevel,double,0.75,ATR volatility z threshold |
//| Parameter=PriceWindow,int,20,Price and ATR pattern window        |
//| Parameter=RangeATRDistance,double,0.75,Range excursion inputs ATR    |
//| Parameter=GRUTrainBars,int,160,Closed bars for initial GRU fit   |
//| Parameter=GRUSequence,int,10,GRU regime sequence length          |
//| Parameter=GRUEpochs,int,3,Initial GRU training epochs            |
//| Parameter=GRULearningRate,double,0.01,GRU learning rate          |
//| Parameter=GRUMinConfidence,double,0.15,Minimum absolute GRU vote |
//| Parameter=GRUWeight,double,0.35,GRU share of confirmed vote      |
//+------------------------------------------------------------------+
// wizard description end

#define HMMGRU_STATES    3
#define HMMGRU_OBS       2
#define HMMGRU_INPUTS    5
#define HMMGRU_HIDDEN    6
#define HMMGRU_PI        3.14159265358979323846
#define HMMGRU_EPS       1.0e-12

//+------------------------------------------------------------------+
//| Semantic HMM states.                                             |
//+------------------------------------------------------------------+
enum ENUM_HMMGRU_REGIME
  {
   HMMGRU_REGIME_RANGE    = 0,
   HMMGRU_REGIME_TREND    = 1,
   HMMGRU_REGIME_VOLATILE = 2
  };

//+------------------------------------------------------------------+
//| Selectable entry algorithms.                                     |
//+------------------------------------------------------------------+
enum ENUM_HMMGRU_SIGNAL_MODE
  {
   HMMGRU_MODE_TREND_CONTINUATION = 0,
   HMMGRU_MODE_DI_CROSSOVER       = 1,
   HMMGRU_MODE_RANGE_REVERSION    = 2,
   HMMGRU_MODE_VOLATILITY_BREAK   = 3,
   HMMGRU_MODE_COMPRESSION_BREAK  = 4,
   HMMGRU_MODE_PRICE_ACTION       = 5,
   HMMGRU_MODE_ADAPTIVE           = 6
  };

//+------------------------------------------------------------------+
//| Class CSignalHMMGRU.                                             |
//|                                                                  |
//| ADX supplies trend strength and directional imbalance. ATR       |
//| supplies volatility magnitude. A three-state Gaussian HMM maps   |
//| those observations to range, trend, or volatile regimes.         |
//|                                                                  |
//| In algorithm-only mode, the selected mode returns the entry      |
//| vote. In algorithm-plus-network mode, one of three regime-specific|
//| GRUs must agree with that vote. The GRU is fitted only on closed  |
//| bars assigned to its own regime.                                 |
//+------------------------------------------------------------------+
class CSignalHMMGRU : public CExpertSignal
  {
protected:
   //--- indicator objects
   CiADX             m_adx;
   CiATR             m_atr;

   //--- indicator and signal settings
   int               m_adx_period;
   int               m_atr_period;
   int               m_signal_mode;
   bool              m_use_gru;
   int               m_hmm_window;
   int               m_hmm_iterations;
   double            m_min_regime_probability;
   double            m_trend_adx_level;
   double            m_volatility_z_level;
   int               m_price_window;
   double            m_range_atr_distance;

   //--- GRU settings
   int               m_gru_train_bars;
   int               m_gru_sequence;
   int               m_gru_epochs;
   double            m_gru_learning_rate;
   double            m_gru_min_confidence;
   double            m_gru_weight;

   //--- cached live state
   bool              m_cache_valid;
   datetime          m_cached_time;
   double            m_cached_open;
   double            m_cached_high;
   double            m_cached_low;
   double            m_cached_close;
   double            m_cached_score;
   double            m_cached_algorithm_score;
   double            m_cached_gru_score;
   int               m_current_regime;
   double            m_current_regime_probability;
   double            m_regime_probability[HMMGRU_STATES];

   //--- GRU lifecycle information
   bool              m_gru_ready;
   datetime          m_last_trained_bar;
   double            m_gru_training_loss;

   //--- one independent GRU parameter set per semantic regime
   double            m_wz[HMMGRU_STATES][HMMGRU_HIDDEN][HMMGRU_INPUTS];
   double            m_uz[HMMGRU_STATES][HMMGRU_HIDDEN][HMMGRU_HIDDEN];
   double            m_bz[HMMGRU_STATES][HMMGRU_HIDDEN];
   double            m_wr[HMMGRU_STATES][HMMGRU_HIDDEN][HMMGRU_INPUTS];
   double            m_ur[HMMGRU_STATES][HMMGRU_HIDDEN][HMMGRU_HIDDEN];
   double            m_br[HMMGRU_STATES][HMMGRU_HIDDEN];
   double            m_wh[HMMGRU_STATES][HMMGRU_HIDDEN][HMMGRU_INPUTS];
   double            m_uh[HMMGRU_STATES][HMMGRU_HIDDEN][HMMGRU_HIDDEN];
   double            m_bh[HMMGRU_STATES][HMMGRU_HIDDEN];
   double            m_wo[HMMGRU_STATES][HMMGRU_HIDDEN];
   double            m_bo[HMMGRU_STATES];

public:
                     CSignalHMMGRU(void);
                    ~CSignalHMMGRU(void);

   //--- Wizard-adjustable parameters
   void              ADXPeriod(int value)                    { m_adx_period = value;                 }
   void              ATRPeriod(int value)                    { m_atr_period = value;                 }
   void              SignalMode(int value)                   { m_signal_mode = value;                }
   void              UseGRU(bool value)                      { m_use_gru = value;                    }
   void              HMMWindow(int value)                    { m_hmm_window = value;                 }
   void              HMMIterations(int value)                { m_hmm_iterations = value;             }
   void              MinRegimeProbability(double value)      { m_min_regime_probability = value;     }
   void              TrendADXLevel(double value)             { m_trend_adx_level = value;            }
   void              VolatilityZLevel(double value)          { m_volatility_z_level = value;         }
   void              PriceWindow(int value)                  { m_price_window = value;               }
   void              RangeATRDistance(double value)          { m_range_atr_distance = value;         }
   void              GRUTrainBars(int value)                 { m_gru_train_bars = value;             }
   void              GRUSequence(int value)                  { m_gru_sequence = value;               }
   void              GRUEpochs(int value)                    { m_gru_epochs = value;                 }
   void              GRULearningRate(double value)           { m_gru_learning_rate = value;          }
   void              GRUMinConfidence(double value)          { m_gru_min_confidence = value;         }
   void              GRUWeight(double value)                 { m_gru_weight = value;                 }

   //--- state inspection
   int               CurrentRegime(void)               const { return(m_current_regime);           }
   double            CurrentRegimeProbability(void)    const { return(m_current_regime_probability);}
   double            AlgorithmScore(void)              const { return(m_cached_algorithm_score);   }
   double            GRUScore(void)                    const { return(m_cached_gru_score);         }
   double            GRUTrainingLoss(void)             const { return(m_gru_training_loss);       }

   //--- standard CExpertSignal interface
   virtual bool      ValidationSettings(void);
   virtual bool      InitIndicators(CIndicators *indicators);
   virtual int       LongCondition(void);
   virtual int       ShortCondition(void);

protected:
   //--- initialization
   bool              InitADX(CIndicators *indicators, const int buffer_size);
   bool              InitATR(CIndicators *indicators, const int buffer_size);
   bool              ResizePriceSeries(const int buffer_size);

   //--- indicator and price helpers
   double            ADX(const int index)               { return(m_adx.Main(index));  }
   double            PlusDI(const int index)            { return(m_adx.Plus(index));  }
   double            MinusDI(const int index)           { return(m_adx.Minus(index)); }
   double            ATR(const int index)               { return(m_atr.Main(index));  }
   bool              IsUsable(const double value);
   bool              DataReady(const int first, const int count);
   double            Clamp(const double value, const double lower, const double upper);
   double            Clamp01(const double value);
   double            SignOf(const double value);
   double            Stronger(const double first, const double second);
   double            DirectionalIndex(const int index);
   double            ATRZScore(const int index, const int lookback);
   double            MeanClose(const int index, const int lookback);
   double            HighestPriorHigh(const int index, const int lookback);
   double            LowestPriorLow(const int index, const int lookback);

   //--- HMM estimation
   double            GaussianEmission(const double x0, const double x1,
                                      const double mean0, const double mean1,
                                      const double variance0, const double variance1);
   bool              EstimateRegime(const int offset, int &regime, double &probability,
                                    double &posterior[]);
   double            RegimeGate(const int expected_regime);

   //--- seven separate algorithm modes
   double            ModeTrendContinuation(const int index);
   double            ModeDICrossover(const int index);
   double            ModeRangeReversion(const int index);
   double            ModeVolatilityBreakout(const int index);
   double            ModeCompressionBreakout(const int index);
   double            ModePriceAction(const int index);
   double            ModeAdaptive(const int index);
   double            SelectAlgorithmMode(const int index);

   //--- GRU functions
   void              InitializeGRUWeights(void);
   double            InitialWeight(const int regime, const int gate,
                                   const int row, const int column);
   double            Sigmoid(const double value);
   double            ClipGradient(const double value);
   bool              BuildGRUFeatures(const int index, double &inputs[]);
   bool              GRUForward(const int regime, const double &inputs[],
                                double &hidden[], double &output);
   double            GRUTrainStep(const int regime, const double &inputs[],
                                  double &hidden[], const double target,
                                  const double learning_rate);
   bool              TrainGRUWindow(const int anchor, const int bars,
                                    const int epochs, const double learning_rate);
   bool              PredictGRU(const int index, const int active_regime, double &score);
   double            FuseWithGRU(const double algorithm_score, const double gru_score);

   //--- complete signal calculation and caching
   bool              SameCachedObservation(const int index);
   bool              CalculateSignal(void);
  };

//+------------------------------------------------------------------+
//| Constructor.                                                     |
//+------------------------------------------------------------------+
CSignalHMMGRU::CSignalHMMGRU(void) :
   m_adx_period(14),
   m_atr_period(14),
   m_signal_mode(HMMGRU_MODE_TREND_CONTINUATION),
   m_use_gru(true),
   m_hmm_window(80),
   m_hmm_iterations(6),
   m_min_regime_probability(0.50),
   m_trend_adx_level(25.0),
   m_volatility_z_level(0.75),
   m_price_window(20),
   m_range_atr_distance(0.75),
   m_gru_train_bars(160),
   m_gru_sequence(10),
   m_gru_epochs(3),
   m_gru_learning_rate(0.01),
   m_gru_min_confidence(0.15),
   m_gru_weight(0.35),
   m_cache_valid(false),
   m_cached_time(0),
   m_cached_open(EMPTY_VALUE),
   m_cached_high(EMPTY_VALUE),
   m_cached_low(EMPTY_VALUE),
   m_cached_close(EMPTY_VALUE),
   m_cached_score(0.0),
   m_cached_algorithm_score(0.0),
   m_cached_gru_score(0.0),
   m_current_regime(HMMGRU_REGIME_RANGE),
   m_current_regime_probability(0.0),
   m_gru_ready(false),
   m_last_trained_bar(0),
   m_gru_training_loss(0.0)
  {
   m_used_series = USE_SERIES_OPEN + USE_SERIES_HIGH + USE_SERIES_LOW +
                   USE_SERIES_CLOSE + USE_SERIES_TIME;
   for(int state = 0; state < HMMGRU_STATES; state++)
      m_regime_probability[state] = 0.0;
   InitializeGRUWeights();
  }

//+------------------------------------------------------------------+
//| Destructor.                                                      |
//+------------------------------------------------------------------+
CSignalHMMGRU::~CSignalHMMGRU(void)
  {
  }

//+------------------------------------------------------------------+
//| Validate all adjustable settings.                                |
//+------------------------------------------------------------------+
bool CSignalHMMGRU::ValidationSettings(void)
  {
   if(!CExpertSignal::ValidationSettings())
      return(false);
   if(m_adx_period < 2 || m_atr_period < 2)
     {
      Print(__FUNCTION__ + ": ADXPeriod and ATRPeriod must be at least 2");
      return(false);
     }
   if(m_signal_mode < HMMGRU_MODE_TREND_CONTINUATION ||
      m_signal_mode > HMMGRU_MODE_ADAPTIVE)
     {
      Print(__FUNCTION__ + ": SignalMode must be between 0 and 6");
      return(false);
     }
   if(m_hmm_window < 30 || m_hmm_window > 250)
     {
      Print(__FUNCTION__ + ": HMMWindow must be between 30 and 250");
      return(false);
     }
   if(m_hmm_iterations < 1 || m_hmm_iterations > 20)
     {
      Print(__FUNCTION__ + ": HMMIterations must be between 1 and 20");
      return(false);
     }
   if(m_min_regime_probability < 0.34 || m_min_regime_probability > 0.95)
     {
      Print(__FUNCTION__ + ": MinRegimeProbability must be inputs [0.34,0.95]");
      return(false);
     }
   if(m_trend_adx_level < 5.0 || m_trend_adx_level > 60.0)
     {
      Print(__FUNCTION__ + ": TrendADXLevel must be inputs [5,60]");
      return(false);
     }
   if(m_volatility_z_level < 0.0 || m_volatility_z_level > 3.0)
     {
      Print(__FUNCTION__ + ": VolatilityZLevel must be inputs [0,3]");
      return(false);
     }
   if(m_price_window < 5 || m_price_window > 100)
     {
      Print(__FUNCTION__ + ": PriceWindow must be between 5 and 100");
      return(false);
     }
   if(m_range_atr_distance < 0.10 || m_range_atr_distance > 5.0)
     {
      Print(__FUNCTION__ + ": RangeATRDistance must be inputs [0.10,5.0]");
      return(false);
     }
   if(m_gru_train_bars < 40 || m_gru_train_bars > 800)
     {
      Print(__FUNCTION__ + ": GRUTrainBars must be between 40 and 800");
      return(false);
     }
   if(m_gru_sequence < 2 || m_gru_sequence > 32)
     {
      Print(__FUNCTION__ + ": GRUSequence must be between 2 and 32");
      return(false);
     }
   if(m_gru_epochs < 1 || m_gru_epochs > 20)
     {
      Print(__FUNCTION__ + ": GRUEpochs must be between 1 and 20");
      return(false);
     }
   if(m_gru_learning_rate <= 0.0 || m_gru_learning_rate > 0.10)
     {
      Print(__FUNCTION__ + ": GRULearningRate must be inputs (0,0.10]");
      return(false);
     }
   if(m_gru_min_confidence < 0.0 || m_gru_min_confidence > 1.0)
     {
      Print(__FUNCTION__ + ": GRUMinConfidence must be inputs [0,1]");
      return(false);
     }
   if(m_gru_weight < 0.0 || m_gru_weight > 1.0)
     {
      Print(__FUNCTION__ + ": GRUWeight must be inputs [0,1]");
      return(false);
     }
   return(true);
  }

//+------------------------------------------------------------------+
//| Create the price series and both indicators.                     |
//+------------------------------------------------------------------+
bool CSignalHMMGRU::InitIndicators(CIndicators *indicators)
  {
   if(indicators == NULL)
      return(false);
   if(!CExpertSignal::InitIndicators(indicators))
      return(false);
   int buffer_size = m_hmm_window + m_price_window + 32;
   if(m_use_gru)
      buffer_size += m_gru_train_bars + m_gru_sequence;
   if(!ResizePriceSeries(buffer_size))
      return(false);
   if(!InitADX(indicators, buffer_size))
      return(false);
   if(!InitATR(indicators, buffer_size))
      return(false);
   m_cache_valid = false;
   m_gru_ready = false;
   m_last_trained_bar = 0;
   return(true);
  }

//+------------------------------------------------------------------+
//| Enlarge the shared standard-library price buffers.               |
//+------------------------------------------------------------------+
bool CSignalHMMGRU::ResizePriceSeries(const int buffer_size)
  {
   if(m_open == NULL || m_high == NULL || m_low == NULL ||
      m_close == NULL || m_time == NULL)
     {
      Print(__FUNCTION__ + ": required price series is not available");
      return(false);
     }
   if(m_open.BufferSize() < buffer_size && !m_open.BufferResize(buffer_size))
     {
      Print(__FUNCTION__ + ": cannot resize the open-price buffer");
      return(false);
     }
   if(m_high.BufferSize() < buffer_size && !m_high.BufferResize(buffer_size))
     {
      Print(__FUNCTION__ + ": cannot resize the high-price buffer");
      return(false);
     }
   if(m_low.BufferSize() < buffer_size && !m_low.BufferResize(buffer_size))
     {
      Print(__FUNCTION__ + ": cannot resize the low-price buffer");
      return(false);
     }
   if(m_close.BufferSize() < buffer_size && !m_close.BufferResize(buffer_size))
     {
      Print(__FUNCTION__ + ": cannot resize the close-price buffer");
      return(false);
     }
   if(m_time.BufferSize() < buffer_size && !m_time.BufferResize(buffer_size))
     {
      Print(__FUNCTION__ + ": cannot resize the time buffer");
      return(false);
     }
   return(true);
  }

//+------------------------------------------------------------------+
//| Create ADX.                                                      |
//+------------------------------------------------------------------+
bool CSignalHMMGRU::InitADX(CIndicators *indicators, const int buffer_size)
  {
   if(!indicators.Add(GetPointer(m_adx)))
     {
      Print(__FUNCTION__ + ": cannot add ADX to the indicator collection");
      return(false);
     }
   if(!m_adx.Create(m_symbol.Name(), m_period, m_adx_period))
     {
      Print(__FUNCTION__ + ": cannot create ADX");
      return(false);
     }
   if(!m_adx.BufferResize(buffer_size))
     {
      Print(__FUNCTION__ + ": cannot resize the ADX buffer");
      return(false);
     }
   return(true);
  }

//+------------------------------------------------------------------+
//| Create ATR.                                                      |
//+------------------------------------------------------------------+
bool CSignalHMMGRU::InitATR(CIndicators *indicators, const int buffer_size)
  {
   if(!indicators.Add(GetPointer(m_atr)))
     {
      Print(__FUNCTION__ + ": cannot add ATR to the indicator collection");
      return(false);
     }
   if(!m_atr.Create(m_symbol.Name(), m_period, m_atr_period))
     {
      Print(__FUNCTION__ + ": cannot create ATR");
      return(false);
     }
   if(!m_atr.BufferResize(buffer_size))
     {
      Print(__FUNCTION__ + ": cannot resize the ATR buffer");
      return(false);
     }
   return(true);
  }

//+------------------------------------------------------------------+
//| Numeric validity check.                                          |
//+------------------------------------------------------------------+
bool CSignalHMMGRU::IsUsable(const double value)
  {
   return(value != EMPTY_VALUE && MathIsValidNumber(value));
  }

//+------------------------------------------------------------------+
//| Verify that a complete observation window is available.          |
//+------------------------------------------------------------------+
bool CSignalHMMGRU::DataReady(const int first, const int count)
  {
   if(first < 0 || count < 1)
      return(false);
   int last = first + count - 1;
   if(last >= m_open.BufferSize() || last >= m_high.BufferSize() ||
      last >= m_low.BufferSize() || last >= m_close.BufferSize() ||
      last >= m_adx.BufferSize() || last >= m_atr.BufferSize())
      return(false);
   if(!IsUsable(Open(first)) || !IsUsable(High(first)) ||
      !IsUsable(Low(first)) || !IsUsable(Close(first)) ||
      !IsUsable(ADX(first)) || !IsUsable(PlusDI(first)) ||
      !IsUsable(MinusDI(first)) || !IsUsable(ATR(first)))
      return(false);
   if(!IsUsable(Close(last)) || !IsUsable(ADX(last)) ||
      !IsUsable(PlusDI(last)) || !IsUsable(MinusDI(last)) ||
      !IsUsable(ATR(last)))
      return(false);
   return(true);
  }

//+------------------------------------------------------------------+
//| Clamp a number.                                                  |
//+------------------------------------------------------------------+
double CSignalHMMGRU::Clamp(const double value, const double lower,
                            const double upper)
  {
   if(value < lower)
      return(lower);
   if(value > upper)
      return(upper);
   return(value);
  }

//+------------------------------------------------------------------+
//| Clamp to [0,1].                                                  |
//+------------------------------------------------------------------+
double CSignalHMMGRU::Clamp01(const double value)
  {
   return(Clamp(value, 0.0, 1.0));
  }

//+------------------------------------------------------------------+
//| Return the sign of a value.                                      |
//+------------------------------------------------------------------+
double CSignalHMMGRU::SignOf(const double value)
  {
   if(value > 0.0)
      return(1.0);
   if(value < 0.0)
      return(-1.0);
   return(0.0);
  }

//+------------------------------------------------------------------+
//| Return the score with the greater absolute magnitude.            |
//+------------------------------------------------------------------+
double CSignalHMMGRU::Stronger(const double first, const double second)
  {
   if(MathAbs(first) >= MathAbs(second))
      return(first);
   return(second);
  }

//+------------------------------------------------------------------+
//| Normalized +DI/-DI imbalance.                                    |
//+------------------------------------------------------------------+
double CSignalHMMGRU::DirectionalIndex(const int index)
  {
   double plus = PlusDI(index);
   double minus = MinusDI(index);
   if(!IsUsable(plus) || !IsUsable(minus))
      return(0.0);
   return(Clamp((plus - minus) / (plus + minus + HMMGRU_EPS), -1.0, 1.0));
  }

//+------------------------------------------------------------------+
//| ATR percentage z-score using only the current and older bars.    |
//+------------------------------------------------------------------+
double CSignalHMMGRU::ATRZScore(const int index, const int lookback)
  {
   if(lookback < 5 || !DataReady(index, lookback))
      return(EMPTY_VALUE);
   double mean = 0.0;
   int valid = 0;
   for(int i = 0; i < lookback; i++)
     {
      double close = Close(index + i);
      double atr  = ATR(index + i);
      if(!IsUsable(close) || !IsUsable(atr) || MathAbs(close) <= HMMGRU_EPS)
         continue;
      mean += atr / MathAbs(close);
      valid++;
     }
   if(valid < 5)
      return(EMPTY_VALUE);
   mean /= valid;
   double variance = 0.0;
   for(int i = 0; i < lookback; i++)
     {
      double close = Close(index + i);
      double atr  = ATR(index + i);
      if(!IsUsable(close) || !IsUsable(atr) || MathAbs(close) <= HMMGRU_EPS)
         continue;
      double value = atr / MathAbs(close);
      variance += (value - mean) * (value - mean);
     }
   variance /= valid;
   double deviation = MathSqrt(MathMax(variance, HMMGRU_EPS));
   double current = ATR(index) / MathAbs(Close(index));
   return(Clamp((current - mean) / deviation, -4.0, 4.0));
  }

//+------------------------------------------------------------------+
//| Simple close-price mean.                                         |
//+------------------------------------------------------------------+
double CSignalHMMGRU::MeanClose(const int index, const int lookback)
  {
   if(lookback < 1 || !DataReady(index, lookback))
      return(EMPTY_VALUE);
   double total = 0.0;
   for(int i = 0; i < lookback; i++)
     {
      double value = Close(index + i);
      if(!IsUsable(value))
         return(EMPTY_VALUE);
      total += value;
     }
   return(total / lookback);
  }

//+------------------------------------------------------------------+
//| Highest high before the analyzed bar.                            |
//+------------------------------------------------------------------+
double CSignalHMMGRU::HighestPriorHigh(const int index, const int lookback)
  {
   if(lookback < 1 || !DataReady(index + 1, lookback))
      return(EMPTY_VALUE);
   double result = High(index + 1);
   for(int i = 2; i <= lookback; i++)
      if(High(index + i) > result)
         result = High(index + i);
   return(result);
  }

//+------------------------------------------------------------------+
//| Lowest low before the analyzed bar.                              |
//+------------------------------------------------------------------+
double CSignalHMMGRU::LowestPriorLow(const int index, const int lookback)
  {
   if(lookback < 1 || !DataReady(index + 1, lookback))
      return(EMPTY_VALUE);
   double result = Low(index + 1);
   for(int i = 2; i <= lookback; i++)
      if(Low(index + i) < result)
         result = Low(index + i);
   return(result);
  }

//+------------------------------------------------------------------+
//| Diagonal two-dimensional Gaussian density.                       |
//+------------------------------------------------------------------+
double CSignalHMMGRU::GaussianEmission(const double x0, const double x1,
                                       const double mean0, const double mean1,
                                       const double variance0,
                                       const double variance1)
  {
   double var0 = MathMax(variance0, 0.0025);
   double var1 = MathMax(variance1, 0.0500);
   double d0 = x0 - mean0;
   double d1 = x1 - mean1;
   double exponent = -0.5 * (d0 * d0 / var0 + d1 * d1 / var1);
   exponent = MathMax(exponent, -60.0);
   double denominator = 2.0 * HMMGRU_PI * MathSqrt(var0 * var1);
   return(MathMax(MathExp(exponent) / denominator, 1.0e-300));
  }

//+------------------------------------------------------------------+
//| Fit a three-state HMM and return the current semantic regime.    |
//|                                                                  |
//| Observations run from oldest to newest. Feature 0 is ADX/100.    |
//| Feature 1 is the rolling z-score of ATR as a percentage of price.|
//| Baum-Welch uses scaled forward/backward probabilities. After     |
//| fitting, raw states are relabeled by their emission means so the |
//| labels remain range, trend, and volatile despite label switching.|
//+------------------------------------------------------------------+
bool CSignalHMMGRU::EstimateRegime(const int offset, int &regime,
                                   double &probability, double &posterior[])
  {
   if(ArraySize(posterior) < HMMGRU_STATES)
      return(false);
   const int total = m_hmm_window;
   if(!DataReady(offset, total))
      return(false);
   double obs_adx[];
   double obs_vol_raw[];
   double obs_vol[];
   ArrayResize(obs_adx, total);
   ArrayResize(obs_vol_raw, total);
   ArrayResize(obs_vol, total);
   double vol_mean = 0.0;
   for(int t = 0; t < total; t++)
     {
      int index = offset + (total - 1 - t);
      double close = Close(index);
      double adx  = ADX(index);
      double atr  = ATR(index);
      if(!IsUsable(close) || !IsUsable(adx) || !IsUsable(atr) ||
         MathAbs(close) <= HMMGRU_EPS)
         return(false);
      obs_adx[t] = Clamp(adx / 100.0, 0.0, 1.0);
      obs_vol_raw[t] = atr / MathAbs(close);
      vol_mean += obs_vol_raw[t];
     }
   vol_mean /= total;
   double vol_variance = 0.0;
   for(int t = 0; t < total; t++)
      vol_variance += (obs_vol_raw[t] - vol_mean) * (obs_vol_raw[t] - vol_mean);
   vol_variance /= total;
   double vol_deviation = MathSqrt(MathMax(vol_variance, HMMGRU_EPS));
   for(int t = 0; t < total; t++)
      obs_vol[t] = Clamp((obs_vol_raw[t] - vol_mean) / vol_deviation, -4.0, 4.0);
//--- persistent-transition initialization
   double prior[HMMGRU_STATES];
   double transition[HMMGRU_STATES][HMMGRU_STATES];
   double mean[HMMGRU_STATES][HMMGRU_OBS];
   double variance[HMMGRU_STATES][HMMGRU_OBS];
   prior[0] = 0.40;
   prior[1] = 0.35;
   prior[2] = 0.25;
   for(int i = 0; i < HMMGRU_STATES; i++)
      for(int j = 0; j < HMMGRU_STATES; j++)
         transition[i][j] = (i == j ? 0.90 : 0.05);
   mean[0][0] = 0.18;
   mean[0][1] = -0.35;
   mean[1][0] = 0.42;
   mean[1][1] = 0.00;
   mean[2][0] = 0.30;
   mean[2][1] = 1.00;
   variance[0][0] = 0.018;
   variance[0][1] = 0.40;
   variance[1][0] = 0.025;
   variance[1][1] = 0.55;
   variance[2][0] = 0.040;
   variance[2][1] = 0.75;
   double emission[][HMMGRU_STATES];
   double alpha[][HMMGRU_STATES];
   double beta[][HMMGRU_STATES];
   double gamma[][HMMGRU_STATES];
   double scale[];
   ArrayResize(emission, total);
   ArrayResize(alpha, total);
   ArrayResize(beta, total);
   ArrayResize(gamma, total);
   ArrayResize(scale, total);
//--- an extra final pass evaluates posteriors after the last update
   for(int iteration = 0; iteration <= m_hmm_iterations; iteration++)
     {
      for(int t = 0; t < total; t++)
         for(int state = 0; state < HMMGRU_STATES; state++)
            emission[t][state] = GaussianEmission(obs_adx[t], obs_vol[t],
                                                  mean[state][0], mean[state][1],
                                                  variance[state][0],
                                                  variance[state][1]);
      //--- scaled forward pass
      scale[0] = 0.0;
      for(int state = 0; state < HMMGRU_STATES; state++)
        {
         alpha[0][state] = prior[state] * emission[0][state];
         scale[0] += alpha[0][state];
        }
      if(scale[0] <= HMMGRU_EPS)
         return(false);
      for(int state = 0; state < HMMGRU_STATES; state++)
         alpha[0][state] /= scale[0];
      for(int t = 1; t < total; t++)
        {
         scale[t] = 0.0;
         for(int state = 0; state < HMMGRU_STATES; state++)
           {
            double incoming = 0.0;
            for(int previous = 0; previous < HMMGRU_STATES; previous++)
               incoming += alpha[t - 1][previous] * transition[previous][state];
            alpha[t][state] = incoming * emission[t][state];
            scale[t] += alpha[t][state];
           }
         if(scale[t] <= HMMGRU_EPS)
            return(false);
         for(int state = 0; state < HMMGRU_STATES; state++)
            alpha[t][state] /= scale[t];
        }
      //--- scaled backward pass
      for(int state = 0; state < HMMGRU_STATES; state++)
         beta[total - 1][state] = 1.0;
      for(int t = total - 2; t >= 0; t--)
        {
         for(int state = 0; state < HMMGRU_STATES; state++)
           {
            double outgoing = 0.0;
            for(int next = 0; next < HMMGRU_STATES; next++)
               outgoing += transition[state][next] * emission[t + 1][next] *
                           beta[t + 1][next];
            beta[t][state] = outgoing / MathMax(scale[t + 1], HMMGRU_EPS);
           }
        }
      //--- smoothed state probabilities
      for(int t = 0; t < total; t++)
        {
         double denominator = 0.0;
         for(int state = 0; state < HMMGRU_STATES; state++)
           {
            gamma[t][state] = alpha[t][state] * beta[t][state];
            denominator += gamma[t][state];
           }
         if(denominator <= HMMGRU_EPS)
            return(false);
         for(int state = 0; state < HMMGRU_STATES; state++)
            gamma[t][state] /= denominator;
        }
      if(iteration == m_hmm_iterations)
         break;
      //--- transition sufficient statistics
      double xi_sum[HMMGRU_STATES][HMMGRU_STATES];
      double gamma_transition_sum[HMMGRU_STATES];
      for(int i = 0; i < HMMGRU_STATES; i++)
        {
         gamma_transition_sum[i] = 0.0;
         for(int j = 0; j < HMMGRU_STATES; j++)
            xi_sum[i][j] = 0.0;
        }
      for(int t = 0; t < total - 1; t++)
        {
         double denominator = 0.0;
         for(int i = 0; i < HMMGRU_STATES; i++)
            for(int j = 0; j < HMMGRU_STATES; j++)
               denominator += alpha[t][i] * transition[i][j] *
                              emission[t + 1][j] * beta[t + 1][j];
         if(denominator <= HMMGRU_EPS)
            continue;
         for(int i = 0; i < HMMGRU_STATES; i++)
           {
            gamma_transition_sum[i] += gamma[t][i];
            for(int j = 0; j < HMMGRU_STATES; j++)
               xi_sum[i][j] += alpha[t][i] * transition[i][j] *
                               emission[t + 1][j] * beta[t + 1][j] / denominator;
           }
        }
      //--- initial probabilities and transition matrix
      double prior_total = 0.0;
      for(int state = 0; state < HMMGRU_STATES; state++)
        {
         prior[state] = MathMax(gamma[0][state], 0.001);
         prior_total += prior[state];
        }
      for(int state = 0; state < HMMGRU_STATES; state++)
         prior[state] /= prior_total;
      for(int i = 0; i < HMMGRU_STATES; i++)
        {
         double row_total = 0.0;
         for(int j = 0; j < HMMGRU_STATES; j++)
           {
            double estimate = transition[i][j];
            if(gamma_transition_sum[i] > HMMGRU_EPS)
               estimate = xi_sum[i][j] / gamma_transition_sum[i];
            //--- a small persistence prior prevents single-bar state flicker
            double persistent = (i == j ? 0.90 : 0.05);
            transition[i][j] = MathMax(0.98 * estimate + 0.02 * persistent, 0.001);
            row_total += transition[i][j];
           }
         for(int j = 0; j < HMMGRU_STATES; j++)
            transition[i][j] /= row_total;
        }
      //--- Gaussian means
      double new_mean[HMMGRU_STATES][HMMGRU_OBS];
      double state_weight[HMMGRU_STATES];
      for(int state = 0; state < HMMGRU_STATES; state++)
        {
         state_weight[state] = 0.0;
         new_mean[state][0] = 0.0;
         new_mean[state][1] = 0.0;
         for(int t = 0; t < total; t++)
           {
            state_weight[state] += gamma[t][state];
            new_mean[state][0] += gamma[t][state] * obs_adx[t];
            new_mean[state][1] += gamma[t][state] * obs_vol[t];
           }
         if(state_weight[state] > HMMGRU_EPS)
           {
            new_mean[state][0] /= state_weight[state];
            new_mean[state][1] /= state_weight[state];
           }
         else
           {
            new_mean[state][0] = mean[state][0];
            new_mean[state][1] = mean[state][1];
           }
        }
      //--- diagonal Gaussian variances
      for(int state = 0; state < HMMGRU_STATES; state++)
        {
         double new_variance0 = 0.0;
         double new_variance1 = 0.0;
         for(int t = 0; t < total; t++)
           {
            double d0 = obs_adx[t] - new_mean[state][0];
            double d1 = obs_vol[t] - new_mean[state][1];
            new_variance0 += gamma[t][state] * d0 * d0;
            new_variance1 += gamma[t][state] * d1 * d1;
           }
         if(state_weight[state] > HMMGRU_EPS)
           {
            new_variance0 /= state_weight[state];
            new_variance1 /= state_weight[state];
           }
         variance[state][0] = MathMax(new_variance0, 0.0025);
         variance[state][1] = MathMax(new_variance1, 0.0500);
         mean[state][0] = new_mean[state][0];
         mean[state][1] = new_mean[state][1];
        }
     }
//--- semantic relabeling: highest volatility first, then highest ADX
   int volatile_raw = 0;
   for(int state = 1; state < HMMGRU_STATES; state++)
      if(mean[state][1] > mean[volatile_raw][1])
         volatile_raw = state;
   int trend_raw = -1;
   for(int state = 0; state < HMMGRU_STATES; state++)
     {
      if(state == volatile_raw)
         continue;
      if(trend_raw < 0 || mean[state][0] > mean[trend_raw][0])
         trend_raw = state;
     }
   int range_raw = 0;
   for(int state = 0; state < HMMGRU_STATES; state++)
      if(state != volatile_raw && state != trend_raw)
        {
         range_raw = state;
         break;
        }
   posterior[HMMGRU_REGIME_RANGE]   = gamma[total - 1][range_raw];
   posterior[HMMGRU_REGIME_TREND]   = gamma[total - 1][trend_raw];
   posterior[HMMGRU_REGIME_VOLATILE] = gamma[total - 1][volatile_raw];
   double posterior_total = posterior[0] + posterior[1] + posterior[2];
   if(posterior_total <= HMMGRU_EPS)
      return(false);
   for(int state = 0; state < HMMGRU_STATES; state++)
      posterior[state] /= posterior_total;
   regime = 0;
   probability = posterior[0];
   for(int state = 1; state < HMMGRU_STATES; state++)
      if(posterior[state] > probability)
        {
         regime = state;
         probability = posterior[state];
        }
   return(true);
  }

//+------------------------------------------------------------------+
//| Posterior gate for a dedicated regime.                           |
//+------------------------------------------------------------------+
double CSignalHMMGRU::RegimeGate(const int expected_regime)
  {
   if(expected_regime < 0 || expected_regime >= HMMGRU_STATES)
      return(0.0);
   if(m_current_regime != expected_regime)
      return(0.0);
   if(m_regime_probability[expected_regime] < m_min_regime_probability)
      return(0.0);
//--- after the posterior clears the hard gate, retain a tradeable
//--- vote scale while still rewarding higher state certainty
   double normalized = (m_regime_probability[expected_regime] -
                        m_min_regime_probability) /
                       MathMax(1.0 - m_min_regime_probability, HMMGRU_EPS);
   return(0.75 + 0.25 * Clamp01(normalized));
  }

//+------------------------------------------------------------------+
//| Mode 0: continuation inside an established trend regime.         |
//+------------------------------------------------------------------+
double CSignalHMMGRU::ModeTrendContinuation(const int index)
  {
   double gate = RegimeGate(HMMGRU_REGIME_TREND);
   if(gate <= 0.0)
      return(0.0);
   double adx = ADX(index);
   double previous_adx = ADX(index + 1);
   double di = DirectionalIndex(index);
   double atr_z = ATRZScore(index, m_price_window);
   if(!IsUsable(adx) || !IsUsable(previous_adx) || !IsUsable(atr_z))
      return(0.0);
   if(adx < m_trend_adx_level || adx < previous_adx - 2.0 ||
      MathAbs(di) < 0.08 || atr_z > 2.0 * m_volatility_z_level + 0.50)
      return(0.0);
   double adx_strength = Clamp01((adx - m_trend_adx_level) /
                                 MathMax(60.0 - m_trend_adx_level, 5.0));
   double di_strength = Clamp01(MathAbs(di) * 2.0);
   double strength = Clamp01(0.60 + 0.20 * adx_strength + 0.20 * di_strength);
   return(100.0 * gate * SignOf(di) * strength);
  }

//+------------------------------------------------------------------+
//| Mode 1: +DI/-DI crossover confirmed by rising ADX and ATR.       |
//+------------------------------------------------------------------+
double CSignalHMMGRU::ModeDICrossover(const int index)
  {
   double gate = RegimeGate(HMMGRU_REGIME_TREND);
   if(gate <= 0.0)
      return(0.0);
   double current_difference = PlusDI(index) - MinusDI(index);
   double previous_difference = PlusDI(index + 1) - MinusDI(index + 1);
   double adx = ADX(index);
   double previous_adx = ADX(index + 1);
   double atr_z = ATRZScore(index, m_price_window);
   if(!IsUsable(adx) || !IsUsable(previous_adx) || !IsUsable(atr_z))
      return(0.0);
   if(adx < 0.75 * m_trend_adx_level || adx < previous_adx - 1.0 || atr_z < -1.25)
      return(0.0);
   double direction = 0.0;
   if(current_difference > 0.0 && previous_difference <= 0.0)
      direction = 1.0;
   else
      if(current_difference < 0.0 && previous_difference >= 0.0)
         direction = -1.0;
   if(direction == 0.0)
      return(0.0);
   double separation = Clamp01(MathAbs(current_difference) / 25.0);
   double slope = Clamp01((adx - previous_adx + 2.0) / 6.0);
   double strength = Clamp01(0.65 + 0.20 * separation + 0.15 * slope);
   return(100.0 * gate * direction * strength);
  }

//+------------------------------------------------------------------+
//| Mode 2: ATR-scaled mean reversion inside a range regime.         |
//+------------------------------------------------------------------+
double CSignalHMMGRU::ModeRangeReversion(const int index)
  {
   double gate = RegimeGate(HMMGRU_REGIME_RANGE);
   if(gate <= 0.0)
      return(0.0);
   double atr = ATR(index);
   double mean = MeanClose(index, m_price_window);
   double adx = ADX(index);
   if(!IsUsable(atr) || !IsUsable(mean) || !IsUsable(adx) ||
      atr <= HMMGRU_EPS || adx > 1.15 * m_trend_adx_level)
      return(0.0);
   double displacement = (Close(index) - mean) / atr;
   if(MathAbs(displacement) < m_range_atr_distance)
      return(0.0);
   double direction = -SignOf(displacement);
   double body = (Close(index) - Open(index)) / atr;
   double turn = Clamp01(direction * body + 0.35);
   double excursion = Clamp01((MathAbs(displacement) - m_range_atr_distance) / 1.50 + 0.35);
   double weakness = Clamp01((1.15 * m_trend_adx_level - adx) /
                             MathMax(m_trend_adx_level, 5.0));
   double strength = Clamp01(0.60 + 0.20 * excursion + 0.10 * turn + 0.10 * weakness);
   return(100.0 * gate * direction * strength);
  }

//+------------------------------------------------------------------+
//| Mode 3: ATR expansion and price breakout inputs a volatile regime.   |
//+------------------------------------------------------------------+
double CSignalHMMGRU::ModeVolatilityBreakout(const int index)
  {
   double gate = RegimeGate(HMMGRU_REGIME_VOLATILE);
   if(gate <= 0.0)
      return(0.0);
   int lookback = m_price_window / 2;
   if(lookback < 5)
      lookback = 5;
   double highest = HighestPriorHigh(index, lookback);
   double lowest = LowestPriorLow(index, lookback);
   double atr_z  = ATRZScore(index, m_price_window);
   double di     = DirectionalIndex(index);
   if(!IsUsable(highest) || !IsUsable(lowest) || !IsUsable(atr_z))
      return(0.0);
   if(atr_z < m_volatility_z_level)
      return(0.0);
   double direction = 0.0;
   if(Close(index) > highest && di > 0.05)
      direction = 1.0;
   else
      if(Close(index) < lowest && di < -0.05)
         direction = -1.0;
   if(direction == 0.0)
      return(0.0);
   double expansion = Clamp01((atr_z - m_volatility_z_level) / 2.0 + 0.40);
   double alignment = Clamp01(MathAbs(di) * 2.0);
   double strength = Clamp01(0.65 + 0.20 * expansion + 0.15 * alignment);
   return(100.0 * gate * direction * strength);
  }

//+------------------------------------------------------------------+
//| Mode 4: breakout following ATR compression.                      |
//+------------------------------------------------------------------+
double CSignalHMMGRU::ModeCompressionBreakout(const int index)
  {
   if(m_current_regime == HMMGRU_REGIME_VOLATILE)
      return(0.0);
   double gate = RegimeGate(m_current_regime);
   if(gate <= 0.0)
      return(0.0);
   int lookback = m_price_window / 2;
   if(lookback < 5)
      lookback = 5;
   double previous_atr_z = ATRZScore(index + 1, m_price_window);
   double current_atr = ATR(index);
   double previous_atr = ATR(index + 1);
   double adx = ADX(index);
   double previous_adx = ADX(index + 1);
   double highest = HighestPriorHigh(index, lookback);
   double lowest = LowestPriorLow(index, lookback);
   double di = DirectionalIndex(index);
   if(!IsUsable(previous_atr_z) || !IsUsable(current_atr) ||
      !IsUsable(previous_atr) || !IsUsable(adx) ||
      !IsUsable(previous_adx) || !IsUsable(highest) ||
      !IsUsable(lowest) || previous_atr <= HMMGRU_EPS)
      return(0.0);
   if(previous_atr_z > -0.35 || current_atr < 1.05 * previous_atr ||
      adx <= previous_adx)
      return(0.0);
   double direction = 0.0;
   if(Close(index) > highest && di > 0.0)
      direction = 1.0;
   else
      if(Close(index) < lowest && di < 0.0)
         direction = -1.0;
   if(direction == 0.0)
      return(0.0);
   double expansion = Clamp01(current_atr / previous_atr - 1.0);
   double adx_slope = Clamp01((adx - previous_adx) / 5.0);
   double strength = Clamp01(0.55 + 0.25 * expansion + 0.20 * adx_slope);
   return(100.0 * gate * direction * strength);
  }

//+------------------------------------------------------------------+
//| Mode 5: engulfing or impulse price action interpreted per regime.|
//+------------------------------------------------------------------+
double CSignalHMMGRU::ModePriceAction(const int index)
  {
   double gate = RegimeGate(m_current_regime);
   if(gate <= 0.0)
      return(0.0);
   double atr = ATR(index);
   if(!IsUsable(atr) || atr <= HMMGRU_EPS || !DataReady(index, 2))
      return(0.0);
   bool bullish_engulf = (Close(index) > Open(index) &&
                          Close(index + 1) < Open(index + 1) &&
                          Close(index) >= Open(index + 1) &&
                          Open(index) <= Close(index + 1));
   bool bearish_engulf = (Close(index) < Open(index) &&
                          Close(index + 1) > Open(index + 1) &&
                          Close(index) <= Open(index + 1) &&
                          Open(index) >= Close(index + 1));
   double body = (Close(index) - Open(index)) / atr;
   double direction = 0.0;
   double pattern_strength = 0.0;
   if(bullish_engulf)
     {
      direction = 1.0;
      pattern_strength = 0.90;
     }
   else
      if(bearish_engulf)
        {
         direction = -1.0;
         pattern_strength = 0.90;
        }
      else
         if(MathAbs(body) >= 0.80)
           {
            direction = SignOf(body);
            pattern_strength = Clamp01(MathAbs(body) / 1.50);
           }
   if(direction == 0.0)
      return(0.0);
   double di = DirectionalIndex(index);
   double adx = ADX(index);
   double atr_z = ATRZScore(index, m_price_window);
   if(!IsUsable(adx) || !IsUsable(atr_z))
      return(0.0);
   double context_strength = 0.0;
   if(m_current_regime == HMMGRU_REGIME_TREND)
     {
      if(adx < m_trend_adx_level || direction * di <= 0.0)
         return(0.0);
      context_strength = Clamp01(0.50 + MathAbs(di));
     }
   else
      if(m_current_regime == HMMGRU_REGIME_RANGE)
        {
         double mean = MeanClose(index, m_price_window);
         if(!IsUsable(mean) || direction * (mean - Close(index)) <= 0.0 ||
            adx > 1.15 * m_trend_adx_level)
            return(0.0);
         context_strength = Clamp01(MathAbs(Close(index) - mean) / (atr * 2.0));
        }
      else
        {
         if(atr_z < m_volatility_z_level || direction * di <= 0.0)
            return(0.0);
         context_strength = Clamp01((atr_z - m_volatility_z_level) / 2.0 + 0.50);
        }
   double strength = Clamp01(0.60 * pattern_strength + 0.40 * context_strength);
   return(100.0 * gate * direction * strength);
  }

//+------------------------------------------------------------------+
//| Mode 6: automatically select the strongest regime-native setup.  |
//+------------------------------------------------------------------+
double CSignalHMMGRU::ModeAdaptive(const int index)
  {
   if(m_current_regime == HMMGRU_REGIME_TREND)
      return(Stronger(ModeTrendContinuation(index), ModeDICrossover(index)));
   if(m_current_regime == HMMGRU_REGIME_RANGE)
      return(Stronger(ModeRangeReversion(index), ModeCompressionBreakout(index)));
   return(Stronger(ModeVolatilityBreakout(index), ModePriceAction(index)));
  }

//+------------------------------------------------------------------+
//| Dispatch one selected algorithm mode.                            |
//+------------------------------------------------------------------+
double CSignalHMMGRU::SelectAlgorithmMode(const int index)
  {
   if(!IS_PATTERN_USAGE(m_signal_mode))
      return(0.0);
   switch(m_signal_mode)
     {
      case HMMGRU_MODE_TREND_CONTINUATION:
         return(ModeTrendContinuation(index));
      case HMMGRU_MODE_DI_CROSSOVER:
         return(ModeDICrossover(index));
      case HMMGRU_MODE_RANGE_REVERSION:
         return(ModeRangeReversion(index));
      case HMMGRU_MODE_VOLATILITY_BREAK:
         return(ModeVolatilityBreakout(index));
      case HMMGRU_MODE_COMPRESSION_BREAK:
         return(ModeCompressionBreakout(index));
      case HMMGRU_MODE_PRICE_ACTION:
         return(ModePriceAction(index));
      case HMMGRU_MODE_ADAPTIVE:
         return(ModeAdaptive(index));
     }
   return(0.0);
  }

//+------------------------------------------------------------------+
//| Deterministic small initial weight.                              |
//+------------------------------------------------------------------+
double CSignalHMMGRU::InitialWeight(const int regime, const int gate,
                                    const int row, const int column)
  {
   double phase = (regime + 1) * 12.9898 + (gate + 1) * 78.233 +
                  (row + 1) * 37.719 + (column + 1) * 19.913;
   return(0.16 * MathSin(phase));
  }

//+------------------------------------------------------------------+
//| Initialize all three GRUs.                                       |
//+------------------------------------------------------------------+
void CSignalHMMGRU::InitializeGRUWeights(void)
  {
   for(int regime = 0; regime < HMMGRU_STATES; regime++)
     {
      m_bo[regime] = 0.0;
      for(int row = 0; row < HMMGRU_HIDDEN; row++)
        {
         m_bz[regime][row] = 0.50;
         m_br[regime][row] = 0.00;
         m_bh[regime][row] = 0.00;
         m_wo[regime][row] = InitialWeight(regime, 9, row, 0);
         for(int column = 0; column < HMMGRU_INPUTS; column++)
           {
            m_wz[regime][row][column] = InitialWeight(regime, 0, row, column);
            m_wr[regime][row][column] = InitialWeight(regime, 1, row, column);
            m_wh[regime][row][column] = InitialWeight(regime, 2, row, column);
           }
         for(int column = 0; column < HMMGRU_HIDDEN; column++)
           {
            m_uz[regime][row][column] = InitialWeight(regime, 3, row, column);
            m_ur[regime][row][column] = InitialWeight(regime, 4, row, column);
            m_uh[regime][row][column] = InitialWeight(regime, 5, row, column);
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| Numerically safe sigmoid.                                        |
//+------------------------------------------------------------------+
double CSignalHMMGRU::Sigmoid(const double value)
  {
   double bounded = Clamp(value, -30.0, 30.0);
   return(1.0 / (1.0 + MathExp(-bounded)));
  }

//+------------------------------------------------------------------+
//| Gradient clipping for stable online updates.                     |
//+------------------------------------------------------------------+
double CSignalHMMGRU::ClipGradient(const double value)
  {
   return(Clamp(value, -2.0, 2.0));
  }

//+------------------------------------------------------------------+
//| Construct five causal GRU features.                              |
//+------------------------------------------------------------------+
bool CSignalHMMGRU::BuildGRUFeatures(const int index, double &inputs[])
  {
   if(ArraySize(inputs) < HMMGRU_INPUTS || !DataReady(index, m_price_window + 1))
      return(false);
   double atr = ATR(index);
   double adx = ADX(index);
   double atr_z = ATRZScore(index, m_price_window);
   if(!IsUsable(atr) || !IsUsable(adx) || !IsUsable(atr_z) ||
      atr <= HMMGRU_EPS)
      return(false);
   inputs[0] = Clamp((adx - m_trend_adx_level) / 25.0, -1.0, 1.0);
   inputs[1] = DirectionalIndex(index);
   inputs[2] = Clamp(atr_z / 3.0, -1.0, 1.0);
   inputs[3] = Clamp((Close(index) - Close(index + 1)) / (2.0 * atr), -1.0, 1.0);
   inputs[4] = Clamp((Close(index) - Open(index)) / atr, -1.0, 1.0);
   return(true);
  }

//+------------------------------------------------------------------+
//| One GRU forward step.                                            |
//+------------------------------------------------------------------+
bool CSignalHMMGRU::GRUForward(const int regime, const double &inputs[],
                               double &hidden[], double &output)
  {
   if(regime < 0 || regime >= HMMGRU_STATES ||
      ArraySize(inputs) < HMMGRU_INPUTS ||
      ArraySize(hidden) < HMMGRU_HIDDEN)
      return(false);
   double previous[HMMGRU_HIDDEN];
   double update[HMMGRU_HIDDEN];
   double reset[HMMGRU_HIDDEN];
   double candidate[HMMGRU_HIDDEN];
   double next_hidden[HMMGRU_HIDDEN];
   for(int row = 0; row < HMMGRU_HIDDEN; row++)
      previous[row] = hidden[row];
   for(int row = 0; row < HMMGRU_HIDDEN; row++)
     {
      double z_value = m_bz[regime][row];
      double r_value = m_br[regime][row];
      for(int column = 0; column < HMMGRU_INPUTS; column++)
        {
         z_value += m_wz[regime][row][column] * inputs[column];
         r_value += m_wr[regime][row][column] * inputs[column];
        }
      for(int column = 0; column < HMMGRU_HIDDEN; column++)
        {
         z_value += m_uz[regime][row][column] * previous[column];
         r_value += m_ur[regime][row][column] * previous[column];
        }
      update[row] = Sigmoid(z_value);
      reset[row] = Sigmoid(r_value);
     }
   for(int row = 0; row < HMMGRU_HIDDEN; row++)
     {
      double value = m_bh[regime][row];
      for(int column = 0; column < HMMGRU_INPUTS; column++)
         value += m_wh[regime][row][column] * inputs[column];
      for(int column = 0; column < HMMGRU_HIDDEN; column++)
         value += m_uh[regime][row][column] *
                  (reset[column] * previous[column]);
      candidate[row] = MathTanh(value);
      next_hidden[row] = (1.0 - update[row]) * candidate[row] +
                         update[row] * previous[row];
     }
   double output_value = m_bo[regime];
   for(int row = 0; row < HMMGRU_HIDDEN; row++)
     {
      hidden[row] = next_hidden[row];
      output_value += m_wo[regime][row] * next_hidden[row];
     }
   output = MathTanh(output_value);
   return(true);
  }

//+------------------------------------------------------------------+
//| Train one GRU step with one-step truncated backpropagation.      |
//|                                                                  |
//| The previous hidden state is treated as a constant. Repeated     |
//| chronological updates still train the recurrent matrices while   |
//| keeping online computation bounded for an Expert Advisor.        |
//+------------------------------------------------------------------+
double CSignalHMMGRU::GRUTrainStep(const int regime, const double &inputs[],
                                   double &hidden[], const double target,
                                   const double learning_rate)
  {
   double previous[HMMGRU_HIDDEN];
   double update[HMMGRU_HIDDEN];
   double reset[HMMGRU_HIDDEN];
   double candidate[HMMGRU_HIDDEN];
   double next_hidden[HMMGRU_HIDDEN];
   for(int row = 0; row < HMMGRU_HIDDEN; row++)
      previous[row] = hidden[row];
   for(int row = 0; row < HMMGRU_HIDDEN; row++)
     {
      double z_value = m_bz[regime][row];
      double r_value = m_br[regime][row];
      for(int column = 0; column < HMMGRU_INPUTS; column++)
        {
         z_value += m_wz[regime][row][column] * inputs[column];
         r_value += m_wr[regime][row][column] * inputs[column];
        }
      for(int column = 0; column < HMMGRU_HIDDEN; column++)
        {
         z_value += m_uz[regime][row][column] * previous[column];
         r_value += m_ur[regime][row][column] * previous[column];
        }
      update[row] = Sigmoid(z_value);
      reset[row] = Sigmoid(r_value);
     }
   for(int row = 0; row < HMMGRU_HIDDEN; row++)
     {
      double value = m_bh[regime][row];
      for(int column = 0; column < HMMGRU_INPUTS; column++)
         value += m_wh[regime][row][column] * inputs[column];
      for(int column = 0; column < HMMGRU_HIDDEN; column++)
         value += m_uh[regime][row][column] *
                  (reset[column] * previous[column]);
      candidate[row] = MathTanh(value);
      next_hidden[row] = (1.0 - update[row]) * candidate[row] +
                         update[row] * previous[row];
     }
   double output_value = m_bo[regime];
   for(int row = 0; row < HMMGRU_HIDDEN; row++)
      output_value += m_wo[regime][row] * next_hidden[row];
   double output = MathTanh(output_value);
   double error = output - Clamp(target, -1.0, 1.0);
   double output_gradient = error * (1.0 - output * output);
   double hidden_gradient[HMMGRU_HIDDEN];
   double candidate_gradient[HMMGRU_HIDDEN];
   double update_gradient[HMMGRU_HIDDEN];
   double reset_gradient[HMMGRU_HIDDEN];
   double reset_path_gradient[HMMGRU_HIDDEN];
   for(int row = 0; row < HMMGRU_HIDDEN; row++)
     {
      hidden_gradient[row] = output_gradient * m_wo[regime][row];
      candidate_gradient[row] = hidden_gradient[row] * (1.0 - update[row]) *
                                (1.0 - candidate[row] * candidate[row]);
      update_gradient[row] = hidden_gradient[row] *
                             (previous[row] - candidate[row]) *
                             update[row] * (1.0 - update[row]);
      reset_path_gradient[row] = 0.0;
     }
   for(int column = 0; column < HMMGRU_HIDDEN; column++)
     {
      for(int row = 0; row < HMMGRU_HIDDEN; row++)
         reset_path_gradient[column] += candidate_gradient[row] *
                                        m_uh[regime][row][column];
      reset_gradient[column] = reset_path_gradient[column] * previous[column] *
                               reset[column] * (1.0 - reset[column]);
     }
   const double decay = 1.0e-5;
//--- output layer
   for(int row = 0; row < HMMGRU_HIDDEN; row++)
      m_wo[regime][row] -= learning_rate *
                           (ClipGradient(output_gradient * next_hidden[row]) +
                            decay * m_wo[regime][row]);
   m_bo[regime] -= learning_rate * ClipGradient(output_gradient);
//--- update, reset, and candidate parameters
   for(int row = 0; row < HMMGRU_HIDDEN; row++)
     {
      for(int column = 0; column < HMMGRU_INPUTS; column++)
        {
         m_wz[regime][row][column] -= learning_rate *
                                      (ClipGradient(update_gradient[row] * inputs[column]) +
                                       decay * m_wz[regime][row][column]);
         m_wr[regime][row][column] -= learning_rate *
                                      (ClipGradient(reset_gradient[row] * inputs[column]) +
                                       decay * m_wr[regime][row][column]);
         m_wh[regime][row][column] -= learning_rate *
                                      (ClipGradient(candidate_gradient[row] * inputs[column]) +
                                       decay * m_wh[regime][row][column]);
        }
      for(int column = 0; column < HMMGRU_HIDDEN; column++)
        {
         m_uz[regime][row][column] -= learning_rate *
                                      (ClipGradient(update_gradient[row] * previous[column]) +
                                       decay * m_uz[regime][row][column]);
         m_ur[regime][row][column] -= learning_rate *
                                      (ClipGradient(reset_gradient[row] * previous[column]) +
                                       decay * m_ur[regime][row][column]);
         m_uh[regime][row][column] -= learning_rate *
                                      (ClipGradient(candidate_gradient[row] * reset[column] *
                                            previous[column]) +
                                       decay * m_uh[regime][row][column]);
        }
      m_bz[regime][row] -= learning_rate * ClipGradient(update_gradient[row]);
      m_br[regime][row] -= learning_rate * ClipGradient(reset_gradient[row]);
      m_bh[regime][row] -= learning_rate * ClipGradient(candidate_gradient[row]);
      hidden[row] = next_hidden[row];
     }
   return(0.5 * error * error);
  }

//+------------------------------------------------------------------+
//| Train on a causal, oldest-to-newest window of closed bars.       |
//+------------------------------------------------------------------+
bool CSignalHMMGRU::TrainGRUWindow(const int anchor, const int bars,
                                   const int epochs,
                                   const double learning_rate)
  {
   if(anchor < 1 || bars < 20 || epochs < 1)
      return(false);
   if(!DataReady(anchor, bars + m_hmm_window + m_price_window + 2))
      return(false);
   double samples[][HMMGRU_INPUTS];
   double targets[];
   double confidences[];
   int regimes[];
   bool valid[];
   ArrayResize(samples, bars);
   ArrayResize(targets, bars);
   ArrayResize(confidences, bars);
   ArrayResize(regimes, bars);
   ArrayResize(valid, bars);
   int valid_total = 0;
   for(int sample = 0; sample < bars; sample++)
     {
      valid[sample] = false;
      int index = anchor + bars - sample;
      double inputs[HMMGRU_INPUTS];
      if(!BuildGRUFeatures(index, inputs))
         continue;
      double posterior[HMMGRU_STATES];
      int regime = 0;
      double confidence = 0.0;
      if(!EstimateRegime(index, regime, confidence, posterior))
         continue;
      double atr = ATR(index);
      double future_change = Close(index - 1) - Close(index);
      if(!IsUsable(atr) || !IsUsable(future_change) || atr <= HMMGRU_EPS)
         continue;
      for(int feature = 0; feature < HMMGRU_INPUTS; feature++)
         samples[sample][feature] = inputs[feature];
      targets[sample] = MathTanh(future_change / (0.75 * atr));
      confidences[sample] = Clamp(confidence, 0.35, 1.0);
      regimes[sample] = regime;
      valid[sample] = true;
      valid_total++;
     }
   if(valid_total < 20)
      return(false);
   double accumulated_loss = 0.0;
   int update_count = 0;
   for(int epoch = 0; epoch < epochs; epoch++)
     {
      double state[HMMGRU_STATES][HMMGRU_HIDDEN];
      for(int regime = 0; regime < HMMGRU_STATES; regime++)
         for(int unit = 0; unit < HMMGRU_HIDDEN; unit++)
            state[regime][unit] = 0.0;
      int previous_regime = -1;
      for(int sample = 0; sample < bars; sample++)
        {
         if(!valid[sample])
           {
            previous_regime = -1;
            continue;
           }
         int regime = regimes[sample];
         if(regime != previous_regime)
            for(int unit = 0; unit < HMMGRU_HIDDEN; unit++)
               state[regime][unit] = 0.0;
         double inputs[HMMGRU_INPUTS];
         double hidden[HMMGRU_HIDDEN];
         for(int feature = 0; feature < HMMGRU_INPUTS; feature++)
            inputs[feature] = samples[sample][feature];
         for(int unit = 0; unit < HMMGRU_HIDDEN; unit++)
            hidden[unit] = state[regime][unit];
         double step_rate = learning_rate * confidences[sample];
         accumulated_loss += GRUTrainStep(regime, inputs, hidden,
                                          targets[sample], step_rate);
         update_count++;
         for(int unit = 0; unit < HMMGRU_HIDDEN; unit++)
            state[regime][unit] = hidden[unit];
         previous_regime = regime;
        }
     }
   if(update_count > 0)
      m_gru_training_loss = accumulated_loss / update_count;
   return(update_count > 0);
  }

//+------------------------------------------------------------------+
//| Predict with only the GRU belonging to the active HMM regime.    |
//+------------------------------------------------------------------+
bool CSignalHMMGRU::PredictGRU(const int index, const int active_regime,
                               double &score)
  {
   score = 0.0;
   if(!m_gru_ready || active_regime < 0 ||
      active_regime >= HMMGRU_STATES)
      return(false);
   int oldest = index;
   for(int candidate = index + 1; candidate < index + m_gru_sequence; candidate++)
     {
      double posterior[HMMGRU_STATES];
      int regime = 0;
      double probability = 0.0;
      if(!EstimateRegime(candidate, regime, probability, posterior))
         break;
      if(regime != active_regime ||
         probability < m_min_regime_probability)
         break;
      oldest = candidate;
     }
   double hidden[HMMGRU_HIDDEN];
   for(int unit = 0; unit < HMMGRU_HIDDEN; unit++)
      hidden[unit] = 0.0;
   bool processed = false;
   for(int current = oldest; current >= index; current--)
     {
      double inputs[HMMGRU_INPUTS];
      if(!BuildGRUFeatures(current, inputs))
         return(false);
      if(!GRUForward(active_regime, inputs, hidden, score))
         return(false);
      processed = true;
     }
   return(processed);
  }

//+------------------------------------------------------------------+
//| Require directional agreement and blend vote magnitudes.         |
//+------------------------------------------------------------------+
double CSignalHMMGRU::FuseWithGRU(const double algorithm_score,
                                  const double gru_score)
  {
   if(MathAbs(algorithm_score) < HMMGRU_EPS)
      return(0.0);
   if(MathAbs(gru_score) < m_gru_min_confidence)
      return(0.0);
   if(algorithm_score * gru_score <= 0.0)
      return(0.0);
   double regime_weight = RegimeGate(m_current_regime);
   double network_vote = 100.0 * MathAbs(gru_score) * regime_weight;
   double supported_vote = MathMax(MathAbs(algorithm_score), network_vote);
   double magnitude = (1.0 - m_gru_weight) * MathAbs(algorithm_score) +
                      m_gru_weight * supported_vote;
   return(SignOf(algorithm_score) * Clamp(magnitude, 0.0, 100.0));
  }

//+------------------------------------------------------------------+
//| Check whether LongCondition and ShortCondition can share a vote. |
//+------------------------------------------------------------------+
bool CSignalHMMGRU::SameCachedObservation(const int index)
  {
   if(!m_cache_valid || Time(index) != m_cached_time)
      return(false);
   if(!m_every_tick)
      return(true);
   return(Open(index) == m_cached_open &&
          High(index) == m_cached_high &&
          Low(index) == m_cached_low &&
          Close(index) == m_cached_close);
  }

//+------------------------------------------------------------------+
//| Estimate the regime, run one algorithm mode, and optionally GRU. |
//+------------------------------------------------------------------+
bool CSignalHMMGRU::CalculateSignal(void)
  {
   int index = StartIndex();
   if(!DataReady(index, m_hmm_window + m_price_window + 2))
      return(false);
   if(SameCachedObservation(index))
      return(true);
   double posterior[HMMGRU_STATES];
   int regime = 0;
   double probability = 0.0;
   if(!EstimateRegime(index, regime, probability, posterior))
     {
      m_cache_valid = false;
      return(false);
     }
   m_current_regime = regime;
   m_current_regime_probability = probability;
   for(int state = 0; state < HMMGRU_STATES; state++)
      m_regime_probability[state] = posterior[state];
   double algorithm_score = Clamp(SelectAlgorithmMode(index), -100.0, 100.0);
   double final_score = algorithm_score;
   double gru_score = 0.0;
   if(m_use_gru)
     {
      datetime closed_bar_time = Time(1);
      //--- initial fit uses only completed targets, ending at bar 2
      if(!m_gru_ready)
        {
         if(TrainGRUWindow(1, m_gru_train_bars, m_gru_epochs,
                           m_gru_learning_rate))
           {
            m_gru_ready = true;
            m_last_trained_bar = closed_bar_time;
           }
        }
      //--- modest walk-forward refresh once for each newly closed bar
      else
         if(closed_bar_time != 0 && closed_bar_time != m_last_trained_bar)
           {
            int online_bars = m_gru_train_bars;
            if(online_bars > 24)
               online_bars = 24;
            TrainGRUWindow(1, online_bars, 1, 0.35 * m_gru_learning_rate);
            m_last_trained_bar = closed_bar_time;
           }
      if(!m_gru_ready ||
         !PredictGRU(index, m_current_regime, gru_score))
         final_score = 0.0;
      else
         final_score = FuseWithGRU(algorithm_score, gru_score);
     }
   m_cached_algorithm_score = algorithm_score;
   m_cached_gru_score = gru_score;
   m_cached_score = Clamp(final_score, -100.0, 100.0);
   m_cached_time = Time(index);
   m_cached_open = Open(index);
   m_cached_high = High(index);
   m_cached_low = Low(index);
   m_cached_close = Close(index);
   m_cache_valid = true;
   return(true);
  }

//+------------------------------------------------------------------+
//| Vote that price will rise.                                       |
//+------------------------------------------------------------------+
int CSignalHMMGRU::LongCondition(void)
  {
   if(!CalculateSignal() || m_cached_score <= 0.0)
      return(0);
   return((int)MathRound(Clamp(m_cached_score, 0.0, 100.0)));
  }

//+------------------------------------------------------------------+
//| Vote that price will fall.                                       |
//+------------------------------------------------------------------+
int CSignalHMMGRU::ShortCondition(void)
  {
   if(!CalculateSignal() || m_cached_score >= 0.0)
      return(0);
   return((int)MathRound(Clamp(-m_cached_score, 0.0, 100.0)));
  }
//+------------------------------------------------------------------+
