//+------------------------------------------------------------------+
//|                                              FXP_OrderBlocks.mq5 |
//|                                                    FXPIPS ONLINE |
//+------------------------------------------------------------------+
#property copyright "FXPIPS ONLINE"
#property link      "https://fxpips.online/mt5-indicators/order-blocks/"
#property version   "1.00"
#property description "Bullish and bearish order blocks: the last opposite candle before a"
#property description "move that breaks swing structure. Mitigated blocks are greyed or removed."
/*
   License: MIT. Copyright (c) FXPIPS ONLINE.
   Educational tool - not financial advice. Trading involves substantial risk of loss.
*/
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   0

enum ENUM_FXP_MITIGATION
  {
   MIT_TOUCH = 0,     // Wick touches the block
   MIT_MID   = 1,     // Wick reaches the block midpoint
   MIT_CLOSE = 2      // Candle closes beyond the far edge
  };

enum ENUM_FXP_MITIGATED_ACTION
  {
   ACT_GREY   = 0,    // Keep as grey rectangle
   ACT_DELETE = 1     // Delete rectangle
  };

input group "Detection"
input int    InpSwingLen      = 5;        // Swing pivot length (bars each side)
input double InpMinDispATR    = 1.0;      // Min displacement candle body (x ATR, 0 = off)
input int    InpATRPeriod     = 14;       // ATR period
input bool   InpUseBody       = false;    // Use candle body instead of full range
input int    InpMaxBars       = 2000;     // History bars to analyse
input group "Blocks"
input ENUM_FXP_MITIGATION InpMitigation = MIT_CLOSE; // Block is mitigated when
input ENUM_FXP_MITIGATED_ACTION InpMitigatedAction = ACT_GREY; // Mitigated blocks
input int    InpMaxActive     = 5;        // Max active blocks per side
input int    InpMaxMitigated  = 10;       // Max grey blocks kept
input int    InpExtendBars    = 10;       // Extend active blocks right by N bars
input color  InpBullColor     = C'20,70,130';   // Bullish block color
input color  InpBearColor     = C'130,30,40';   // Bearish block color
input color  InpGreyColor     = C'70,70,70';    // Mitigated block color
input group "Alerts"
input bool   InpAlerts        = false;    // Alert on new block / mitigation (once per bar)

const string PREFIX = "FXP_OB_";

struct OBlock
  {
   string   name;
   double   top;
   double   bottom;
   datetime t1;
   bool     bull;
   bool     active;
   int      created;   // bar index where the block was confirmed
  };

double   AtrBuf[];
OBlock   g_blocks[];
int      g_next = -1;
double   g_lastHigh = 0.0, g_lastLow = 0.0;
int      g_lastHighIdx = -1, g_lastLowIdx = -1;
bool     g_highBroken = true, g_lowBroken = true;
datetime g_lastAlert = 0;

//+------------------------------------------------------------------+
int OnInit()
  {
   if(InpSwingLen < 1 || InpATRPeriod < 1)
      return(INIT_PARAMETERS_INCORRECT);
   SetIndexBuffer(0, AtrBuf, INDICATOR_CALCULATIONS);
   ArraySetAsSeries(AtrBuf, false);
   IndicatorSetString(INDICATOR_SHORTNAME, "FXP Order Blocks");
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ObjectsDeleteAll(0, PREFIX);
   ChartRedraw();
  }

//+------------------------------------------------------------------+
void ResetState()
  {
   ObjectsDeleteAll(0, PREFIX);
   ArrayResize(g_blocks, 0);
   g_next = -1;
   g_lastHigh = 0.0;
   g_lastLow = 0.0;
   g_lastHighIdx = -1;
   g_lastLowIdx = -1;
   g_highBroken = true;
   g_lowBroken = true;
  }

//+------------------------------------------------------------------+
bool PivotHigh(const double &h[], int p, int total)
  {
   if(p - InpSwingLen < 0 || p + InpSwingLen >= total)
      return(false);
   for(int k = 1; k <= InpSwingLen; k++)
      if(h[p - k] >= h[p] || h[p + k] > h[p])
         return(false);
   return(true);
  }

//+------------------------------------------------------------------+
bool PivotLow(const double &l[], int p, int total)
  {
   if(p - InpSwingLen < 0 || p + InpSwingLen >= total)
      return(false);
   for(int k = 1; k <= InpSwingLen; k++)
      if(l[p - k] <= l[p] || l[p + k] < l[p])
         return(false);
   return(true);
  }

