//+------------------------------------------------------------------+
//| Strong SD Magnet.mq5                                             |
//| Translated from Pine Script – original concept by FeelsStrategy  |
//+------------------------------------------------------------------+
#property copyright "Translated to MQ5"
#property indicator_chart_window
#property indicator_plots 0

//--- Enums for drop-down menus
enum ENUM_SESS_MODE
{
   SESS_DAILY,    // Daily
   SESS_WEEKLY,   // Weekly
   SESS_MONTHLY,  // Monthly
   SESS_ASIAN,    // Asian
   SESS_LONDON,   // London
   SESS_NY,       // New York
   SESS_CUSTOM    // Custom
};

enum ENUM_INV_METHOD
{
   INV_CLOSE,     // Close
   INV_WICK       // Wick
};

enum ENUM_LABEL_SIZE
{
   LBL_SIZE_TINY,   // Tiny
   LBL_SIZE_SMALL,  // Small
   LBL_SIZE_NORMAL, // Normal
   LBL_SIZE_LARGE   // Large
};

enum ENUM_DASH_SIZE
{
   DASH_SIZE_SMALL,  // Small
   DASH_SIZE_NORMAL, // Normal
   DASH_SIZE_LARGE   // Large
};

enum ENUM_ALERT_MODE
{
   ALERT_CURRENT,    // On Current Bar
   ALERT_CLOSED,     // On Just Closed Bar
   ALERT_NONE        // No Alerts
};

//--- Session
input ENUM_SESS_MODE InpSessionMode   = SESS_DAILY;  // New-Zone Session
input string         InpCustomSess    = "0000-2359"; // Custom Session (HHMM-HHMM)
input int      InpTzShift       = 0;           // Timezone Shift (Hours, 0 = exchange)
input int      InpSessionsBack  = 10;          // Sessions To Display
input int      InpMaxPerSession = 3;           // Max Zones Per Side / Session
input bool     InpWholeSession  = true;        // Zones Span Whole Session
input bool     InpFadeOld       = true;        // Fade Older Sessions

//--- Global session parameters
int g_sessStartHour = 0;
int g_sessStartMin  = 0;
int g_sessEndHour   = 23;
int g_sessEndMin    = 59;

//--- Zone Detection
input int      InpSwingLen      = 12;          // Swing Length (sensitivity)
input int      InpMinZoneDist   = 15;          // Min Distance Between Zones (bars)
input bool     InpMergeOverlap  = true;        // Merge Overlapping Zones

//--- Zone Height
input int      InpAtrLen        = 20;          // ATR Length
input double   InpUniHeight     = 0.5;         // Universal Zone Height (ATR x)
input double   InpMinSwingATR   = 0.3;         // Ignore Pivots Smaller Than (ATR x)

//--- Behaviour
input ENUM_INV_METHOD InpInvMethod     = INV_CLOSE;   // Invalidation

//--- Strength Scoring
input bool     InpOnlyStrong    = false;       // Show Only Strong Zones
input double   InpStrongThr     = 4.0;         // Strong Threshold
input double   InpTouchNorm     = 3.0;         // Touches Until Fully Consumed
input double   InpImpulseNorm   = 3.0;         // Impulse For Full Score (ATR x)
input int      InpVolLen        = 20;          // Volume Baseline Length
input double   InpWImp          = 0.35;        // Weight: Departure Impulse
input double   InpWVol          = 0.20;        // Weight: Volume
input double   InpWWick         = 0.20;        // Weight: Rejection Wick
input double   InpWFresh        = 0.25;        // Weight: Freshness
input bool     InpUseDecay      = true;        // Decay Strength While Idle
input int      InpDecayBars     = 600;         // Full Decay Over (idle bars)
input double   InpDecayFloor    = 0.6;         // Decay Floor

//--- Display
input bool             InpShowLabels    = true;               // Show In-Band Labels
input bool             InpCompactLabels = true;               // Compact Labels
input double           InpLabelPosFrac  = 0.6;               // Label Position Along Band
input ENUM_LABEL_SIZE  InpLabelSize     = LBL_SIZE_SMALL;    // Label Size
input bool             InpShowDash      = true;               // Show Dashboard
input ENUM_BASE_CORNER InpDashCorner    = CORNER_RIGHT_UPPER; // Dashboard Corner [Left-Upper/Right-Upper/Left-Lower/Right-Lower]
input int              InpDashX         = 5;                  // Dashboard X Offset (pixels)
input int              InpDashY         = 5;                  // Dashboard Y Offset (pixels)
input ENUM_DASH_SIZE   InpDashSize      = DASH_SIZE_NORMAL;   // Dashboard Size
input bool             InpShowToggleBtn = true;               // Show Dashboard Toggle Button
input ENUM_BASE_CORNER InpBtnCorner     = CORNER_RIGHT_UPPER; // Button Corner [Left-Upper/Right-Upper/Left-Lower/Right-Lower]
input int              InpBtnX          = 5;                  // Button X Offset (pixels)
input int              InpBtnY          = 270;                // Button Y Offset (pixels)

//--- Colors
input color    InpSupCol        = C'229,75,94';    // Supply Color
input color    InpDemCol        = C'39,192,122';   // Demand Color
input bool     InpSharpenByStr  = true;            // Sharpen Fill By Strength
input int      InpTrStrong      = 58;              // Strongest Fill Transparency
input int      InpTrWeak        = 82;              // Weakest Fill Transparency
input int      InpTrBroken      = 88;              // Invalidated Fill Transparency
input int      InpBorderTransp  = 20;              // Border Transparency
input int      InpBorderWidth   = 1;               // Border Width
input color    InpBestSupTxt    = C'255,157,60';   // Best Supply Text
input color    InpBestDemTxt    = C'123,247,176';  // Best Demand Text
input color    InpNormTxt       = C'232,234,237';  // Normal Text

//--- Liquidity Magnet
input bool     InpMagnetsOn     = true;        // Show Magnet Pulls
input int      InpMagMaxPerSess = 2;           // Max Magnets Per Session
input int      InpMagSessBack   = 10;          // Show Magnets For Last N Sessions
input double   InpMagnetThr     = 6.5;         // Source Zone Min Score
input int      InpMagMinTouch   = 1;           // Source Min Re-tests
input double   InpMagTargetThr  = 5.0;         // Target Zone Min Score
input double   InpMagMaxReach   = 25.0;        // Max Reach To Target (ATR)
input double   InpMagAnchorFrac = 0.7;         // Arrow Position Along Session
input double   InpMagProbMid    = 5.5;         // Pull% Midpoint
input double   InpMagProbSlope  = 1.5;         // Pull% Steepness
input color    InpMagColor      = C'255,179,0'; // Magnet Color
input ENUM_LABEL_SIZE InpMagLblSize    = LBL_SIZE_SMALL; // Magnet Label Size

//--- Alerts
input ENUM_ALERT_MODE InpAlertMode = ALERT_CURRENT; // Alert Mode
input double   InpAlertProx     = 0.10;        // Proximity (% of price)
input double   InpInstThr       = 7.0;         // Strong-Zone Score for alert

#define OBJ_PREFIX "SDM_"

//+------------------------------------------------------------------+
struct ZoneData
{
   string   boxName;
   string   labName;
   double   top;
   double   bot;
   double   mid;
   datetime leftTime;
   datetime rightTime;
   int      bornBar;
   int      lastTouch;
   int      sessId;
   string   sess;
   string   kind;
   int      touches;
   bool     inPrev;
   bool     broken;
   double   impF;
   double   volF;
   double   wickF;
   double   score;
};

struct MagnetData
{
   string lnName;
   string labName;
};

