//+------------------------------------------------------------------+
//|                                        RMI_Trend_Sniper.mq5      |
//|                                  Copyright 2026, TZack88 ported  |
//|                                       https://www.mql5.com       |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026"
#property link      "https://www.mql5.com"
#property version   "1.24"
#property indicator_chart_window
#property indicator_buffers 6
#property indicator_plots   3

//--- Plot 1 Properties (Center)
#property indicator_type1   DRAW_COLOR_LINE
#property indicator_color1  clrDodgerBlue,clrRed
#property indicator_style1  STYLE_SOLID
#property indicator_width1  2
#property indicator_label1  "RRTH Center"

//--- Plot 2 Properties (Top)
#property indicator_type2   DRAW_COLOR_LINE
#property indicator_color2  clrDodgerBlue,clrRed
#property indicator_style2  STYLE_DASH
#property indicator_width2  1
#property indicator_label2  "RRTH Top"

//--- Plot 3 Properties (Bottom)
#property indicator_type3   DRAW_COLOR_LINE
#property indicator_color3  clrDodgerBlue,clrRed
#property indicator_style3  STYLE_DASH
#property indicator_width3  1
#property indicator_label3  "RRTH Bottom"

//--- Enums
enum ENUM_ALERT_MODE
{
   ALERT_CURRENT,    // On Current Bar
   ALERT_CLOSED,     // On Just-Closed Bar
   ALERT_NONE        // No Alerts
};

//--- Inputs
input group "Timeframe Settings"
input ENUM_TIMEFRAMES InpCustomTimeframe = PERIOD_CURRENT; // Calculation Timeframe (PERIOD_CURRENT for auto)

input group "RMI Settings"
input int         InpLength   = 14;     
input int         InpPmom     = 66;     
input int         InpNmom     = 30;     

input group "TDFI Filter Settings"
input bool        InpUseTDFI      = true;  // Joint TDFI Filter Active
input int         InpTDFILen      = 13;
input double      InpTDFILevel    = 0.3;   // Threshold as a multiple of the recent N-bar AVERAGE |force| (self-normalized, see InpTDFINormWindow)
input int         InpTDFINormWindow = 50;  // Self-normalization lookback (bars)

input group "Visuals"
input bool        InpShowMA     = true;  // Show RRTH Center
input bool        InpShowBands  = true;  // Show RRTH Top/Bottom

input group "Signals"
input bool        InpShowArrows     = true;         // Show BUY/SELL Arrows
input color       InpArrowBullColor = clrDodgerBlue; // Bull Arrow Color
input color       InpArrowBearColor = clrRed;        // Bear Arrow Color
input int         InpArrowSize      = 2;             // Arrow Size
input ENUM_ALERT_MODE InpAlertMode  = ALERT_CURRENT;  // Alert Mode

#define OBJ_PREFIX "SSSSSS_SIG_"

//--- Buffers
double BufRwmaCenter[];
double BufRwmaCenterColors[];
double BufTopBand[];
double BufTopBandColors[];
double BufBottomBand[];
double BufBottomBandColors[];

int atrHandle;
int emaHandle;
int mfiHandle;
ENUM_TIMEFRAMES calcTf;
datetime g_lastAlertBarTime = 0;