//+------------------------------------------------------------------+
void Notify(string msg, datetime t)
  {
   if(!InpAlerts || t == g_lastAlert)
      return;
   g_lastAlert = t;
   Alert(msg);
  }

//+------------------------------------------------------------------+
void RemoveAt(int idx)
  {
   ObjectDelete(0, g_blocks[idx].name);
   int n = ArraySize(g_blocks);
   for(int i = idx; i < n - 1; i++)
      g_blocks[i] = g_blocks[i + 1];
   ArrayResize(g_blocks, n - 1);
  }

//+------------------------------------------------------------------+
void EnforceLimits()
  {
   // oldest entries are at the front of the array
   int activeBull = 0, activeBear = 0, grey = 0;
   for(int i = ArraySize(g_blocks) - 1; i >= 0; i--)
     {
      bool drop = false;
      if(g_blocks[i].active)
        {
         if(g_blocks[i].bull)
           {
            activeBull++;
            drop = (activeBull > InpMaxActive);
           }
         else
           {
            activeBear++;
            drop = (activeBear > InpMaxActive);
           }
        }
      else
        {
         grey++;
         drop = (grey > InpMaxMitigated);
        }
      if(drop)
         RemoveAt(i);
     }
  }

//+------------------------------------------------------------------+
void AddBlock(bool bull, int obIdx, int c, const datetime &time[], const double &open[],
              const double &high[], const double &low[], const double &close[])
  {
   string name = PREFIX + (bull ? "B_" : "S_") + IntegerToString((long)time[obIdx]);
   if(ObjectFind(0, name) >= 0)
      return;
   double top = high[obIdx], bottom = low[obIdx];
   if(InpUseBody)
     {
      top = MathMax(open[obIdx], close[obIdx]);
      bottom = MathMin(open[obIdx], close[obIdx]);
     }
   if(top <= bottom)
      return;
   int n = ArraySize(g_blocks);
   ArrayResize(g_blocks, n + 1);
   g_blocks[n].name = name;
   g_blocks[n].top = top;
   g_blocks[n].bottom = bottom;
   g_blocks[n].t1 = time[obIdx];
   g_blocks[n].bull = bull;
   g_blocks[n].active = true;
   g_blocks[n].created = c;

   ObjectCreate(0, name, OBJ_RECTANGLE, 0, time[obIdx], top, time[c], bottom);
   ObjectSetInteger(0, name, OBJPROP_COLOR, bull ? InpBullColor : InpBearColor);
   ObjectSetInteger(0, name, OBJPROP_FILL, true);
   ObjectSetInteger(0, name, OBJPROP_BACK, true);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
   ObjectSetString(0, name, OBJPROP_TOOLTIP, bull ? "Bullish order block" : "Bearish order block");
   EnforceLimits();
  }

//+------------------------------------------------------------------+
bool HasDisplacement(int from, int to, const double &open[], const double &close[])
  {
   if(InpMinDispATR <= 0.0)
      return(true);
   double atr = AtrBuf[to];
   if(atr <= 0.0)
      return(false);
   for(int i = from; i <= to; i++)
      if(MathAbs(close[i] - open[i]) >= InpMinDispATR * atr)
         return(true);
   return(false);
  }