struct MagStatData
{
   int    sessId;
   bool   down;
   string srcTag;
   string tgtTag;
   bool   tgtFresh;
   double tgtMid;
   double srcMid;
   double pull10;
};

//--- Global state
ZoneData    zones[];
MagnetData  magnets[];
MagStatData magStats[];
int         g_sessId       = 0;
datetime    g_curSessStart = 0;
int         g_lastSupBar   = 0;
int         g_lastDemBar   = 0;
int         g_objCounter   = 0;

//--- Dashboard state & caching
bool        g_showDashState = true;
double      g_lastClose     = 0;
double      g_lastAtr       = 0;
MagStatData g_lastCs[];
int         g_lastCsCnt     = 0;

//--- Alert throttling state
datetime    g_lastAlertTime_NewSup     = 0;
datetime    g_lastAlertTime_NewDem     = 0;
datetime    g_lastAlertTime_NearSup    = 0;
datetime    g_lastAlertTime_NearDem    = 0;
datetime    g_lastAlertTime_NearStrong = 0;
datetime    g_lastAlertTime_Magnet     = 0;

//+------------------------------------------------------------------+
color ColorFade(color clr, int transp)
{
   double t = MathMax(0, MathMin(transp, 100)) / 100.0;
   color bg = (color)ChartGetInteger(0, CHART_COLOR_BACKGROUND);
   
   int r_fg = clr & 0xFF;
   int g_fg = (clr >> 8) & 0xFF;
   int b_fg = (clr >> 16) & 0xFF;
   
   int r_bg = bg & 0xFF;
   int g_bg = (bg >> 8) & 0xFF;
   int b_bg = (bg >> 16) & 0xFF;
   
   int r = (int)(r_fg * (1.0 - t) + r_bg * t);
   int g = (int)(g_fg * (1.0 - t) + g_bg * t);
   int b = (int)(b_fg * (1.0 - t) + b_bg * t);
   
   return (color)(r | (g << 8) | (b << 16));
}

string NewName(string tag)
{
   g_objCounter++;
   return OBJ_PREFIX + tag + IntegerToString(g_objCounter);
}

void DelObj(string nm)
{
   if(nm != "" && ObjectFind(0, nm) >= 0) ObjectDelete(0, nm);
}

void DeleteAllOurs()
{
   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);
   }
}

//+------------------------------------------------------------------+
string f_stars(double sc)
{
   if(sc >= 8.0) return "★★★★";
   if(sc >= 6.5) return "★★★";
   if(sc >= 5.0) return "★★";
   if(sc >= 3.0) return "★";
   return "·";
}
string f_tier(double sc)
{
   if(sc >= 8.0) return "ELITE";
   if(sc >= 6.5) return "STRONG";
   if(sc >= 5.0) return "MODERATE";
   if(sc >= 3.0) return "WEAK";
   return "FORMING";
}
string f_state(bool brk, int t)
{
   if(brk)    return "✗ INVAL";
   if(t == 0) return "FRESH";
   return "TESTED";
}
double f_ageF(int idle)
{
   if(!InpUseDecay) return 1.0;
   double dB  = MathMax((double)InpDecayBars, 1.0);
   double raw = 1.0 - (1.0 - InpDecayFloor) * MathMin((double)idle / dB, 1.0);
   return MathMax(raw, InpDecayFloor);
}
string f_sessName(datetime t)
{
   datetime shifted = t + InpTzShift * 3600;
   MqlDateTime ts; TimeToStruct(shifted, ts);
   if(ts.hour <  8) return "Asia";
   if(ts.hour < 13) return "London";
   if(ts.hour < 22) return "NY";
   return "Late";
}
double f_score(double impF, double volF, double wickF, int touches, int idle)
{
   double freshF = MathMax(0.0, 1.0 - touches / MathMax(InpTouchNorm, 1.0));
   double wSum   = InpWImp + InpWVol + InpWWick + InpWFresh;
   double raw    = wSum > 0 ? (impF*InpWImp + volF*InpWVol + wickF*InpWWick + freshF*InpWFresh) / wSum : 0.0;
   return MathMin(MathMax(raw * 10.0 * f_ageF(idle), 0.0), 10.0);
}
bool f_overlap(double t1, double b1, double t2, double b2)
{
   return t1 >= b2 && b1 <= t2;
}
int f_fontSize(ENUM_LABEL_SIZE s)
{
   if(s == LBL_SIZE_TINY)   return 8;
   if(s == LBL_SIZE_NORMAL) return 11;
   if(s == LBL_SIZE_LARGE)  return 14;
   return 9; // Small is default
}

void ParseSessionSettings()
{
   if(InpSessionMode == SESS_DAILY || InpSessionMode == SESS_WEEKLY || InpSessionMode == SESS_MONTHLY) return;
   
   string sStr = "";
   if(InpSessionMode == SESS_ASIAN)         sStr = "0000-0800";
   else if(InpSessionMode == SESS_LONDON)   sStr = "0800-1300";
   else if(InpSessionMode == SESS_NY)       sStr = "1300-2200";
   else                                     sStr = InpCustomSess;
   
   string parts[];
   int nParts = StringSplit(sStr, '-', parts);
   if(nParts >= 2)
   {
      string startPart = parts[0];
      string endPart = parts[1];
      StringTrimLeft(startPart); StringTrimRight(startPart);
      StringTrimLeft(endPart); StringTrimRight(endPart);
      
      if(StringLen(startPart) >= 4)
      {
         g_sessStartHour = (int)StringToInteger(StringSubstr(startPart, 0, 2));
         g_sessStartMin  = (int)StringToInteger(StringSubstr(startPart, 2, 2));
      }
      if(StringLen(endPart) >= 4)
      {
         g_sessEndHour = (int)StringToInteger(StringSubstr(endPart, 0, 2));
         g_sessEndMin  = (int)StringToInteger(StringSubstr(endPart, 2, 2));
      }
   }
}

bool IsInSession(datetime t)
{
   datetime shifted = t + InpTzShift * 3600;
   MqlDateTime dt;
   TimeToStruct(shifted, dt);
   int curMin = dt.hour * 60 + dt.min;
   int sMin = g_sessStartHour * 60 + g_sessStartMin;
   int eMin = g_sessEndHour * 60 + g_sessEndMin;
   
   if(sMin < eMin)
   {
      return (curMin >= sMin && curMin < eMin);
   }
   else // crosses midnight
   {
      return (curMin >= sMin || curMin < eMin);
   }
}

//+------------------------------------------------------------------+
double CalcATR(const double &high[], const double &low[],
               const double &close[], int idx, int len)
{
   if(idx < len) return 0.0;
   double sum = 0.0;
   for(int k = idx; k > idx - len; k--)
   {
      double tr = MathMax(high[k] - low[k],
                  MathMax(MathAbs(high[k] - close[k-1]),
                          MathAbs(low[k]  - close[k-1])));
      sum += tr;
   }
   return sum / len;
}

double CalcVolSMA(const long &vol[], int idx, int len)
{
   if(idx < len - 1) return 0.0;
   double sum = 0.0;
   for(int k = idx; k > idx - len; k--) sum += (double)vol[k];
   return sum / len;
}

bool IsPivotHigh(const double &high[], int idx, int len)
{
   int c = idx - len;
   if(c - len < 0) return false;
   double ph = high[c];
   for(int k = c - len; k <= c + len; k++)
      if(k != c && high[k] >= ph) return false;
   return true;
}

bool IsPivotLow(const double &low[], int idx, int len)
{
   int c = idx - len;
   if(c - len < 0) return false;
   double pl = low[c];
   for(int k = c - len; k <= c + len; k++)
      if(k != c && low[k] <= pl) return false;
   return true;
}

