//+------------------------------------------------------------------+
//|                                           DeltaVolumeBubble.mq5  |
//|                                  Copyright 2025, Google Deepmind |
//|                                       Translated from Pine Script|
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Google Deepmind"
#property link      "https://www.google.com"
#property version   "1.01"
#property indicator_chart_window
#property indicator_buffers 4
#property indicator_plots   0

//--- Constants
#define PREFIX "DVB_"

//--- Enums
enum ENUM_CALC_SOURCE {
   SRC_DELTA,  // Volume Delta
   SRC_VOLUME  // Regular Volume
};

enum ENUM_CALC_MODE {
   MODE_ADAPTIVE, // Adaptive (Z-Score)
   MODE_FIXED     // Fixed (Absolute Value)
};

enum ENUM_THEME {
   THEME_DARK,  // Dark Theme (Mint-Coral)
   THEME_LIGHT  // Light Theme (Royal-Sunset)
};

enum ENUM_FONT_TYPE {
   FONT_DEFAULT,
   FONT_MONOSPACE
};

enum ENUM_ALERT_MODE {
   ALERT_OFF,     // Off
   ALERT_CURRENT, // Current Bar (Realtime)
   ALERT_CLOSED   // Closed Bar (Confirmed)
};

//--- Inputs
input group "Data Settings"
input ENUM_CALC_SOURCE InpSource = SRC_DELTA; // Calculation Source
input ENUM_TIMEFRAMES InpAnchorTF = PERIOD_CURRENT; // Anchor TF (Maintained for Input Parity, Ignored mathematically)
input ENUM_TIMEFRAMES InpLowerTF = PERIOD_CURRENT; // High-Precision Data (Current = Standard)
input int InpLookback = 60; // Statistical Lookback (20 min)
input int InpMaxHistory = 5000; // Max Bars to Draw Bubbles

input group "Quant Logic"
input ENUM_CALC_MODE InpCalcMode = MODE_ADAPTIVE; // Calculation Mode
input double InpZThreshold = 2.0; // Z-Score Threshold (sigma)
input double InpMinVolume = 200.0; // Fixed Mode: Min Volume

input group "Absorption Logic"
input bool InpDetectAbsorption = false; // Detect Absorption
input double InpAbsorptionRatio = 0.6; // Absorption Ratio (0.1 - 1.0)

input group "Visuals"
input bool InpShowBullish = true; // Show Bullish
input bool InpShowBearish = true; // Show Bearish
input bool InpScaleSize = true; // Scale Size by Z-Score
input bool InpAdaptiveColor = true; // Adaptive Color Intensity
input int InpGhostOpacity = 85; // Fixed Opacity (1-100)
input bool InpGlowEffect = true; // Glow Effect
input ENUM_FONT_TYPE InpFontType = FONT_DEFAULT; // Font
input ENUM_THEME InpTheme = THEME_DARK; // Theme

input group "Alerts"
input ENUM_ALERT_MODE InpAlertTiming = ALERT_CURRENT; // Alert Timing
input bool InpAlertThreshold = false; // Alert: Unusual Threshold Trigger
input bool InpAlertAbsorption = false; // Alert: Absorption Detected

//--- Buffers
double RawBuffer[];
double ZScoreBuffer[];
double AvgBodyBuffer[];
double MagicCarpetBuffer[];

//--- Global Variables
// Color Palette
struct UnicornVomit {
   color happy_juice;
   color angry_juice;
   color happy_glow;
   color angry_glow;
   color sponge_glow;
   color ink_color;
   uchar default_glow_alpha;
   uchar default_sponge_alpha;
};

UnicornVomit current_outfit;