//+------------------------------------------------------------------+
int OnInit()
{
   SetIndexBuffer(0, BufRwmaCenter, INDICATOR_DATA);
   SetIndexBuffer(1, BufRwmaCenterColors, INDICATOR_COLOR_INDEX);
   SetIndexBuffer(2, BufTopBand, INDICATOR_DATA);
   SetIndexBuffer(3, BufTopBandColors, INDICATOR_COLOR_INDEX);
   SetIndexBuffer(4, BufBottomBand, INDICATOR_DATA);
   SetIndexBuffer(5, BufBottomBandColors, INDICATOR_COLOR_INDEX);

   ArraySetAsSeries(BufRwmaCenter, true);
   ArraySetAsSeries(BufRwmaCenterColors, true);
   ArraySetAsSeries(BufTopBand, true);
   ArraySetAsSeries(BufTopBandColors, true);
   ArraySetAsSeries(BufBottomBand, true);
   ArraySetAsSeries(BufBottomBandColors, true);

   PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_COLOR_LINE);
   PlotIndexSetInteger(0, PLOT_COLOR_INDEXES, 2);
   PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, 70);

   PlotIndexSetInteger(1, PLOT_DRAW_TYPE, DRAW_COLOR_LINE);
   PlotIndexSetInteger(1, PLOT_COLOR_INDEXES, 2);
   PlotIndexSetInteger(1, PLOT_DRAW_BEGIN, 70);

   PlotIndexSetInteger(2, PLOT_DRAW_TYPE, DRAW_COLOR_LINE);
   PlotIndexSetInteger(2, PLOT_COLOR_INDEXES, 2);
   PlotIndexSetInteger(2, PLOT_DRAW_BEGIN, 70);

   calcTf = (InpCustomTimeframe == PERIOD_CURRENT) ? _Period : InpCustomTimeframe;

   atrHandle  = iATR(_Symbol, calcTf, 30);
   emaHandle  = iMA(_Symbol, calcTf, 5, 0, MODE_EMA, PRICE_CLOSE);
   mfiHandle  = iMFI(_Symbol, calcTf, InpLength, VOLUME_TICK);

   if(atrHandle == INVALID_HANDLE || emaHandle == INVALID_HANDLE || mfiHandle == INVALID_HANDLE)
   {
      Print("Failed to create standard handles. Error: ", GetLastError());
      return(INIT_FAILED);
   }

   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   int total = ObjectsTotal(0, -1, -1);
   for(int i = total - 1; i >= 0; i--)
   {
      string nm = ObjectName(0, i, -1, -1);
      if(StringFind(nm, OBJ_PREFIX) == 0) ObjectDelete(0, nm);
   }
}

//+------------------------------------------------------------------+
// Creates (or replaces) a BUY/SELL arrow marker at a trend-flip bar.
// Delete-then-recreate makes this safe to call every recalculation.
//+------------------------------------------------------------------+
void CreateSignalMarker(string name, datetime t, double price, bool isBull)
{
   if(ObjectFind(0, name) >= 0) ObjectDelete(0, name);
   ObjectCreate(0, name, OBJ_ARROW, 0, t, price);
   ObjectSetInteger(0, name, OBJPROP_ARROWCODE, isBull ? 233 : 234);
   ObjectSetInteger(0, name, OBJPROP_COLOR,  isBull ? InpArrowBullColor : InpArrowBearColor);
   ObjectSetInteger(0, name, OBJPROP_WIDTH,  InpArrowSize);
   ObjectSetInteger(0, name, OBJPROP_ANCHOR, isBull ? ANCHOR_TOP : ANCHOR_BOTTOM);
   ObjectSetInteger(0, name, OBJPROP_BACK,   false);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
}

//+------------------------------------------------------------------+
// Returns the close price at a chart-timeframe offset, correctly mapped
// onto calcTf's own bars when a custom calculation timeframe is in use.
// mappedIdx must already be a calcTf-shift (see BuildLookbackMap).
//+------------------------------------------------------------------+
double ClosePriceAt(int mappedIdx, const double &close[])
{
   if(calcTf == _Period) return close[mappedIdx];
   return iClose(_Symbol, calcTf, mappedIdx);
}

//+------------------------------------------------------------------+
// Fills mapIdx[0..count-1] with the calcTf-bar index corresponding to
// chart bars i, i+1, i+2, ... so every lookback (RSI, EMA, MFI, ATR)
// reads from the SAME timeframe instead of mixing chart-native closes
// with calcTf-native indicator values. Returns false if any offset
// runs off the end of history (caller should skip/treat as invalid).
//+------------------------------------------------------------------+
bool BuildLookbackMap(int i, int count, int rates_total, const datetime &time[], int &mapIdx[])
{
   ArrayResize(mapIdx, count);
   for(int k = 0; k < count; k++)
   {
      int ci = i + k;
      if(ci >= rates_total) return false;

      if(calcTf == _Period) { mapIdx[k] = ci; continue; }

      int mi = iBarShift(_Symbol, calcTf, time[ci]);
      if(mi < 0) return false;
      mapIdx[k] = mi;
   }
   return true;
}