bool IsNewSession(const datetime &time[], int idx)
{
   if(idx == 0) return false;
   if(InpSessionMode == SESS_DAILY || InpSessionMode == SESS_WEEKLY || InpSessionMode == SESS_MONTHLY)
   {
      MqlDateTime cur, prev;
      TimeToStruct(time[idx] + InpTzShift * 3600,   cur);
      TimeToStruct(time[idx-1] + InpTzShift * 3600, prev);
      
      if(InpSessionMode == SESS_DAILY)
         return cur.day != prev.day || cur.mon != prev.mon || cur.year != prev.year;
      else if(InpSessionMode == SESS_WEEKLY)
      {
         long t_cur = (long)(time[idx] + InpTzShift * 3600);
         long t_prev = (long)(time[idx-1] + InpTzShift * 3600);
         long week_cur = (t_cur / 86400 + 3) / 7;
         long week_prev = (t_prev / 86400 + 3) / 7;
         return week_cur != week_prev;
      }
      else
         return cur.mon != prev.mon || cur.year != prev.year;
   }
   bool curIn  = IsInSession(time[idx]);
   bool prevIn = IsInSession(time[idx-1]);
   return curIn && !prevIn;
}

//+------------------------------------------------------------------+
ZoneData MakeZone(string kind, double pivHigh, double pivLow, int pivBar,
                  double impF, double volF, double wF, double atr,
                  datetime barTime, const datetime &time[], int ratesTotal)
{
   double level = (kind == "Supply") ? pivHigh : pivLow;
   double h     = atr * InpUniHeight;
   ZoneData z;
   z.top       = level + h / 2.0;
   z.bot       = level - h / 2.0;
   z.mid       = level;
   z.leftTime  = (InpWholeSession && g_curSessStart != 0) ? g_curSessStart : time[MathMax(0, pivBar)];
   z.rightTime = barTime;
   z.bornBar   = pivBar;
   z.lastTouch = pivBar;
   z.sessId    = g_sessId;
   z.sess      = f_sessName(barTime);
   z.kind      = kind;
   z.touches   = 0;
   z.inPrev    = false;
   z.broken    = false;
   z.impF      = impF;
   z.volF      = volF;
   z.wickF     = wF;
   z.score     = 0.0;
   z.boxName   = "";
   z.labName   = "";
   return z;
}

//+------------------------------------------------------------------+
void SortDesc(int &idx[], int cnt)
{
   for(int a = 0; a < cnt - 1; a++)
   {
      int best = a;
      for(int b = a+1; b < cnt; b++)
         if(zones[idx[b]].score > zones[idx[best]].score) best = b;
      if(best != a) { int t = idx[a]; idx[a] = idx[best]; idx[best] = t; }
   }
}

bool ZoneVisible(int zi, int minSess)
{
   return zones[zi].sessId >= minSess && (!InpOnlyStrong || zones[zi].score >= InpStrongThr);
}

int CollectDashZones(string kind, int &out[])
{
   int n = ArraySize(zones), cnt = 0;
   int tmp[];
   for(int i = 0; i < n; i++)
      if(zones[i].kind == kind && !zones[i].broken)
      { ArrayResize(tmp, cnt+1); tmp[cnt++] = i; }
   SortDesc(tmp, cnt);
   ArrayResize(out, cnt);
   ArrayCopy(out, tmp, 0, 0, cnt);
   return cnt;
}

int DisplayedZones(string kind, int sess, int minSess, int &out[])
{
   int n = ArraySize(zones), raw[], rcnt = 0;
   for(int i = 0; i < n; i++)
      if(zones[i].kind == kind && zones[i].sessId == sess && ZoneVisible(i, minSess))
      { ArrayResize(raw, rcnt+1); raw[rcnt++] = i; }
   SortDesc(raw, rcnt);
   int take = MathMin(InpMaxPerSession, rcnt);
   ArrayResize(out, take);
   for(int k = 0; k < take; k++) out[k] = raw[k];
   return take;
}

//+------------------------------------------------------------------+
void DrawRect(string nm, datetime t1, double top, datetime t2, double bot,
              color fill, color border, int bw)
{
   if(ObjectFind(0, nm) < 0) ObjectCreate(0, nm, OBJ_RECTANGLE, 0, t1, top, t2, bot);
   ObjectSetInteger(0, nm, OBJPROP_TIME,  0, t1);
   ObjectSetDouble(0,  nm, OBJPROP_PRICE, 0, top);
   ObjectSetInteger(0, nm, OBJPROP_TIME,  1, t2);
   ObjectSetDouble(0,  nm, OBJPROP_PRICE, 1, bot);
   ObjectSetInteger(0, nm, OBJPROP_COLOR,      border);
   ObjectSetInteger(0, nm, OBJPROP_BGCOLOR,    fill);
   ObjectSetInteger(0, nm, OBJPROP_FILL,       true);
   ObjectSetInteger(0, nm, OBJPROP_WIDTH,      bw);
   ObjectSetInteger(0, nm, OBJPROP_BACK,       true);
   ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, nm, OBJPROP_HIDDEN,     true);
}

void DrawTxt(string nm, datetime t, double price, string txt, color clr, int fs)
{
   if(ObjectFind(0, nm) < 0) ObjectCreate(0, nm, OBJ_TEXT, 0, t, price);
   ObjectSetString(0,  nm, OBJPROP_TEXT,       txt);
   ObjectSetInteger(0, nm, OBJPROP_TIME,   0,  t);
   ObjectSetDouble(0,  nm, OBJPROP_PRICE,  0,  price);
   ObjectSetInteger(0, nm, OBJPROP_COLOR,      clr);
   ObjectSetInteger(0, nm, OBJPROP_FONTSIZE,   fs);
   ObjectSetInteger(0, nm, OBJPROP_ANCHOR,     ANCHOR_CENTER);
   ObjectSetInteger(0, nm, OBJPROP_BACK,       false);
   ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, nm, OBJPROP_HIDDEN,     true);
}

//+------------------------------------------------------------------+
void DrawZone(int zi, int rank, bool isBest)
{
   ZoneData z    = zones[zi];
   bool fresh    = !z.broken && z.touches == 0;
   color base    = (z.kind == "Supply") ? InpSupCol : InpDemCol;

   int tr;
   if(z.broken) tr = InpTrBroken;
   else if(InpSharpenByStr)
      tr = (int)MathRound(InpTrWeak - (InpTrWeak - InpTrStrong) * (z.score / 10.0));
   else
      tr = InpTrStrong;
   if(InpFadeOld && z.sessId != g_sessId && !z.broken) tr = MathMin(tr + 6, 96);
   if(fresh) tr = MathMax(tr - 12, 28);

   int btrTr = isBest ? 0 : (z.broken ? MathMin(InpBorderTransp + 50, 90) : InpBorderTransp);
   int bw    = isBest ? MathMax(InpBorderWidth, 2) : InpBorderWidth;

   if(zones[zi].boxName == "") zones[zi].boxName = NewName("BOX");
   DrawRect(zones[zi].boxName, z.leftTime, z.top, z.rightTime, z.bot,
            ColorFade(base, tr), ColorFade(base, btrTr), bw);

   if(InpShowLabels)
   {
      string tag  = (z.kind == "Supply" ? "S" : "D") + IntegerToString(rank);
      string scStr = DoubleToString(z.score, 1);
      string txt;
      string bestMk = isBest ? "«" : "";
      string stateG = fresh ? "◆ " : "";
      string brkMk  = z.broken ? " ✗" : "";
      if(InpCompactLabels)
         txt = tag + bestMk + " " + stateG + f_stars(z.score) + " " + scStr + brkMk;
      else
         txt = tag + (isBest?"«BEST":"") + "  "
             + DoubleToString(z.bot,_Digits) + ".." + DoubleToString(z.top,_Digits)
             + "  " + f_stars(z.score) + scStr + "/10 " + f_tier(z.score)
             + "  R:" + IntegerToString(z.touches) + "  " + f_state(z.broken,z.touches)
             + "  " + z.sess;

      color tcol = isBest ? (z.kind=="Supply" ? InpBestSupTxt : InpBestDemTxt)
                          : (z.broken ? ColorFade(base,15) : InpNormTxt);

      long lxL = (long)z.leftTime, lxR = (long)z.rightTime;
      datetime lx = (datetime)(lxL + (long)((lxR - lxL) * InpLabelPosFrac));

      if(zones[zi].labName == "") zones[zi].labName = NewName("LBL");
      DrawTxt(zones[zi].labName, lx, z.mid, txt, tcol, f_fontSize(InpLabelSize));
   }
}