//+------------------------------------------------------------------+
//| Custom Indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- indicator buffers mapping
   SetIndexBuffer(0, RawBuffer, INDICATOR_CALCULATIONS);
   SetIndexBuffer(1, ZScoreBuffer, INDICATOR_CALCULATIONS);
   SetIndexBuffer(2, AvgBodyBuffer, INDICATOR_CALCULATIONS);
   SetIndexBuffer(3, MagicCarpetBuffer, INDICATOR_CALCULATIONS);

   ArraySetAsSeries(RawBuffer, false);
   ArraySetAsSeries(ZScoreBuffer, false);
   ArraySetAsSeries(AvgBodyBuffer, false);
   ArraySetAsSeries(MagicCarpetBuffer, false);

   SetupTheme();
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Custom Indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ObjectsDeleteAll(0, PREFIX);
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Helper: Setup Theme Colors                                       |
//+------------------------------------------------------------------+
void SetupTheme()
{
   if(InpTheme == THEME_DARK)
   {
      // Mint-Coral
      current_outfit.happy_juice = (color)C'0,150,100'; 
      current_outfit.angry_juice = (color)C'182,0,61'; 
      current_outfit.happy_glow  = (color)C'0,209,157'; 
      current_outfit.angry_glow  = (color)C'255,54,121'; 
      current_outfit.sponge_glow = (color)C'157,0,255'; 
      current_outfit.ink_color   = clrWhite; 
      
      current_outfit.default_glow_alpha   = (uchar)(255 * 0.15); // Pine 85 Transp = 15% Opacity
      current_outfit.default_sponge_alpha = (uchar)(255 * 0.70); // Pine 30 Transp = 70% Opacity
   }
   else
   {
      // Light Theme (Royal-Sunset)
      current_outfit.happy_juice = (color)C'0,102,255';
      current_outfit.angry_juice = (color)C'255,128,0';
      current_outfit.happy_glow  = (color)C'102,178,255';
      current_outfit.angry_glow  = (color)C'255,179,102';
      current_outfit.sponge_glow = (color)C'106,0,255'; 
      current_outfit.ink_color   = clrBlack;
      
      current_outfit.default_glow_alpha   = (uchar)(255 * 0.15); // Pine 85 Transp = 15% Opacity
      current_outfit.default_sponge_alpha = (uchar)(255 * 0.60); // Pine 40 Transp = 60% Opacity
   }
}

//+------------------------------------------------------------------+
//| Helper: Format Volume                                            |
//+------------------------------------------------------------------+
string FormatVolume(double vol)
{
   double abs_vol = MathAbs(vol);
   if(abs_vol >= 1e9) return StringFormat("%.1fB", abs_vol / 1e9);
   if(abs_vol >= 1e6) return StringFormat("%.1fM", abs_vol / 1e6);
   if(abs_vol >= 1e3) return StringFormat("%.1fK", abs_vol / 1e3);
   return StringFormat("%.0f", abs_vol);
}