//+------------------------------------------------------------------+
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[])
  {
   ArraySetAsSeries(time, false);
   ArraySetAsSeries(open, false);
   ArraySetAsSeries(high, false);
   ArraySetAsSeries(low, false);
   ArraySetAsSeries(close, false);

   if(rates_total < 2 * InpSwingLen + InpATRPeriod + 3)
      return(0);

   // --- ATR (Wilder), incremental
   int startAtr = (prev_calculated > 1) ? prev_calculated - 1 : 1;
   if(prev_calculated <= 1)
      AtrBuf[0] = high[0] - low[0];
   for(int i = startAtr; i < rates_total; i++)
     {
      double tr = MathMax(high[i], close[i - 1]) - MathMin(low[i], close[i - 1]);
      if(i < InpATRPeriod)
         AtrBuf[i] = (AtrBuf[i - 1] * i + tr) / (i + 1);
      else
         AtrBuf[i] = (AtrBuf[i - 1] * (InpATRPeriod - 1) + tr) / InpATRPeriod;
     }

   if(prev_calculated == 0 || g_next < 0)
     {
      ResetState();
      g_next = MathMax(2 * InpSwingLen + InpATRPeriod, rates_total - InpMaxBars);
     }
   bool live = (prev_calculated > 0);
   int lastClosed = rates_total - 2;

   for(int c = g_next; c <= lastClosed; c++)
     {
      // --- mitigation of existing blocks
      for(int b = ArraySize(g_blocks) - 1; b >= 0; b--)
        {
         if(!g_blocks[b].active || c <= g_blocks[b].created)
            continue;
         bool hit = false;
         double mid = (g_blocks[b].top + g_blocks[b].bottom) / 2.0;
         if(g_blocks[b].bull)
           {
            if(InpMitigation == MIT_TOUCH)
               hit = (low[c] <= g_blocks[b].top);
            else
               if(InpMitigation == MIT_MID)
                  hit = (low[c] <= mid);
               else
                  hit = (close[c] < g_blocks[b].bottom);
           }
         else
           {
            if(InpMitigation == MIT_TOUCH)
               hit = (high[c] >= g_blocks[b].bottom);
            else
               if(InpMitigation == MIT_MID)
                  hit = (high[c] >= mid);
               else
                  hit = (close[c] > g_blocks[b].top);
           }
         if(!hit)
            continue;
         if(live && c == lastClosed)
            Notify(StringFormat("FXP Order Blocks %s %s: %s block mitigated", _Symbol, EnumToString(_Period),
                                g_blocks[b].bull ? "bullish" : "bearish"), time[c]);
         if(InpMitigatedAction == ACT_DELETE)
            RemoveAt(b);
         else
           {
            g_blocks[b].active = false;
            ObjectSetInteger(0, g_blocks[b].name, OBJPROP_COLOR, InpGreyColor);
            ObjectSetInteger(0, g_blocks[b].name, OBJPROP_TIME, 1, time[c]);
           }
        }
      EnforceLimits();

      // --- swing pivots confirmed at c
      int p = c - InpSwingLen;
      if(PivotHigh(high, p, rates_total))
        {
         g_lastHigh = high[p];
         g_lastHighIdx = p;
         g_highBroken = false;
        }
      if(PivotLow(low, p, rates_total))
        {
         g_lastLow = low[p];
         g_lastLowIdx = p;
         g_lowBroken = false;
        }

      // --- bullish break: last bearish candle at the origin of the move
      if(!g_highBroken && c > g_lastHighIdx && close[c] > g_lastHigh)
        {
         g_highBroken = true;
         int m = g_lastHighIdx;
         for(int i = g_lastHighIdx; i <= c; i++)
            if(low[i] < low[m])
               m = i;
         int ob = -1;
         for(int i = m; i >= MathMax(0, m - 10); i--)
            if(close[i] < open[i])
              {
               ob = i;
               break;
              }
         if(ob >= 0 && HasDisplacement(m, c, open, close))
           {
            AddBlock(true, ob, c, time, open, high, low, close);
            if(live && c == lastClosed)
               Notify(StringFormat("FXP Order Blocks %s %s: new bullish order block", _Symbol, EnumToString(_Period)), time[c]);
           }
        }

      // --- bearish break: last bullish candle at the origin of the move
      if(!g_lowBroken && c > g_lastLowIdx && close[c] < g_lastLow)
        {
         g_lowBroken = true;
         int m = g_lastLowIdx;
         for(int i = g_lastLowIdx; i <= c; i++)
            if(high[i] > high[m])
               m = i;
         int ob = -1;
         for(int i = m; i >= MathMax(0, m - 10); i--)
            if(close[i] > open[i])
              {
               ob = i;
               break;
              }
         if(ob >= 0 && HasDisplacement(m, c, open, close))
           {
            AddBlock(false, ob, c, time, open, high, low, close);
            if(live && c == lastClosed)
               Notify(StringFormat("FXP Order Blocks %s %s: new bearish order block", _Symbol, EnumToString(_Period)), time[c]);
           }
        }
     }
   g_next = lastClosed + 1;

   // --- extend active blocks to the right
   datetime right = time[rates_total - 1] + (datetime)(PeriodSeconds(_Period) * InpExtendBars);
   for(int b = 0; b < ArraySize(g_blocks); b++)
      if(g_blocks[b].active)
         ObjectSetInteger(0, g_blocks[b].name, OBJPROP_TIME, 1, right);
   return(rates_total);
  }
//+------------------------------------------------------------------+