void HideZone(int zi)
{
   if(zones[zi].boxName != "")
   {
      DelObj(zones[zi].boxName);
      zones[zi].boxName = "";
   }
   if(zones[zi].labName != "")
   {
      DelObj(zones[zi].labName);
      zones[zi].labName = "";
   }
}

void RenderSide(string kind)
{
   int minSess = MathMax(0, g_sessId - InpSessionsBack + 1);
   int n = ArraySize(zones);
   int gBestIdx = -1; double gBest = -1.0;
   for(int i = 0; i < n; i++)
   {
      ZoneData z = zones[i];
      if(z.kind == kind && !z.broken && ZoneVisible(i, minSess) && z.score > gBest)
         { gBest = z.score; gBestIdx = i; }
   }
   for(int s = g_sessId; s >= minSess; s--)
   {
      int idx[], cnt = 0;
      for(int i = 0; i < n; i++)
         if(zones[i].kind == kind && zones[i].sessId == s && ZoneVisible(i, minSess))
            { ArrayResize(idx, cnt+1); idx[cnt++] = i; }
      SortDesc(idx, cnt);
      int shown = 0;
      for(int k = 0; k < cnt; k++)
      {
         if(shown < InpMaxPerSession) { shown++; DrawZone(idx[k], shown, idx[k]==gBestIdx); }
         else HideZone(idx[k]);
      }
   }
}

//+------------------------------------------------------------------+
int GetDashWidth()
{
   int c0 = 120, c1 = 110, c2 = 50, c3 = 55;
   if(InpDashSize == DASH_SIZE_SMALL)
   {
      c0 = 80; c1 = 100; c2 = 35; c3 = 45;
   }
   else if(InpDashSize == DASH_SIZE_LARGE)
   {
      c0 = 150; c1 = 130; c2 = 60; c3 = 65;
   }
   return c0 + c1 + c2 + c3 + 25;
}

int GetRowHeight()
{
   if(InpDashSize == DASH_SIZE_SMALL) return 12;
   if(InpDashSize == DASH_SIZE_LARGE) return 18;
   return 14;
}

int GetDashFontSize()
{
   if(InpDashSize == DASH_SIZE_SMALL) return 7;
   if(InpDashSize == DASH_SIZE_LARGE) return 10;
   return 8;
}

int GetColX(int col)
{
   int c0 = 120, c1 = 110, c2 = 50, c3 = 55;
   if(InpDashSize == DASH_SIZE_SMALL)
   {
      c0 = 80; c1 = 100; c2 = 35; c3 = 45;
   }
   else if(InpDashSize == DASH_SIZE_LARGE)
   {
      c0 = 150; c1 = 130; c2 = 60; c3 = 65;
   }
   
   bool isLeft = (InpDashCorner == CORNER_LEFT_UPPER || InpDashCorner == CORNER_LEFT_LOWER);
   
   if(isLeft)
   {
      // Left to right: Col 0 -> Col 1 -> Col 2 -> Col 3
      int x = InpDashX + 5;
      if(col == 0) return x;
      x += c0 + 5;
      if(col == 1) return x;
      x += c1 + 5;
      if(col == 2) return x;
      x += c2 + 5;
      return x;
   }
   else
   {
      // Right to left: Col 3 (rightmost) -> Col 2 -> Col 1 -> Col 0 (leftmost)
      int x = InpDashX + 5;
      if(col == 3) return x + c3;
      x += c3 + 5;
      if(col == 2) return x + c2;
      x += c2 + 5;
      if(col == 1) return x + c1;
      x += c1 + 5;
      return x + c0;
   }
}

int GetRowY(int row, int totalRows)
{
   int rh = GetRowHeight();
   bool isLower = (InpDashCorner == CORNER_LEFT_LOWER || InpDashCorner == CORNER_RIGHT_LOWER);
   if(isLower)
   {
      return InpDashY + (totalRows - 1 - row) * rh + 5;
   }
   else
   {
      return InpDashY + row * rh + 5;
   }
}

void DeleteDashObjs()
{
   DelObj(OBJ_PREFIX + "DASH_BG");
   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+"DASH_") == 0) ObjectDelete(0, nm);
   }
}

void DCell(int row, int col, string txt, color clr, int totalRows)
{
   string nm = OBJ_PREFIX + "DASH_r" + IntegerToString(row) + "c" + IntegerToString(col);
   if(ObjectFind(0,nm) < 0)
   {
      ObjectCreate(0, nm, OBJ_LABEL, 0, 0, 0);
      ObjectSetInteger(0, nm, OBJPROP_SELECTABLE,false);
      ObjectSetInteger(0, nm, OBJPROP_HIDDEN,    true);
   }
   ObjectSetInteger(0, nm, OBJPROP_CORNER,    InpDashCorner);
   ObjectSetInteger(0, nm, OBJPROP_FONTSIZE,  GetDashFontSize());
   ObjectSetInteger(0, nm, OBJPROP_XDISTANCE, GetColX(col));
   ObjectSetInteger(0, nm, OBJPROP_YDISTANCE, GetRowY(row, totalRows));
   ObjectSetString(0,  nm, OBJPROP_TEXT,      txt == "" ? " " : txt);
   ObjectSetInteger(0, nm, OBJPROP_COLOR,     clr);
}