//+------------------------------------------------------------------+
//| Helper: Create/Update Bubble Object                              |
//+------------------------------------------------------------------+
void CreateBubble(int index, datetime time, double price, double z_score, double raw_val, double total_vol, bool is_bullish, bool is_absorp)
{
   string name_base = PREFIX + IntegerToString(time);
   string name_glow = name_base + "_Glow";
   string name_bub  = name_base + "_Bub";
   string name_txt  = name_base + "_Txt";

   //--- Determine Size
   string pants_size = "S";
   if(InpScaleSize)
   {
      double check_z = InpCalcMode == MODE_ADAPTIVE ? z_score : 0.0;
      double check_v = InpCalcMode == MODE_FIXED ? MathAbs(raw_val) : 0.0;
      
      if(InpCalcMode == MODE_ADAPTIVE)
         pants_size = (check_z > InpZThreshold + 2.0) ? "L" : (check_z > InpZThreshold + 1.0) ? "M" : "S";
      else
         pants_size = (check_v > InpMinVolume * 6) ? "L" : (check_v > InpMinVolume * 4) ? "M" : "S";
   }

   int glow_size = (pants_size == "L") ? 6 : (pants_size == "M") ? 5 : 4; 
   int bub_size  = (pants_size == "L") ? 4 : (pants_size == "M") ? 3 : 2; 
   int txt_size  = (pants_size == "L") ? 10 : (pants_size == "M") ? 8 : 7;
   
   //--- Determine Colors
   int alpha_val = (int)((InpGhostOpacity / 100.0) * 255);
   if(InpAdaptiveColor)
   {
      double pine_transp = MathMax(0.0, 100.0 - (z_score * 20.0));
      alpha_val = (int)(( (100.0 - pine_transp) / 100.0 ) * 255.0);
   }

   color base_col = is_bullish ? current_outfit.happy_juice : current_outfit.angry_juice;
   color radioactive_sludge = is_absorp ? current_outfit.sponge_glow : (is_bullish ? current_outfit.happy_glow : current_outfit.angry_glow);
   
   int glow_alpha = InpAdaptiveColor ? 
                    (is_absorp ? current_outfit.default_sponge_alpha : current_outfit.default_glow_alpha) : 
                    (int)((InpGhostOpacity / 100.0) * 255.0);

   color final_bub_col = (color)ColorToARGB(base_col, (uchar)alpha_val); 
   color final_glow_col = (color)ColorToARGB(radioactive_sludge, (uchar)glow_alpha); 

   //--- Draw Glow
   if(InpGlowEffect)
   {
      if(ObjectFind(0, name_glow) < 0) ObjectCreate(0, name_glow, OBJ_TEXT, 0, time, price);
      ObjectSetString(0, name_glow, OBJPROP_TEXT, "l"); 
      ObjectSetString(0, name_glow, OBJPROP_FONT, "Wingdings");
      ObjectSetInteger(0, name_glow, OBJPROP_FONTSIZE, glow_size * 10); 
      ObjectSetInteger(0, name_glow, OBJPROP_COLOR, final_glow_col);
      ObjectSetInteger(0, name_glow, OBJPROP_ANCHOR, ANCHOR_CENTER);
      ObjectSetInteger(0, name_glow, OBJPROP_ZORDER, 0); 
   }

   //--- Draw Bubble Core
   if(ObjectFind(0, name_bub) < 0) ObjectCreate(0, name_bub, OBJ_TEXT, 0, time, price);
   ObjectSetString(0, name_bub, OBJPROP_TEXT, "l"); 
   ObjectSetString(0, name_bub, OBJPROP_FONT, "Wingdings");
   ObjectSetInteger(0, name_bub, OBJPROP_FONTSIZE, bub_size * 10);
   ObjectSetInteger(0, name_bub, OBJPROP_COLOR, final_bub_col);
   ObjectSetInteger(0, name_bub, OBJPROP_ANCHOR, ANCHOR_CENTER);
   ObjectSetInteger(0, name_bub, OBJPROP_ZORDER, 1);

   //--- Draw Text
   string txt_val = (InpSource == SRC_DELTA && is_bullish ? "+" : (InpSource==SRC_DELTA ? "-" : "")) + FormatVolume(raw_val);
   if(ObjectFind(0, name_txt) < 0) ObjectCreate(0, name_txt, OBJ_TEXT, 0, time, price);
   ObjectSetString(0, name_txt, OBJPROP_TEXT, txt_val);
   ObjectSetString(0, name_txt, OBJPROP_FONT, (InpFontType == FONT_MONOSPACE) ? "Consolas" : "Arial");
   ObjectSetInteger(0, name_txt, OBJPROP_FONTSIZE, txt_size);
   ObjectSetInteger(0, name_txt, OBJPROP_COLOR, current_outfit.ink_color);
   ObjectSetInteger(0, name_txt, OBJPROP_ANCHOR, ANCHOR_CENTER);
   ObjectSetInteger(0, name_txt, OBJPROP_ZORDER, 2); 
   
   //--- Tooltip (secret_scroll)
   string scary_text = is_absorp ? "\n⚠️ ABSORPTION DETECTED ⚠️\n(High Vol / Low Move)" : "";
   string label_thingy = (InpSource == SRC_DELTA) ? "Delta Vol: " : "Total Vol: ";
   string way_to_go = is_bullish ? "Bullish " : "Bearish ";
   string sign_language = (InpSource == SRC_DELTA) ? (is_bullish ? "+" : "-") : "";
   
   double plain_water = total_vol; 
   double stuff_for_math = (InpSource == SRC_DELTA) ? plain_water : MathAbs(raw_val);
   double mystery_ratio = (stuff_for_math != 0) ? MathAbs(raw_val) / stuff_for_math : 0.0;
   
   string dom_text = (InpSource == SRC_DELTA) ? StringFormat("\nDom: %.2f%%", mystery_ratio * 100.0) : "";
   
   string tooltip = way_to_go + ((InpSource == SRC_DELTA) ? "Delta" : "Vol") + scary_text + "\n" +
                    label_thingy + sign_language + FormatVolume(raw_val) + "\n" +
                    StringFormat("Z-Score: %.2fσ", z_score) + dom_text;
                    
   ObjectSetString(0, name_bub, OBJPROP_TOOLTIP, tooltip);
}

