//+------------------------------------------------------------------+
//|                                   QuantMasterLiquidityEvents.mq4 |
//|                                  Copyright 2025, Google Deepmind |
//|                                       Translated from Pine Script|
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Google Deepmind"
#property link      "https://www.google.com"
#property version   "1.00"
#property strict

#property indicator_chart_window
#property indicator_buffers 5
#property indicator_color1  clrNONE
#property indicator_color2  clrNONE
#property indicator_color3  clrNONE
#property indicator_color4  clrNONE
#property indicator_color5  clrNONE

//--- Constants
#define PREFIX "QMLE_"

//--- Enums
enum ENUM_SRC_VOL {
   SRC_DELTA,  // Volume Delta
   SRC_VOLUME  // Total Volume
};

enum ENUM_DASH_POS {
   POS_TOP_RIGHT,    // Top Right
   POS_BOTTOM_RIGHT, // Bottom Right
   POS_BOTTOM_LEFT,  // Bottom Left
   POS_TOP_LEFT      // Top Left
};

enum ENUM_DASH_SIZE {
   SIZE_SMALL,  // Small
   SIZE_NORMAL, // Normal
   SIZE_LARGE,  // Large
   SIZE_HUGE    // Huge
};

enum ENUM_ALERT_MODE {
   ALERT_OFF,       // No Alerts
   ALERT_CURRENT,   // Alert on Current Bar
   ALERT_CLOSED     // Alert on Closed Bar
};

//--- Inputs
input ENUM_SRC_VOL InpSrcVol = SRC_DELTA;      // Volume Source
input ENUM_TIMEFRAMES InpTFLower = PERIOD_M1;  // Intrabar Precision (Trend Timeframe)
input int InpLenStat = 60;                     // Statistical Lookback (20-...)

input double InpZVolThresh = 2.0;              // Min Volume Z-Score (sigma)
input double InpZPxThresh = 2.0;               // Min Price Z-Score (sigma)
input bool InpUseDiv = true;                   // Highlight CVD Divergences

input bool InpShowBull = true;                 // Show Bullish Events
input bool InpShowBear = true;                 // Show Bearish Events
input bool InpScaleSize = true;                // Dynamic Bubble Size
input bool InpShowGhosts = true;               // Show Liquidity Memory (Ghost Lines)

input bool InpShowDash = true;                 // Show Info Dashboard
input ENUM_DASH_POS InpDashPos = POS_TOP_RIGHT;// Position
input ENUM_DASH_SIZE InpDashSize = SIZE_NORMAL;// Size

input ENUM_ALERT_MODE InpAlertMode = ALERT_CURRENT; // Alert Mode

//--- Buffers
double ValBuffer[];    // Raw Value
double ZVolBuffer[];   // Volume Z-Score
double ZPxBuffer[];    // Price Z-Score
double EventTypeBuffer[]; // 0=None, 1=Bull, 2=Bear, 3=AbsorbBuy, 4=AbsorbSell
double AvgBodyBuffer[]; 

//--- Global Variables
struct LogEvent {
   datetime time;
   int type;
   double z_score;
   double price;
};
LogEvent g_event_log[]; 

// Colors
color col_bull      = C'0,230,118';   // #00E676
color col_bear      = C'255,23,68';   // #FF1744
color col_absorb_bu = C'41,98,255';   // #2962FF
color col_absorb_be = C'255,109,0';   // #FF6D00

datetime g_last_alert_time = 0;

//+------------------------------------------------------------------+
//| Custom Indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   SetIndexBuffer(0, ValBuffer);
   SetIndexStyle(0, DRAW_NONE);
   SetIndexBuffer(1, ZVolBuffer);
   SetIndexStyle(1, DRAW_NONE);
   SetIndexBuffer(2, ZPxBuffer);
   SetIndexStyle(2, DRAW_NONE);
   SetIndexBuffer(3, EventTypeBuffer);
   SetIndexStyle(3, DRAW_NONE);
   SetIndexBuffer(4, AvgBodyBuffer);
   SetIndexStyle(4, DRAW_NONE);

   if(InpLenStat < 2) {
      Alert("Statistical Lookback must be >= 2");
      return(INIT_FAILED);
   }

   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Custom Indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ObjectsDeleteAll(0, PREFIX);
   Comment("");
  }