void BuildDashboard(double curClose, double atr,
                    MagStatData &cs[], int csCnt)
{
   if(!g_showDashState || !InpShowDash)
   {
      DeleteDashObjs();
      return;
   }
   
   int   mp  = InpMaxPerSession;
   int dr0 = 2 + mp;
   int mHdr = dr0 + 1 + mp;
   int cHdr = mHdr + 1;
   int fHdr = cHdr + 1 + InpMagMaxPerSess;
   int totalRows = fHdr + 4;
   
   // Create or update background frame
   string bgName = OBJ_PREFIX + "DASH_BG";
   if(ObjectFind(0, bgName) < 0)
   {
      ObjectCreate(0, bgName, OBJ_RECTANGLE_LABEL, 0, 0, 0);
      ObjectSetInteger(0, bgName, OBJPROP_SELECTABLE, false);
      ObjectSetInteger(0, bgName, OBJPROP_HIDDEN, true);
      ObjectSetInteger(0, bgName, OBJPROP_BGCOLOR, C'12,15,20');
      ObjectSetInteger(0, bgName, OBJPROP_COLOR, C'58,63,75');
      ObjectSetInteger(0, bgName, OBJPROP_BORDER_TYPE, BORDER_FLAT);
      ObjectSetInteger(0, bgName, OBJPROP_WIDTH, 1);
      ObjectSetInteger(0, bgName, OBJPROP_BACK, false);
   }
   int totalWidth = GetDashWidth();
   int totalHeight = 10 + totalRows * GetRowHeight();
   ObjectSetInteger(0, bgName, OBJPROP_CORNER, InpDashCorner);
   ObjectSetInteger(0, bgName, OBJPROP_XDISTANCE, InpDashX);
   ObjectSetInteger(0, bgName, OBJPROP_YDISTANCE, InpDashY);
   ObjectSetInteger(0, bgName, OBJPROP_XSIZE, totalWidth);
   ObjectSetInteger(0, bgName, OBJPROP_YSIZE, totalHeight);

   color dim = ColorFade(InpNormTxt, 50);

   DCell(0,0,"INTRADAY SD+MAG", InpNormTxt, totalRows);
   DCell(0,1,Symbol(),          ColorFade(InpNormTxt,35), totalRows);
   DCell(0,2,"score",           dim, totalRows);
   DCell(0,3,"state",           dim, totalRows);

   DCell(1,0,"SUPPLY",    InpSupCol, totalRows);
   DCell(1,1,"active", ColorFade(InpNormTxt,55), totalRows);
   DCell(1,2," ",dim, totalRows); DCell(1,3," ",dim, totalRows);

   int sIdx[]; int sCnt = CollectDashZones("Supply", sIdx);
   for(int k = 0; k < mp; k++)
   {
      int row = 2 + k;
      if(k < sCnt)
      {
         ZoneData z = zones[sIdx[k]];
         bool fr = !z.broken && z.touches==0;
         color tc = z.broken ? ColorFade(InpSupCol,45) : InpSupCol;
         color sc = z.broken ? ColorFade(InpNormTxt,45) : (fr ? InpBestSupTxt : ColorFade(InpNormTxt,12));
         DCell(row,0,"S"+IntegerToString(k+1)+(fr?" ◆":""),tc, totalRows);
         DCell(row,1,DoubleToString(z.bot,_Digits)+".."+DoubleToString(z.top,_Digits),ColorFade(InpNormTxt,12), totalRows);
         DCell(row,2,DoubleToString(z.score,1),tc, totalRows);
         DCell(row,3,f_state(z.broken,z.touches),sc, totalRows);
      }
      else { DCell(row,0," ",dim, totalRows); DCell(row,1," ",dim, totalRows); DCell(row,2," ",dim, totalRows); DCell(row,3," ",dim, totalRows); }
   }

   DCell(dr0,0,"DEMAND",   InpDemCol, totalRows);
   DCell(dr0,1,"active",ColorFade(InpNormTxt,55), totalRows);
   DCell(dr0,2," ",dim, totalRows); DCell(dr0,3," ",dim, totalRows);

   int dIdx[]; int dCnt = CollectDashZones("Demand", dIdx);
   for(int k = 0; k < mp; k++)
   {
      int row = dr0 + 1 + k;
      if(k < dCnt)
      {
         ZoneData z = zones[dIdx[k]];
         bool fr = !z.broken && z.touches==0;
         color tc = z.broken ? ColorFade(InpDemCol,45) : InpDemCol;
         color sc = z.broken ? ColorFade(InpNormTxt,45) : (fr ? InpBestDemTxt : ColorFade(InpNormTxt,12));
         DCell(row,0,"D"+IntegerToString(k+1)+(fr?" ◆":""),tc, totalRows);
         DCell(row,1,DoubleToString(z.bot,_Digits)+".."+DoubleToString(z.top,_Digits),ColorFade(InpNormTxt,12), totalRows);
         DCell(row,2,DoubleToString(z.score,1),tc, totalRows);
         DCell(row,3,f_state(z.broken,z.touches),sc, totalRows);
      }
      else { DCell(row,0," ",dim, totalRows); DCell(row,1," ",dim, totalRows); DCell(row,2," ",dim, totalRows); DCell(row,3," ",dim, totalRows); }
   }

   DCell(mHdr,0,"MAGNET PULLS",InpMagColor, totalRows);
   DCell(mHdr,1,"src→tgt",ColorFade(InpNormTxt,55), totalRows);
   DCell(mHdr,2," ",dim, totalRows);
   DCell(mHdr,3,IntegerToString(csCnt)+" active",dim, totalRows);

   DCell(cHdr,0,"route",dim, totalRows); DCell(cHdr,1,"pull/10",dim, totalRows);
   DCell(cHdr,2,"target",dim, totalRows); DCell(cHdr,3,"to price",dim, totalRows);

   double downStr=0, upStr=0;
   int nearI=-1; double nearD=1e18;

   for(int k = 0; k < InpMagMaxPerSess; k++)
   {
      int row = cHdr + 1 + k;
      if(k < csCnt)
      {
         MagStatData ms = cs[k];
         string route = (ms.down?"▼ ":"▲ ") + ms.srcTag + "→" + ms.tgtTag + (ms.tgtFresh?"◆":"");
         double distPct = curClose != 0 ? (ms.tgtMid - curClose)/curClose*100.0 : 0.0;
         double distAtr = atr > 0 ? MathAbs(ms.tgtMid - curClose)/atr : 0.0;
         DCell(row,0,route,InpMagColor, totalRows);
         DCell(row,1,DoubleToString(ms.pull10,1),InpMagColor, totalRows);
         DCell(row,2,DoubleToString(ms.tgtMid,_Digits),ColorFade(InpNormTxt,10), totalRows);
         DCell(row,3,DoubleToString(distPct,2)+"%·"+DoubleToString(distAtr,1)+"x",ColorFade(InpNormTxt,10), totalRows);
         if(ms.down) downStr+=ms.pull10; else upStr+=ms.pull10;
         double dd=MathAbs(ms.tgtMid-curClose);
         if(dd<nearD){nearD=dd;nearI=k;}
      }
      else
      {
         DCell(row,0,(k==0&&csCnt==0)?"-- none --":" ",dim, totalRows);
         DCell(row,1," ",dim, totalRows); DCell(row,2," ",dim, totalRows); DCell(row,3," ",dim, totalRows);
      }
   }

   DCell(fHdr,0,"FIELD",InpNormTxt, totalRows); DCell(fHdr,1," ",dim, totalRows); DCell(fHdr,2," ",dim, totalRows); DCell(fHdr,3," ",dim, totalRows);

   string netTx; color netC;
   if(csCnt==0)         {netTx="--";       netC=dim;}
   else if(downStr>upStr){netTx="▼ DOWN";   netC=InpSupCol;}
   else if(upStr>downStr){netTx="▲ UP";     netC=InpDemCol;}
   else                  {netTx="BALANCED"; netC=InpNormTxt;}

   DCell(fHdr+1,0," Net pull",ColorFade(InpNormTxt,35), totalRows);
   DCell(fHdr+1,1,netTx,netC, totalRows);
   DCell(fHdr+1,2,"Σ▼"+DoubleToString(downStr,1),InpSupCol, totalRows);
   DCell(fHdr+1,3,"Σ▲"+DoubleToString(upStr,1),InpDemCol, totalRows);

   string nrRoute="--",nrPx=" ",nrDist=" ";
   if(nearI>=0)
   {
      MagStatData ms=cs[nearI];
      nrRoute=(ms.down?"▼ ":"▲ ")+ms.tgtTag;
      nrPx=DoubleToString(ms.tgtMid,_Digits);
      nrDist=DoubleToString(atr>0?MathAbs(ms.tgtMid-curClose)/atr:0.0,1)+"x";
   }
   DCell(fHdr+2,0," Nearest",ColorFade(InpNormTxt,35), totalRows);
   DCell(fHdr+2,1,nrRoute,InpMagColor, totalRows);
   DCell(fHdr+2,2,nrPx,ColorFade(InpNormTxt,10), totalRows);
   DCell(fHdr+2,3,nrDist,ColorFade(InpNormTxt,10), totalRows);

   DCell(fHdr+3,0," Active",ColorFade(InpNormTxt,35), totalRows);
   DCell(fHdr+3,1,IntegerToString(csCnt)+" pulls",ColorFade(InpNormTxt,10), totalRows);
   DCell(fHdr+3,2,IntegerToString(MathMin(mp,sCnt))+"S",InpSupCol, totalRows);
   DCell(fHdr+3,3,IntegerToString(MathMin(mp,dCnt))+"D",InpDemCol, totalRows);

   g_lastClose = curClose;
   g_lastAtr   = atr;
   ArrayResize(g_lastCs, csCnt);
   for(int i = 0; i < csCnt; i++)
   {
      g_lastCs[i] = cs[i];
   }
   g_lastCsCnt = csCnt;
}