//+------------------------------------------------------------------+
// Raw (un-normalized) TDFI force at chart bar baseIdx: the average
// bar-to-bar close change over InpTDFILen bars, mapped onto calcTf.
// Returns false (outValue left at 0) if there isn't enough history.
//+------------------------------------------------------------------+
bool ComputeTdfiRaw(int baseIdx, int rates_total, const datetime &time[], const double &close[], double &outValue)
{
   outValue = 0.0;
   int tmpMap[];
   if(!BuildLookbackMap(baseIdx, InpTDFILen + 1, rates_total, time, tmpMap))
      return false;

   double sum = 0.0;
   for(int k = 0; k < InpTDFILen; k++)
      sum += (ClosePriceAt(tmpMap[k], close) - ClosePriceAt(tmpMap[k+1], close));

   outValue = (InpTDFILen > 0) ? sum / InpTDFILen : 0.0;
   return true;
}

//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
   if(rates_total < 70) return(0);

   ArraySetAsSeries(close, true);
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low, true);
   ArraySetAsSeries(time, true);

   double atrVal[], emaVal[], mfiVal[];
   ArraySetAsSeries(atrVal, true);
   ArraySetAsSeries(emaVal, true);
   ArraySetAsSeries(mfiVal, true);

   int copied = CopyBuffer(atrHandle, 0, 0, rates_total, atrVal);
   if(copied <= 0) return(0);
   CopyBuffer(emaHandle, 0, 0, rates_total, emaVal);
   CopyBuffer(mfiHandle, 0, 0, rates_total, mfiVal);

   int start = prev_calculated - 1;
   if(prev_calculated <= 70 || start < 1 || start >= rates_total)  
      start = rates_total - 2;

   int realStart = start;
   if(prev_calculated <= 70)
   {
      // Walk the ENTIRE available history on a full recalculation (attach,
      // parameter change, or a chart timeframe switch - which forces MT5 to
      // call OnInit again) instead of stopping 72 bars back. statePositive/
      // stateNegative are re-derived from scratch on every call, so a
      // shallow walk forgets any trend older than 72 bars and can lock onto
      // a stray dip inside that window with no memory of what should have
      // reconfirmed the true trend just before it - producing a wrong color
      // right after a switch even though live ticking had it right.
      realStart = rates_total - 1;
   }

   bool statePositive = false;
   bool stateNegative = false;

   // Dynamic lookback scaling based on timeframe seconds ratio
   double tfRatio = 1.0;
   if(calcTf != _Period && calcTf != PERIOD_CURRENT)
   {
      double baseSec = PeriodSeconds(_Period);
      double calcSec = PeriodSeconds(calcTf);
      if(baseSec > 0) tfRatio = calcSec / baseSec;
   }
   
   // Adjust lookback depth inversely for higher TFs so equivalent temporal window is evaluated
   int effectiveRwmaPeriod = 20;
   if(tfRatio < 1.0 && tfRatio > 0)
   {
      effectiveRwmaPeriod = (int)MathMin(50, MathMax(5, 20.0 / tfRatio));
   }

   for(int i = realStart; i >= 0 && !IsStopped(); i--)
   {
      if(i >= rates_total - InpLength - effectiveRwmaPeriod - 5 || (i + effectiveRwmaPeriod) >= rates_total)  
      {
         BufRwmaCenter[i]    = EMPTY_VALUE;
         BufTopBand[i]       = EMPTY_VALUE;
         BufBottomBand[i]    = EMPTY_VALUE;
         continue;
      }

      // Single calcTf-mapped lookback covers RSI, RSI-prev, EMA and MFI so
      // every one of them reads the SAME timeframe's bars (previously RSI
      // walked the chart's own close[] regardless of InpCustomTimeframe,
      // while EMA/MFI/ATR correctly used calcTf).
      int mapIdx[];
      if(!BuildLookbackMap(i, InpLength + 2, rates_total, time, mapIdx))
         continue;
      int h_idx      = mapIdx[0];
      int h_idx_prev = mapIdx[1];

      double upSum = 0, downSum = 0;
      for(int k = 0; k < InpLength; k++)
      {
         double change = ClosePriceAt(mapIdx[k], close) - ClosePriceAt(mapIdx[k+1], close);
         if(change > 0) upSum += change;
         else downSum -= change;
      }

      double rsi = (downSum == 0) ? 100.0 : (upSum == 0) ? 0.0 : (100.0 - (100.0 / (1.0 + (upSum / downSum))));
      double mfi_val_current = (h_idx < ArraySize(mfiVal)) ? mfiVal[h_idx] : 50.0;
      double rsi_mfi = (rsi + mfi_val_current) / 2.0;

      double upSumPrev = 0, downSumPrev = 0;
      for(int k = 0; k < InpLength; k++)
      {
         double change = ClosePriceAt(mapIdx[k+1], close) - ClosePriceAt(mapIdx[k+2], close);
         if(change > 0) upSumPrev += change;
         else downSumPrev -= change;
      }
      double rsi_prev = (downSumPrev == 0) ? 100.0 : (upSumPrev == 0) ? 0.0 : (100.0 - (100.0 / (1.0 + (upSumPrev / downSumPrev))));

      double mfi_val_prev = (h_idx_prev >= 0 && h_idx_prev < ArraySize(mfiVal)) ? mfiVal[h_idx_prev] : 50.0;
      double rsi_mfi_prev = (rsi_prev + mfi_val_prev) / 2.0;

      double emaChange = 0;
      if(h_idx >= 0 && h_idx + 1 < ArraySize(emaVal))
         emaChange = emaVal[h_idx] - emaVal[h_idx+1];

      bool base_p_mom = (rsi_mfi_prev < InpPmom) && (rsi_mfi > InpPmom) && (rsi_mfi > InpNmom) && (emaChange > 0);
      bool base_n_mom = (rsi_mfi < InpNmom) && (emaChange < 0);

      // --- TDFI: self-normalization against the recent AVERAGE |force| ---
      // A min/max-based scale (dividing by the single biggest force seen in
      // the window) turned out to have the same failure mode as the raw/ATR
      // versions: one big outlier bar (e.g. the initial rally) permanently
      // sets the bar too high, so every smaller-but-real continuation move
      // for the rest of that ~50-bar window reads as "not enough force" and
      // never flips. Averaging |force| over the window instead means one
      // outlier only nudges the baseline by 1/window, so the filter keeps
      // responding to normal-sized trend moves right after a big spike.
      double tdfiWindow[];
      ArrayResize(tdfiWindow, InpTDFINormWindow + 1);
      bool tdfiOk = true;
      for(int w = 0; w <= InpTDFINormWindow && tdfiOk; w++)
         tdfiOk = ComputeTdfiRaw(i + w, rates_total, time, close, tdfiWindow[w]);

      bool tdfi_pass_p = false;
      bool tdfi_pass_n = false;
      if(!InpUseTDFI)
      {
         tdfi_pass_p = true;
         tdfi_pass_n = true;
      }
      else if(tdfiOk)
      {
         double sumAbsCurrent = 0.0, sumAbsPrev = 0.0;
         for(int w = 0; w < InpTDFINormWindow; w++)
            sumAbsCurrent += MathAbs(tdfiWindow[w]);
         for(int w = 1; w <= InpTDFINormWindow; w++)
            sumAbsPrev += MathAbs(tdfiWindow[w]);

         double meanAbsCurrent = sumAbsCurrent / InpTDFINormWindow;
         double meanAbsPrev    = sumAbsPrev    / InpTDFINormWindow;

         double tdfi_norm      = (meanAbsCurrent > 0) ? tdfiWindow[0] / meanAbsCurrent : 0.0;
         double tdfi_prev_norm = (meanAbsPrev    > 0) ? tdfiWindow[1] / meanAbsPrev    : 0.0;

         tdfi_pass_p = (tdfi_norm >= InpTDFILevel && tdfi_norm >= tdfi_prev_norm);
         tdfi_pass_n = (tdfi_norm <= -InpTDFILevel && tdfi_norm <= tdfi_prev_norm);
      }
      // else: not enough history for the full normalization window yet -
      // conservatively block both directions rather than guessing.

      bool p_mom = base_p_mom && (!InpUseTDFI || tdfi_pass_p);
      bool n_mom = base_n_mom && (!InpUseTDFI || tdfi_pass_n);

      bool wasPositive = statePositive;
      bool wasNegative = stateNegative;

      if(p_mom) { statePositive = true; stateNegative = false; }
      else if(n_mom) { statePositive = false; stateNegative = true; }

      bool newPositive = statePositive && !wasPositive;
      bool newNegative = stateNegative && !wasNegative;

      string sigName = OBJ_PREFIX + (string)time[i];
      if(newPositive || newNegative)
      {
         if(InpShowArrows)
         {
            double sigPrice = newPositive ? low[i] : high[i];
            CreateSignalMarker(sigName, time[i], sigPrice, newPositive);
         }

         // Only alert for the bar matching the selected mode - a full
         // recalculation (attach, parameter change, timeframe switch) walks
         // the entire history and would otherwise replay an alert for every
         // past flip.
         bool alertBarMatch = (InpAlertMode == ALERT_CURRENT && i == 0) ||
                              (InpAlertMode == ALERT_CLOSED  && i == 1);
         if(alertBarMatch && time[i] != g_lastAlertBarTime)
         {
            string msg = _Symbol + " " + EnumToString((ENUM_TIMEFRAMES)_Period) +
                         " RMI Trend Sniper: " + (newPositive ? "BUY" : "SELL");
            Alert(msg);
            SendNotification(msg);
            g_lastAlertBarTime = time[i];
         }
      }
      else if(i == 0 && ObjectFind(0, sigName) >= 0)
      {
         // The still-forming bar no longer satisfies the flip condition
         // after a live-tick price revision - remove the stale marker.
         ObjectDelete(0, sigName);
      }

      double atr_value = (h_idx < ArraySize(atrVal)) ? atrVal[h_idx] : 0;

      // Normalized band width calculation compensating for structural chart variance
      double band = MathMin(atr_value * 0.3, close[i] * (0.3 / 100.0)) * 4.0;
      if(calcTf == PERIOD_CURRENT)
      {
         band *= 1.0;
      }
      else
      {
         // Scale relative to timeframe difference to maintain uniform visual proportions
         double adjustment = MathSqrt(MathMax(0.1, tfRatio));
         band *= adjustment;
      }

      double sumWeight = 0;
      double sumProd = 0;
      for(int p = 0; p < effectiveRwmaPeriod && (i+p) < rates_total; p++)
      {
         double r = high[i+p] - low[i+p];
         sumWeight += r;
      }
      
      double rwma = close[i];
      if(sumWeight > 0)
      {
         for(int p = 0; p < effectiveRwmaPeriod && (i+p) < rates_total; p++)
         {
            double r = high[i+p] - low[i+p];
            double weight = r / sumWeight;
            sumProd += close[i+p] * weight;
         }
         rwma = sumProd;
      }

      double finalRwma = rwma;
      if(statePositive) finalRwma = rwma - band;
      else if(stateNegative) finalRwma = rwma + band;

      int colorIdx = stateNegative ? 1 : 0;

      if(InpShowMA)
      {
         BufRwmaCenter[i]        = finalRwma;
         BufRwmaCenterColors[i]  = colorIdx;
      }
      else
      {
         BufRwmaCenter[i]        = EMPTY_VALUE;
      }

      if(InpShowBands)
      {
         BufTopBand[i]           = finalRwma + band;
         BufTopBandColors[i]     = colorIdx;

         BufBottomBand[i]        = finalRwma - band;
         BufBottomBandColors[i]  = colorIdx;
      }
      else
      {
         BufTopBand[i]           = EMPTY_VALUE;
         BufBottomBand[i]        = EMPTY_VALUE;
      }
   }

   return(rates_total);
}
//+------------------------------------------------------------------+