//+------------------------------------------------------------------+
//| Helper: Format Volume                                            |
//+------------------------------------------------------------------+
string FormatVolume(double vol)
  {
   double abs_vol = MathAbs(vol);
   if(abs_vol >= 1e9) return StringFormat("%.2fB", abs_vol / 1e9);
   if(abs_vol >= 1e6) return StringFormat("%.2fM", abs_vol / 1e6);
   if(abs_vol >= 1e3) return StringFormat("%.0fK", abs_vol / 1e3);
   return StringFormat("%.0f", abs_vol);
  }

//+------------------------------------------------------------------+
//| Custom Indicator iteration function                              |
//+------------------------------------------------------------------+
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 < InpLenStat + 1) return(0);

   int limit;
   if(prev_calculated == 0) limit = rates_total - 1;
   else limit = rates_total - prev_calculated;

   // Main Loop
   // We use iTime, iClose etc which are 0=Newest (Series).
   // Our loop goes from limit (History) down to 0 (Current).
   for(int i = limit; i >= 0; i--)
     {
      // --- 1. Hybrid Data Aggregation ---
      double bar_delta = 0.0;
      double bar_vol   = 0.0;
      bool is_high_res = false; // Track quality
      
      long vol_current = (long)iVolume(NULL, 0, i);

      // Use Lower Timeframe if requested and valid
      bool use_ltf = (InpTFLower != PERIOD_CURRENT && PeriodSeconds(InpTFLower) < PeriodSeconds(Period()));
      
      if(use_ltf)
        {
         MqlRates rates[];
         datetime t_start = iTime(NULL, 0, i);
         datetime t_end   = t_start + PeriodSeconds(Period());
         
         // MQL4 CopyRates available in newer builds
         int count = CopyRates(NULL, InpTFLower, t_start, t_end, rates);
         
         if(count > 0)
           {
            is_high_res = true;
            for(int k=0; k<count; k++)
              {
               if(rates[k].time >= t_end) continue;
               double v = (double)rates[k].tick_volume;
               double d = (rates[k].close >= rates[k].open) ? v : -v;
               bar_delta += d;
               bar_vol += v;
              }
           }
         else
           {
            // Fallback
            double range = iHigh(NULL, 0, i) - iLow(NULL, 0, i);
            double mfm = (range != 0) ? ((iClose(NULL, 0, i)-iLow(NULL, 0, i)) - (iHigh(NULL, 0, i)-iClose(NULL, 0, i))) / range : 0;
            bar_delta = (double)vol_current * mfm;
            bar_vol = (double)vol_current;
           }
        }
      else
        {
         double range = iHigh(NULL, 0, i) - iLow(NULL, 0, i);
         double mfm = (range != 0) ? ((iClose(NULL, 0, i)-iLow(NULL, 0, i)) - (iHigh(NULL, 0, i)-iClose(NULL, 0, i))) / range : 0;
         bar_delta = (double)vol_current * mfm;
         bar_vol = (double)vol_current;
        }

      double metric_raw = (InpSrcVol == SRC_DELTA) ? bar_delta : bar_vol;
      double metric_abs = MathAbs(metric_raw);
      ValBuffer[i] = metric_raw;

      // --- 2. Z-Score Matrix ---
      if(i > rates_total - InpLenStat - 1) { // Startup
         ZVolBuffer[i] = 0.0;
         ZPxBuffer[i] = 0.0;
         continue; 
      }

      double sum_vol = 0;
      double sum_sq_vol = 0;
      double sum_px = 0;
      double sum_sq_px = 0; 
      
      for(int k=0; k<InpLenStat; k++)
        {
         // History: i + 1 + k (looking back from previous bar)
         int idx = i + 1 + k;
         
         double v_hist = MathAbs(ValBuffer[idx]);
         sum_vol += v_hist;
         sum_sq_vol += v_hist * v_hist;
         
         double p_hist = iClose(NULL, 0, idx);
         sum_px += p_hist;
         sum_sq_px += p_hist * p_hist;
        }

      double mean_vol = sum_vol / InpLenStat;
      double var_vol  = (sum_sq_vol / InpLenStat) - (mean_vol * mean_vol);
      double std_vol  = (var_vol > 0) ? MathSqrt(var_vol) : 0;
      double z_vol    = (std_vol != 0) ? (metric_abs - mean_vol) / std_vol : 0.0;

      double mean_px = sum_px / InpLenStat;
      double var_px  = (sum_sq_px / InpLenStat) - (mean_px * mean_px);
      double std_px  = (var_px > 0) ? MathSqrt(var_px) : 0;
      double z_px    = (std_px != 0) ? (iClose(NULL, 0, i) - mean_px) / std_px : 0.0;

      ZVolBuffer[i] = z_vol;
      ZPxBuffer[i]  = z_px;

      // --- 3. Logic Gates ---
      bool is_vol_event = z_vol >= InpZVolThresh;
      bool is_px_event  = MathAbs(z_px) >= InpZPxThresh;
      bool is_bull_div = (iClose(NULL, 0, i) < iOpen(NULL, 0, i)) && (bar_delta > 0);
      bool is_bear_div = (iClose(NULL, 0, i) > iOpen(NULL, 0, i)) && (bar_delta < 0);
      
      bool valid_trigger = is_vol_event && (is_px_event || (InpUseDiv && (is_bull_div || is_bear_div)));

         if(valid_trigger)
        {
         bool is_up = metric_raw > 0;
         int event_type = 0; 
         string tooltip = "";
         color bubble_col = clrNONE;

         if(is_bull_div) {
            bubble_col = col_absorb_bu;
            tooltip = "STOP ABSORPTION BUY (Iceberg)\nBias: Reversal Long";
            event_type = 3;
         } else if(is_bear_div) {
            bubble_col = col_absorb_be;
            tooltip = "STOP ABSORPTION SELL (Iceberg)\nBias: Reversal Short";
            event_type = 4; // Absorb Sell
         } else {
            bubble_col = is_up ? col_bull : col_bear;
            tooltip = is_up ? "MOMENTUM BUY (Aggression)" : "MOMENTUM SELL (Aggression)";
            event_type = is_up ? 1 : 2; 
         }

         EventTypeBuffer[i] = event_type;

         // Alerts (Only on last bar)
         if(InpAlertMode != ALERT_OFF && i == 0) // i=0 is current
           {
            if(InpAlertMode == ALERT_CURRENT && iTime(NULL, 0, 0) != g_last_alert_time) {
                 string clean_tt = tooltip; 
                 Alert("QuantMaster: " + clean_tt + " on " + Symbol());
                 g_last_alert_time = iTime(NULL, 0, 0);
            }
           }
           
         // Visuals
         if((event_type == 1 && InpShowBull) || (event_type == 2 && InpShowBear) || 
            (event_type > 2 && (InpShowBull || InpShowBear))) 
           {
            bool draw_me = false;
            if(event_type == 1 && InpShowBull) draw_me = true;
            if(event_type == 2 && InpShowBear) draw_me = true;
            if(event_type == 3 && InpShowBull) draw_me = true;
            if(event_type == 4 && InpShowBear) draw_me = true;

            if(draw_me)
              {
                datetime t = iTime(NULL, 0, i);
                double h = iHigh(NULL, 0, i);
                
                string name = PREFIX + "Bub_" + IntegerToString(t);
                if(ObjectFind(0, name) < 0) 
                  {
                   ObjectCreate(0, name, OBJ_ARROW, 0, t, h); 
                   double anchor_price = (h + iLow(NULL, 0, i) + iClose(NULL, 0, i)) / 3.0;
                   ObjectSetDouble(0, name, OBJPROP_PRICE, anchor_price);
                   ObjectSetInteger(0, name, OBJPROP_ANCHOR, ANCHOR_CENTER);
                  }

                int f_size = 1; // Arrow width
                if(InpScaleSize) 
                  {
                   if(z_vol > InpZVolThresh + 2.0) f_size = 3; 
                   else if(z_vol > InpZVolThresh + 1.0) f_size = 2; 
                  }

                ObjectSetInteger(0, name, OBJPROP_ARROWCODE, 108); // Circle
                ObjectSetInteger(0, name, OBJPROP_WIDTH, f_size);
                ObjectSetInteger(0, name, OBJPROP_COLOR, bubble_col);
                ObjectSetInteger(0, name, OBJPROP_HIDDEN, false);
                ObjectSetInteger(0, name, OBJPROP_SELECTABLE, true);
                ObjectSetInteger(0, name, OBJPROP_ZORDER, 5);
                ObjectSetInteger(0, name, OBJPROP_BACK, false);
                
                string vol_lbl_key = (InpSrcVol == SRC_DELTA) ? "Vol Delta" : "Total Vol";
                string qual_str = is_high_res ? "(High Res)" : "(Low Res / Est)";
                
                string tt_str = tooltip + "\nVol Z: " + DoubleToString(z_vol, 2) + 
                                "\n" + vol_lbl_key + ": " + FormatVolume(metric_raw) + 
                                "\nData Quality: " + qual_str;
                                
                ObjectSetString(0, name, OBJPROP_TOOLTIP, tt_str); 
                
                // Text Label (Volume)
                string name_txt = PREFIX + "Txt_" + IntegerToString(t);
                if(ObjectFind(0, name_txt) < 0) ObjectCreate(0, name_txt, OBJ_TEXT, 0, t, h);
                ObjectSetDouble(0, name_txt, OBJPROP_PRICE, (h + iLow(NULL, 0, i) + iClose(NULL, 0, i)) / 3.0);
                ObjectSetInteger(0, name_txt, OBJPROP_ANCHOR, ANCHOR_CENTER);
                ObjectSetString(0, name_txt, OBJPROP_TEXT, FormatVolume(metric_raw));
                ObjectSetInteger(0, name_txt, OBJPROP_FONTSIZE, 7);
                ObjectSetInteger(0, name_txt, OBJPROP_COLOR, clrWhite);
                ObjectSetInteger(0, name_txt, OBJPROP_HIDDEN, false);
                ObjectSetInteger(0, name_txt, OBJPROP_SELECTABLE, true);
                ObjectSetInteger(0, name_txt, OBJPROP_ZORDER, 6);
                ObjectSetInteger(0, name_txt, OBJPROP_BACK, false);

                // Ghost Lines
                bool is_sig = (f_size >= 2);
                if(InpShowGhosts && is_sig)
                  {
                   double def_level = is_up ? iLow(NULL, 0, i) : h;
                   string l_name = PREFIX + "Ghost_" + IntegerToString(t);
                   ObjectCreate(0, l_name, OBJ_TREND, 0, t, def_level, t+PeriodSeconds()*50, def_level);
                   ObjectSetInteger(0, l_name, OBJPROP_COLOR, bubble_col);
                   ObjectSetInteger(0, l_name, OBJPROP_STYLE, STYLE_DOT);
                   ObjectSetInteger(0, l_name, OBJPROP_WIDTH, 1);
                   ObjectSetInteger(0, l_name, OBJPROP_RAY_RIGHT, false);
                   ObjectSetInteger(0, l_name, OBJPROP_HIDDEN, false);
                   ObjectSetInteger(0, l_name, OBJPROP_SELECTABLE, true);
                   ObjectSetInteger(0, l_name, OBJPROP_ZORDER, 4);
                  }
              }
           }
        }
        else
        {
           EventTypeBuffer[i] = 0.0;
        }
     }

   if(InpShowDash) UpdateDashboard(rates_total);

   return(rates_total);
}