//+------------------------------------------------------------------+
bool BuildMagnets(double atr, double curClose, int curBar,
                  MagStatData &csOut[], int &csCnt)
{
   int mTotal = ArraySize(magnets);
   for(int mi=0;mi<mTotal;mi++){DelObj(magnets[mi].lnName);DelObj(magnets[mi].labName);}
   ArrayResize(magnets,0);
   ArrayResize(magStats,0);
   csCnt=0;
   bool freshHit=false;
   if(!InpMagnetsOn) return false;

   int minSessM=MathMax(0,g_sessId-InpMagSessBack+1);
   int minSessV=MathMax(0,g_sessId-InpSessionsBack+1);

   for(int s=g_sessId;s>=minSessM;s--)
   {
      int supD[],demD[];
      int sDCnt=DisplayedZones("Supply",s,minSessV,supD);
      int dDCnt=DisplayedZones("Demand",s,minSessV,demD);

      int cSrc[],cTgt[],cSR[],cTR[]; double cScr[]; int nc=0;

      // Supply → Demand (down magnets)
      for(int a=0;a<sDCnt;a++)
      {
         int si=supD[a]; ZoneData src=zones[si];
         if(!src.broken && src.touches>=InpMagMinTouch && src.score>=InpMagnetThr)
         {
            int ti=-1,tp=-1; double bd=1e18;
            for(int b=0;b<dDCnt;b++)
            {
               int di=demD[b]; ZoneData d0=zones[di];
               if(!d0.broken && d0.mid<src.mid && d0.score>=InpMagTargetThr)
               {
                  double dd=src.mid-d0.mid;
                  if(dd<=atr*InpMagMaxReach && dd<bd){bd=dd;ti=di;tp=b;}
               }
            }
            if(ti>=0)
            {
               ArrayResize(cSrc,nc+1);cSrc[nc]=si;
               ArrayResize(cTgt,nc+1);cTgt[nc]=ti;
               ArrayResize(cScr,nc+1);cScr[nc]=src.score;
               ArrayResize(cSR, nc+1);cSR[nc]=a+1;
               ArrayResize(cTR, nc+1);cTR[nc]=tp+1;
               nc++;
            }
         }
      }

      // Demand → Supply (up magnets)
      for(int a=0;a<dDCnt;a++)
      {
         int di=demD[a]; ZoneData src=zones[di];
         if(!src.broken && src.touches>=InpMagMinTouch && src.score>=InpMagnetThr)
         {
            int ti=-1,tp=-1; double bd=1e18;
            for(int b=0;b<sDCnt;b++)
            {
               int si2=supD[b]; ZoneData s0=zones[si2];
               if(!s0.broken && s0.mid>src.mid && s0.score>=InpMagTargetThr)
               {
                  double dd=s0.mid-src.mid;
                  if(dd<=atr*InpMagMaxReach && dd<bd){bd=dd;ti=si2;tp=b;}
               }
            }
            if(ti>=0)
            {
               ArrayResize(cSrc,nc+1);cSrc[nc]=di;
               ArrayResize(cTgt,nc+1);cTgt[nc]=ti;
               ArrayResize(cScr,nc+1);cScr[nc]=src.score;
               ArrayResize(cSR, nc+1);cSR[nc]=a+1;
               ArrayResize(cTR, nc+1);cTR[nc]=tp+1;
               nc++;
            }
         }
      }

      // Sort desc by score
      for(int x=0;x<nc-1;x++)
      {
         int best=x;
         for(int y=x+1;y<nc;y++) if(cScr[y]>cScr[best]) best=y;
         if(best!=x)
         {
            double f0=cScr[x];cScr[x]=cScr[best];cScr[best]=f0;
            int i0;
            i0=cSrc[x];cSrc[x]=cSrc[best];cSrc[best]=i0;
            i0=cTgt[x];cTgt[x]=cTgt[best];cTgt[best]=i0;
            i0=cSR[x]; cSR[x]=cSR[best];  cSR[best]=i0;
            i0=cTR[x]; cTR[x]=cTR[best];  cTR[best]=i0;
         }
      }

      int drawn=0;
      for(int x=0;x<nc&&drawn<InpMagMaxPerSess;x++)
      {
         ZoneData src=zones[cSrc[x]], tgt=zones[cTgt[x]];
         bool   dn     =(src.kind=="Supply");
         double pull10 =MathMin(10.0,MathMax(0.0,0.5*src.score+0.5*tgt.score));
         double pullPct=100.0/(1.0+MathExp(-(pull10-InpMagProbMid)/InpMagProbSlope));
         double frac   =MathMin(0.92,InpMagAnchorFrac+drawn*0.16);
         long lxL=(long)src.leftTime,lxR=(long)src.rightTime;
         datetime lx=(datetime)(lxL+(long)((lxR-lxL)*frac));
         if(lx<src.leftTime) lx=src.leftTime;
         if(lx>src.rightTime) lx=src.rightTime;

         string lnNm=NewName("MAGLN");
         if(ObjectFind(0,lnNm)<0) ObjectCreate(0,lnNm,OBJ_ARROWED_LINE,0,lx,src.mid,lx,tgt.mid);
         ObjectSetInteger(0,lnNm,OBJPROP_TIME,  0,lx);
         ObjectSetDouble(0, lnNm,OBJPROP_PRICE, 0,src.mid);
         ObjectSetInteger(0,lnNm,OBJPROP_TIME,  1,lx);
         ObjectSetDouble(0, lnNm,OBJPROP_PRICE, 1,tgt.mid);
         ObjectSetInteger(0,lnNm,OBJPROP_COLOR, InpMagColor);
         ObjectSetInteger(0,lnNm,OBJPROP_WIDTH, 2);
         ObjectSetInteger(0,lnNm,OBJPROP_BACK,  false);
         ObjectSetInteger(0,lnNm,OBJPROP_SELECTABLE,false);
         ObjectSetInteger(0,lnNm,OBJPROP_HIDDEN,true);

         string mTxt=(dn?"▼":"▲")+" MAGNET "+DoubleToString(pull10,1)+" · "+DoubleToString(pullPct,0)+"%";
         string labNm=NewName("MAGLAB");
         DrawTxt(labNm,lx,tgt.mid,mTxt,InpMagColor,f_fontSize(InpMagLblSize));

         MagnetData mg; mg.lnName=lnNm; mg.labName=labNm;
         int msz=ArraySize(magnets); ArrayResize(magnets,msz+1); magnets[msz]=mg;

         MagStatData ms;
         ms.sessId  =s; ms.down=dn;
         ms.srcTag  =(dn?"S":"D")+IntegerToString(cSR[x]);
         ms.tgtTag  =(dn?"D":"S")+IntegerToString(cTR[x]);
         ms.tgtFresh=!tgt.broken&&tgt.touches==0;
         ms.tgtMid  =tgt.mid; ms.srcMid=src.mid; ms.pull10=pull10;
         int ssz=ArraySize(magStats); ArrayResize(magStats,ssz+1); magStats[ssz]=ms;

         drawn++;
         if(src.lastTouch==curBar) freshHit=true;
      }
   }

   // Collect current-session stats
   int stTotal=ArraySize(magStats);
   for(int i=0;i<stTotal;i++)
      if(magStats[i].sessId==g_sessId)
      { ArrayResize(csOut,csCnt+1); csOut[csCnt++]=magStats[i]; }

   return freshHit;
}