//+------------------------------------------------------------------+
//| 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 < InpLookback + 1) return 0;

   int calc_start = prev_calculated - 1;
   if(calc_start < 0) calc_start = 0;
   
   int draw_start = calc_start;
   if(draw_start < rates_total - InpMaxHistory) draw_start = rates_total - InpMaxHistory;

   int raw_start = draw_start - InpLookback;
   if(raw_start < 0) raw_start = 0;

   //--- PASS 1: Calculate core data (raw_fish, average body, magic carpet) natively on chart TF
   for(int i = raw_start; i < rates_total; i++)
   {
       double raw_fish = 0.0;
       double magic_carpet = 0.0;
       double c_open = open[i];
       double c_close = close[i];
       long c_vol = tick_volume[i]; 
       
       bool use_lower = (InpLowerTF != PERIOD_CURRENT && PeriodSeconds(InpLowerTF) < PeriodSeconds());
       
       if(use_lower)
       {
           datetime t_start = time[i];
           datetime t_end = t_start + PeriodSeconds(); 
           
           MqlRates rates[];
           int count = CopyRates(_Symbol, InpLowerTF, t_start, t_end, rates);
           
           if(count > 0)
           {
               double sum_price_vol = 0;
               double sum_vol_lower = 0;
               double sum_delta = 0;
               
               for(int k=0; k<count; k++)
               {
                   if(rates[k].time >= t_end) continue;
                   
                   double r_vol = (double)rates[k].tick_volume;
                   double r_delta = (rates[k].close >= rates[k].open) ? r_vol : -r_vol;
                   
                   sum_delta += r_delta;
                   sum_price_vol += rates[k].close * r_vol; 
                   sum_vol_lower += r_vol;
               }
               
               if(InpSource == SRC_DELTA) raw_fish = sum_delta;
               else raw_fish = (c_close >= c_open) ? sum_vol_lower : -sum_vol_lower;
               
               if(sum_vol_lower > 0) magic_carpet = sum_price_vol / sum_vol_lower;
               else magic_carpet = (open[i] + high[i] + low[i] + close[i]) / 4.0;
           }
           else
           {
              raw_fish = (InpSource == SRC_DELTA) ? ((c_close >= c_open) ? (double)c_vol : -(double)c_vol) : ((c_close >= c_open) ? (double)c_vol : -(double)c_vol);
              magic_carpet = (open[i] + high[i] + low[i] + close[i]) / 4.0;
           }
       }
       else
       {
           raw_fish = (InpSource == SRC_DELTA) ? ((c_close >= c_open) ? (double)c_vol : -(double)c_vol) : ((c_close >= c_open) ? (double)c_vol : -(double)c_vol);
           magic_carpet = (open[i] + high[i] + low[i] + close[i]) / 4.0;
       }
       
       RawBuffer[i] = raw_fish;
       AvgBodyBuffer[i] = MathAbs(c_close - c_open);
       MagicCarpetBuffer[i] = magic_carpet;
   }

   //--- PASS 2: Statistical evaluation and Visuals
   for(int i = draw_start; i < rates_total; i++)
   {
       double raw_fish = RawBuffer[i];
       double abs_vodka = MathAbs(raw_fish);
       double magic_carpet = MagicCarpetBuffer[i];

       // Mean
       double sum_vol = 0;
       for(int k=0; k<InpLookback; k++) 
       {
           int idx = i - k;
           if(idx < 0) continue;
           sum_vol += MathAbs(RawBuffer[idx]);
       }
       double boring_average = sum_vol / InpLookback;

       // StDev
       double sum_sq = 0;
       for(int k=0; k<InpLookback; k++)
       {
           int idx = i - k;
           if(idx < 0) continue;
           sum_sq += MathPow(MathAbs(RawBuffer[idx]) - boring_average, 2);
       }
       double deviant_behavior = MathSqrt(sum_sq / InpLookback);
       
       double z_score = (deviant_behavior != 0) ? (abs_vodka - boring_average) / deviant_behavior : 0.0;
       ZScoreBuffer[i] = z_score;

       // Absorption Logic
       double sum_body = 0;
       for(int k=0; k<InpLookback; k++) 
       {
           int idx = i - k;
           if(idx < 0) continue;
           sum_body += AvgBodyBuffer[idx];
       }
       double avg_body = sum_body / InpLookback;
       
       double body = MathAbs(close[i] - open[i]);
       
       bool kaboom = false;
       if(InpCalcMode == MODE_ADAPTIVE) kaboom = (z_score >= InpZThreshold);
       else kaboom = (abs_vodka >= InpMinVolume);
       
       bool is_spongy = InpDetectAbsorption && kaboom && (body < avg_body * InpAbsorptionRatio);

       // Visuals
       bool show_it = (raw_fish > 0 && InpShowBullish) || (raw_fish <= 0 && InpShowBearish);
       
       if(kaboom && show_it)
       {
           bool is_bull = (raw_fish > 0);
           CreateBubble(i, time[i], magic_carpet, z_score, raw_fish, (double)tick_volume[i], is_bull, is_spongy);

           // Alerts
           if(InpAlertTiming == ALERT_CURRENT && i == rates_total - 1) 
           {
               static datetime last_alert_time = 0;
               if(time[i] != last_alert_time) 
               {
                   string mode_str = (InpSource == SRC_DELTA) ? "Delta" : "Volume";
                   string msg = "";
                   if(is_spongy && InpAlertAbsorption) 
                       msg = StringFormat("⚠️ ABSORPTION DETECTED on %s\nMode: %s\nValue: %s\nPrice: %.*f", _Symbol, mode_str, FormatVolume(raw_fish), _Digits, close[i]);
                   else if(kaboom && InpAlertThreshold && !is_spongy) 
                       msg = StringFormat("🚨 UNUSUAL %s on %s\nValue: %s\nZ-Score: %.2fσ", mode_str, _Symbol, FormatVolume(raw_fish), z_score);
                   
                   if(msg != "")
                   {
                       Alert(msg);
                       PlaySound("alert.wav");
                       last_alert_time = time[i];
                   }
               }
           }
       }
   }
   
   // Closed Bar Logic
   if(InpAlertTiming == ALERT_CLOSED)
   {
      static datetime last_bar_time = 0;
      if(rates_total > 1 && time[rates_total-1] != last_bar_time)
      {
         if(last_bar_time != 0) 
         {
            int prev_idx = rates_total - 2;
            if(prev_idx >= 0)
            {
                double z_val = ZScoreBuffer[prev_idx];
                double raw_val = RawBuffer[prev_idx];
                double avg_body = AvgBodyBuffer[prev_idx]; 
                
                double open_p = open[prev_idx];
                double close_p = close[prev_idx];
                double body = MathAbs(close_p - open_p);
                
                bool p_kaboom = false;
                double abs_raw = MathAbs(raw_val);
                if(InpCalcMode == MODE_ADAPTIVE) p_kaboom = (z_val >= InpZThreshold);
                else p_kaboom = (abs_raw >= InpMinVolume);
                
                bool p_spongy = InpDetectAbsorption && p_kaboom && (body < avg_body * InpAbsorptionRatio);
                
                string mode_str = (InpSource == SRC_DELTA) ? "Delta" : "Volume";
                string msg = "";
                if(p_spongy && InpAlertAbsorption) 
                    msg = StringFormat("⚠️ ABSORPTION DETECTED (Closed) on %s\nMode: %s\nValue: %s\nPrice: %.*f", _Symbol, mode_str, FormatVolume(raw_val), _Digits, close_p);
                else if(p_kaboom && InpAlertThreshold && !p_spongy)
                    msg = StringFormat("🚨 UNUSUAL %s (Closed) on %s\nValue: %s\nZ-Score: %.2fσ", mode_str, _Symbol, FormatVolume(raw_val), z_val);
                
                if(msg != "")
                {
                   Alert(msg);
                   PlaySound("alert.wav");
                }
            }
         }
         last_bar_time = time[rates_total-1];
      }
   }

   return(rates_total);
  }