//+------------------------------------------------------------------+
//| Dashboard Logic                                                  |
//+------------------------------------------------------------------+
void UpdateDashboard(int rates_total)
{
   if(!InpShowDash) return;
   
   // Collect Last 5 Signals
   int found = 0;
   LogEvent logs[5];
   ZeroMemory(logs);
   
   datetime cutoff_time = TimeCurrent() - 86400;

   // Search loop: i from 0 (Newest) to rates_total
   int i=0;
   while(found < 5 && i < rates_total)
   {
      datetime t = iTime(NULL, 0, i);
      if(t < cutoff_time) break; 

      if(EventTypeBuffer[i] > 0.5)
      {
         logs[found].time = t;
         logs[found].type = (int)MathRound(EventTypeBuffer[i]);
         logs[found].z_score = ZVolBuffer[i];
         logs[found].price = iClose(NULL, 0, i);
         found++;
      }
      i++;
   }
   
   // Helper to Draw
   int row_h = (InpDashSize == SIZE_SMALL) ? 15 : (InpDashSize == SIZE_NORMAL) ? 20 : 25;
   int f_size = (InpDashSize == SIZE_SMALL) ? 7 : (InpDashSize == SIZE_NORMAL) ? 9 : 11;
   int col_w_1 = (InpDashSize == SIZE_SMALL) ? 100 : 130; 
   int col_w_2 = (InpDashSize == SIZE_SMALL) ? 180 : 250; 
   int width = col_w_1 + col_w_2;
   
   int start_x = 0, start_y = 0;
   long chart_w = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   long chart_h = ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   int pad = 10;
   
   int total_rows = 15;
   int height = total_rows * row_h;
   
   if(InpDashPos == POS_TOP_RIGHT) { start_x = (int)chart_w - width - pad; start_y = pad; }
   if(InpDashPos == POS_BOTTOM_RIGHT) { start_x = (int)chart_w - width - pad; start_y = (int)chart_h - height - pad; }
   if(InpDashPos == POS_BOTTOM_LEFT) { start_x = pad; start_y = (int)chart_h - height - pad; }
   if(InpDashPos == POS_TOP_LEFT) { start_x = pad; start_y = pad; }

   if(start_x < 0) start_x = 0;
   if(start_y < 0) start_y = 0;
   
   color bg_main   = C'30,30,30';
   color bg_header = C'0,229,255';
   
   // HEADER
   DrawDashCell(PREFIX + "H", start_x, start_y, width, row_h, bg_header, "QUANT LOGIC MATRIX", clrWhite, f_size+1);
   
   int y = start_y + row_h;
   
   // LEGEND
   DrawDashCell(PREFIX + "L1_C", start_x, y, col_w_1, row_h, col_bull, "O M. BUY", clrWhite, f_size, 0);
   DrawDashCell(PREFIX + "L1_T", start_x + col_w_1, y, col_w_2, row_h, bg_main, "Aggressive UP", clrWhite, f_size, 0);
   y += row_h;
   
   DrawDashCell(PREFIX + "L2_C", start_x, y, col_w_1, row_h, col_bear, "O M. SELL", clrWhite, f_size, 0);
   DrawDashCell(PREFIX + "L2_T", start_x + col_w_1, y, col_w_2, row_h, bg_main, "Aggressive DOWN", clrWhite, f_size, 0);
   y += row_h;

   DrawDashCell(PREFIX + "L3_C", start_x, y, col_w_1, row_h, col_absorb_bu, "O ABS BUY", clrWhite, f_size, 0);
   DrawDashCell(PREFIX + "L3_T", start_x + col_w_1, y, col_w_2, row_h, bg_main, "Absorption (Rev Long)", clrWhite, f_size, 0);
   y += row_h;

   DrawDashCell(PREFIX + "L4_C", start_x, y, col_w_1, row_h, col_absorb_be, "O ABS SELL", clrWhite, f_size, 0);
   DrawDashCell(PREFIX + "L4_T", start_x + col_w_1, y, col_w_2, row_h, bg_main, "Absorption (Rev Short)", clrWhite, f_size, 0);
   y += row_h;
   
   // Divider
   DrawDashCell(PREFIX + "SH", start_x, y, width, row_h, C'128,128,128', "LOGS (Last 24H)", clrWhite, f_size);
   
   // LOGS
   for(int k=0; k<5; k++)
   {
      y += row_h;
      if(k < found)
      {
         string t_str = TimeToString(logs[k].time, TIME_DATE|TIME_MINUTES);
         string type_str = "";
         color type_col = clrGray;
         int type_e = logs[k].type;
         
         if(type_e == 1) { type_str = "MOM BUY"; type_col = col_bull; }
         else if(type_e == 2) { type_str = "MOM SELL"; type_col = col_bear; }
         else if(type_e == 3) { type_str = "ABS BUY"; type_col = col_absorb_bu; }
         else if(type_e == 4) { type_str = "ABS SELL"; type_col = col_absorb_be; }
         
         string val_str = type_str + " (" + DoubleToString(logs[k].z_score, 1) + "sig)";
         
         DrawDashCell(PREFIX + "LogT_" + IntegerToString(k), start_x, y, col_w_1, row_h, C'40,40,40', t_str, clrWhite, f_size-1);
         DrawDashCell(PREFIX + "LogV_" + IntegerToString(k), start_x + col_w_1, y, col_w_2, row_h, type_col, val_str, clrWhite, f_size-1, 0);
      }
      else
      {
         DrawDashCell(PREFIX + "LogT_" + IntegerToString(k), start_x, y, col_w_1, row_h, bg_main, "", clrWhite, f_size);
         DrawDashCell(PREFIX + "LogV_" + IntegerToString(k), start_x + col_w_1, y, col_w_2, row_h, bg_main, "", clrGray, f_size, 0);
      }
   }
   
   ChartRedraw();
}