//+------------------------------------------------------------------+
int OnInit()
{
   ArrayResize(zones,0); ArrayResize(magnets,0); ArrayResize(magStats,0);
   g_sessId=0; g_curSessStart=0; g_lastSupBar=0; g_lastDemBar=0; g_objCounter=0;
   g_lastAlertTime_NewSup = 0;
   g_lastAlertTime_NewDem = 0;
   g_lastAlertTime_NearSup = 0;
   g_lastAlertTime_NearDem = 0;
   g_lastAlertTime_NearStrong = 0;
   g_lastAlertTime_Magnet = 0;
   g_showDashState = true;
   g_lastClose = 0;
   g_lastAtr = 0;
   ArrayResize(g_lastCs, 0);
   g_lastCsCnt = 0;
   DeleteAllOurs();
   ParseSessionSettings();
   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason) { DeleteAllOurs(); ChartRedraw(0); }

//+------------------------------------------------------------------+
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(prev_calculated <= 0)
   {
      ArrayResize(zones,0); 
      ArrayResize(magnets,0); 
      ArrayResize(magStats,0);
      g_sessId=0; 
      g_curSessStart=0; 
      g_lastSupBar=0; 
      g_lastDemBar=0; 
      g_objCounter=0;
      g_lastAlertTime_NewSup = 0;
      g_lastAlertTime_NewDem = 0;
      g_lastAlertTime_NearSup = 0;
      g_lastAlertTime_NearDem = 0;
      g_lastAlertTime_NearStrong = 0;
      g_lastAlertTime_Magnet = 0;
      g_showDashState = true;
      g_lastClose = 0;
      g_lastAtr = 0;
      ArrayResize(g_lastCs, 0);
      g_lastCsCnt = 0;
      DeleteAllOurs();
      ParseSessionSettings();
   }

   if(rates_total < InpSwingLen*2 + InpAtrLen + 2) return prev_calculated;

   ArraySetAsSeries(time,  false);
   ArraySetAsSeries(open,  false);
   ArraySetAsSeries(high,  false);
   ArraySetAsSeries(low,   false);
   ArraySetAsSeries(close, false);
   ArraySetAsSeries(volume,false);

   int startBar = (prev_calculated <= 0) ? (InpSwingLen*2 + InpAtrLen + 1) : MathMax(0, prev_calculated-1);

   for(int i = startBar; i < rates_total; i++)
   {
      bool isLast = (i == rates_total - 1);

      // Session
      bool newSess = IsNewSession(time, i);
      if(newSess)      { g_sessId++; g_curSessStart = time[i]; }
      else if(g_curSessStart == 0) g_curSessStart = time[i];

      double atr = CalcATR(high, low, close, i, InpAtrLen);
      if(atr <= 0.0) { if(isLast) return rates_total; continue; }

      // Zone creation on confirmed bars (not last tick)
      if(!isLast)
      {
         double volBase = CalcVolSMA(volume, i, InpVolLen);

         if(IsPivotHigh(high, i, InpSwingLen))
         {
            int pv = i - InpSwingLen;
            if((pv - g_lastSupBar) >= InpMinZoneDist &&
               (high[pv] - low[pv]) >= atr * InpMinSwingATR)
            {
               double loSw = low[pv+1];
               for(int k = pv+1; k <= i; k++) if(low[k]<loSw) loSw=low[k];
               double dep  = high[pv] - loSw;
               double impF = MathMin(MathMax(dep,0.0)/MathMax(atr*InpImpulseNorm,1e-9),1.0);
               double volF = volBase>0 ? MathMin((double)volume[pv]/volBase,1.0) : 0.0;
               double rng  = MathMax(high[pv]-low[pv],1e-9);
               double wF   = MathMin(MathMax(high[pv]-MathMax(open[pv],close[pv]),0.0)/rng,1.0);
               ZoneData z  = MakeZone("Supply",high[pv],low[pv],pv,impF,volF,wF,atr,time[i],time,rates_total);
               bool merged = false;
               if(InpMergeOverlap)
               {
                  int zn=ArraySize(zones);
                  for(int zi=zn-1;zi>=MathMax(0,zn-12);zi--)
                     if(zones[zi].kind=="Supply"&&!zones[zi].broken&&zones[zi].sessId==g_sessId
                        &&f_overlap(z.top,z.bot,zones[zi].top,zones[zi].bot))
                     { zones[zi].top=MathMax(z.top,zones[zi].top);
                       zones[zi].bot=MathMin(z.bot,zones[zi].bot);
                       zones[zi].mid=(zones[zi].top+zones[zi].bot)/2.0;
                       merged=true; break; }
               }
               if(!merged)
               { int zn=ArraySize(zones); ArrayResize(zones,zn+1); zones[zn]=z; g_lastSupBar=pv; }
            }
         }

         if(IsPivotLow(low, i, InpSwingLen))
         {
            int pv = i - InpSwingLen;
            if((pv - g_lastDemBar) >= InpMinZoneDist &&
               (high[pv] - low[pv]) >= atr * InpMinSwingATR)
            {
               double hiSw = high[pv+1];
               for(int k = pv+1; k <= i; k++) if(high[k]>hiSw) hiSw=high[k];
               double dep  = hiSw - low[pv];
               double volBase2 = CalcVolSMA(volume, i, InpVolLen);
               double impF = MathMin(MathMax(dep,0.0)/MathMax(atr*InpImpulseNorm,1e-9),1.0);
               double volF = volBase2>0 ? MathMin((double)volume[pv]/volBase2,1.0) : 0.0;
               double rng  = MathMax(high[pv]-low[pv],1e-9);
               double wF   = MathMin(MathMax(MathMin(open[pv],close[pv])-low[pv],0.0)/rng,1.0);
               ZoneData z  = MakeZone("Demand",high[pv],low[pv],pv,impF,volF,wF,atr,time[i],time,rates_total);
               bool merged = false;
               if(InpMergeOverlap)
               {
                  int zn=ArraySize(zones);
                  for(int zi=zn-1;zi>=MathMax(0,zn-12);zi--)
                     if(zones[zi].kind=="Demand"&&!zones[zi].broken&&zones[zi].sessId==g_sessId
                        &&f_overlap(z.top,z.bot,zones[zi].top,zones[zi].bot))
                     { zones[zi].top=MathMax(z.top,zones[zi].top);
                       zones[zi].bot=MathMin(z.bot,zones[zi].bot);
                       zones[zi].mid=(zones[zi].top+zones[zi].bot)/2.0;
                       merged=true; break; }
               }
               if(!merged)
               { int zn=ArraySize(zones); ArrayResize(zones,zn+1); zones[zn]=z; g_lastDemBar=pv; }
            }
         }
      }

      // Per-bar: update rightTime, touches, broken
      int zTotal = ArraySize(zones);
      for(int zi = 0; zi < zTotal; zi++)
      {
         if(zones[zi].sessId == g_sessId) zones[zi].rightTime = time[i];
         if(!zones[zi].broken)
         {
            bool inZone = high[i] >= zones[zi].bot && low[i] <= zones[zi].top;
            if(inZone && !zones[zi].inPrev)
            {
               zones[zi].touches++;
               zones[zi].lastTouch = i;
               double wk = (zones[zi].kind=="Supply")
                  ? MathMax(high[i]-MathMax(open[i],close[i]),0.0)
                  : MathMax(MathMin(open[i],close[i])-low[i],0.0);
               double rng = MathMax(high[i]-low[i],1e-9);
               zones[zi].wickF = MathMin(zones[zi].wickF + wk/rng, 1.0);
            }
            zones[zi].inPrev = inZone;
             bool brk = (zones[zi].kind=="Supply")
                ? ((InpInvMethod==INV_CLOSE?close[i]:high[i]) > zones[zi].top)
                : ((InpInvMethod==INV_CLOSE?close[i]:low[i])  < zones[zi].bot);
            if(brk) zones[zi].broken = true;
         }
      }

      if(isLast)
      {
         // Prune old zones
         int minKeep = MathMax(0, g_sessId - InpSessionsBack - 1);
         zTotal = ArraySize(zones);
         int nPrune = MathMax(0, zTotal - 480);
         for(int zi = 0; zi < zTotal && zones[zi].sessId < minKeep; zi++) nPrune = MathMax(nPrune, zi+1);
         for(int p = 0; p < nPrune && ArraySize(zones) > 0; p++)
         {
            DelObj(zones[0].boxName); DelObj(zones[0].labName);
            int cur = ArraySize(zones);
            for(int zi = 0; zi < cur-1; zi++) zones[zi] = zones[zi+1];
            ArrayResize(zones, cur-1);
         }

         // Score
         zTotal = ArraySize(zones);
         for(int zi = 0; zi < zTotal; zi++)
            zones[zi].score = f_score(zones[zi].impF, zones[zi].volF, zones[zi].wickF,
                                       zones[zi].touches, i - zones[zi].lastTouch);

         RenderSide("Supply");
         RenderSide("Demand");

         MagStatData cs[]; int csCnt=0;
         bool evMagnet = BuildMagnets(atr, close[i], i, cs, csCnt);

         BuildDashboard(close[i], atr, cs, csCnt);
         DrawToggleButton();

         if(InpAlertMode != ALERT_NONE)
         {
            bool checkAlerts = false;
            int evalIdx = i;
            static datetime s_prevBarTime = 0;
            
            if(InpAlertMode == ALERT_CURRENT)
            {
               checkAlerts = true;
               evalIdx = i;
            }
            else if(InpAlertMode == ALERT_CLOSED)
            {
               if(time[i] != s_prevBarTime && s_prevBarTime != 0)
               {
                  checkAlerts = true;
                  evalIdx = i - 1;
               }
            }
            s_prevBarTime = time[i];

            if(checkAlerts && evalIdx >= 0 && evalIdx < rates_total)
            {
               bool evNewSup=false, evNewDem=false;
               int pvBar = evalIdx - 1 - InpSwingLen;
               if(pvBar >= 0)
               {
                  for(int zi = 0; zi < zTotal; zi++)
                  {
                     if(zones[zi].bornBar == pvBar)
                     { if(zones[zi].kind=="Supply") evNewSup=true; else evNewDem=true; }
                  }
               }
               double prox = close[evalIdx] * InpAlertProx / 100.0;
               bool nearSup=false, nearDem=false, nearStrong=false;
               for(int zi = 0; zi < zTotal; zi++)
               {
                  if(!zones[zi].broken && MathAbs(close[evalIdx]-zones[zi].mid) <= prox)
                  {
                     if(zones[zi].kind=="Supply") nearSup=true; else nearDem=true;
                     if(zones[zi].score >= InpInstThr) nearStrong=true;
                  }
               }
               if(evNewSup && time[evalIdx] > g_lastAlertTime_NewSup)
               {
                  Alert(Symbol()," new supply zone");
                  g_lastAlertTime_NewSup = time[evalIdx];
               }
               if(evNewDem && time[evalIdx] > g_lastAlertTime_NewDem)
               {
                  Alert(Symbol()," new demand zone");
                  g_lastAlertTime_NewDem = time[evalIdx];
               }
               if(nearSup && time[evalIdx] > g_lastAlertTime_NearSup)
               {
                  Alert(Symbol()," price approaching supply");
                  g_lastAlertTime_NearSup = time[evalIdx];
               }
               if(nearDem && time[evalIdx] > g_lastAlertTime_NearDem)
               {
                  Alert(Symbol()," price approaching demand");
                  g_lastAlertTime_NearDem = time[evalIdx];
               }
               if(nearStrong && time[evalIdx] > g_lastAlertTime_NearStrong)
               {
                  Alert(Symbol()," price approaching a strong zone");
                  g_lastAlertTime_NearStrong = time[evalIdx];
               }
               if(evMagnet && time[evalIdx] > g_lastAlertTime_Magnet)
               {
                  Alert(Symbol()," strong zone re-tested - magnet pull active");
                  g_lastAlertTime_Magnet = time[evalIdx];
               }
            }
         }

         ChartRedraw(0);
      }
   }
   return rates_total;
}

//+------------------------------------------------------------------+
//| Draw interactive toggle button                                   |
//+------------------------------------------------------------------+
void DrawToggleButton()
{
   string btnName = OBJ_PREFIX + "TOGGLE_BTN";
   if(!InpShowToggleBtn)
   {
      DelObj(btnName);
      return;
   }

   if(ObjectFind(0, btnName) < 0)
   {
      ObjectCreate(0, btnName, OBJ_BUTTON, 0, 0, 0);
      ObjectSetInteger(0, btnName, OBJPROP_SELECTABLE, false);
      ObjectSetInteger(0, btnName, OBJPROP_HIDDEN, true);
      ObjectSetInteger(0, btnName, OBJPROP_WIDTH, 1);
      ObjectSetInteger(0, btnName, OBJPROP_FONTSIZE, 8);
      ObjectSetString(0, btnName, OBJPROP_FONT, "Lucida Console");
   }

   ObjectSetInteger(0, btnName, OBJPROP_CORNER, InpBtnCorner);
   ObjectSetInteger(0, btnName, OBJPROP_XDISTANCE, InpBtnX);
   ObjectSetInteger(0, btnName, OBJPROP_YDISTANCE, InpBtnY);
   ObjectSetInteger(0, btnName, OBJPROP_XSIZE, 100);
   ObjectSetInteger(0, btnName, OBJPROP_YSIZE, 22);

   if(g_showDashState)
   {
      ObjectSetString(0, btnName, OBJPROP_TEXT, "Dashboard: ON");
      ObjectSetInteger(0, btnName, OBJPROP_BGCOLOR, C'39,192,122');
      ObjectSetInteger(0, btnName, OBJPROP_COLOR, C'255,255,255');
      ObjectSetInteger(0, btnName, OBJPROP_STATE, false);
   }
   else
   {
      ObjectSetString(0, btnName, OBJPROP_TEXT, "Dashboard: OFF");
      ObjectSetInteger(0, btnName, OBJPROP_BGCOLOR, C'58,63,75');
      ObjectSetInteger(0, btnName, OBJPROP_COLOR, C'200,200,200');
      ObjectSetInteger(0, btnName, OBJPROP_STATE, false);
   }
}

//+------------------------------------------------------------------+
//| Handle chart events (clicks on toggle button)                    |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   if(id == CHARTEVENT_OBJECT_CLICK)
   {
      if(sparam == OBJ_PREFIX + "TOGGLE_BTN")
      {
         g_showDashState = !g_showDashState;
         DrawToggleButton();
         if(g_showDashState)
         {
            BuildDashboard(g_lastClose, g_lastAtr, g_lastCs, g_lastCsCnt);
         }
         else
         {
            DeleteDashObjs();
         }
         ChartRedraw(0);
      }
   }
}
//+------------------------------------------------------------------+