void DrawDashCell(string name, int x, int y, int w, int h, color bg_col, string text, color txt_col, int f_size=8, int align=1)
{
   if(ObjectFind(0, name) < 0) ObjectCreate(0, name, OBJ_RECTANGLE_LABEL, 0, 0, 0);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
   ObjectSetInteger(0, name, OBJPROP_XSIZE, w);
   ObjectSetInteger(0, name, OBJPROP_YSIZE, h);
   ObjectSetInteger(0, name, OBJPROP_BGCOLOR, bg_col);
   ObjectSetInteger(0, name, OBJPROP_BORDER_TYPE, BORDER_FLAT);
   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_ZORDER, 10);
   
   string lbl_name = name + "_L";
   if(ObjectFind(0, lbl_name) < 0) ObjectCreate(0, lbl_name, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, lbl_name, OBJPROP_XDISTANCE, x + 5);
   ObjectSetInteger(0, lbl_name, OBJPROP_YDISTANCE, y + h/2 - f_size/2 - 2); 
   ObjectSetInteger(0, lbl_name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetString(0, lbl_name, OBJPROP_TEXT, text);
   ObjectSetString(0, lbl_name, OBJPROP_FONT, "Arial");
   ObjectSetInteger(0, lbl_name, OBJPROP_FONTSIZE, f_size);
   ObjectSetInteger(0, lbl_name, OBJPROP_COLOR, txt_col);
   ObjectSetInteger(0, lbl_name, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER);
   ObjectSetInteger(0, lbl_name, OBJPROP_HIDDEN, true);
   ObjectSetInteger(0, lbl_name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, lbl_name, OBJPROP_ZORDER, 11);
}
