//+------------------------------------------------------------------+
//|                            Zerolag_MACD_(arrows_alerts_MTF).mq5  |
//|                                         Copyright © 2012, mladen |
//|       https://www.mql5.com/en/forum/172902/page5#comment_4202362 |
//+------------------------------------------------------------------+
#property copyright   "Copyright © 2012, mladen"
#property link        "https://www.forex-station.com/"
#property version     "1.00"
#property description "Zero lag MACD with Alerts, Chart Arrows & On/Off Button"
#property indicator_separate_window

#property indicator_buffers 5
#property indicator_plots   4

//--- Plot 1: MACD Histogram
#property indicator_label1  "Zero lag MACD (Hist)"
#property indicator_type1   DRAW_HISTOGRAM
#property indicator_color1  clrDeepSkyBlue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1

//--- Plot 2: OsMA Histogram
#property indicator_label2  "Zero lag OsMA"
#property indicator_type2   DRAW_HISTOGRAM
#property indicator_color2  clrDimGray
#property indicator_style2  STYLE_SOLID
#property indicator_width2  2

//--- Plot 3: MACD Line
#property indicator_label3  "Zero lag MACD"
#property indicator_type3   DRAW_LINE
#property indicator_color3  clrDeepSkyBlue
#property indicator_style3  STYLE_SOLID
#property indicator_width3  2

//--- Plot 4: Signal Line
#property indicator_label4  "Signal line"
#property indicator_type4   DRAW_LINE
#property indicator_color4  clrRed
#property indicator_style4  STYLE_SOLID
#property indicator_width4  1

//--- Alert types enum
enum ENUM_ALERT_TYPE
{
   ALERT_MACD_CROSS_SIGNAL = 0, // MACD crossed signal line
   ALERT_MACD_CROSS_ZERO   = 1, // MACD crossed zero line
   ALERT_SIGNAL_CROSS_ZERO = 2  // Signal line crossed zero line
};

//--- Input parameters
input group "--- MACD Settings ---"
input ENUM_TIMEFRAMES      timeFrame           = PERIOD_CURRENT;          // Time frame
input int                  FastEMA             = 12;                      // Fast EMA period
input int                  SlowEMA             = 26;                      // Slow EMA period
input int                  SignalEMA           = 9;                       // Signal EMA period
input ENUM_ALERT_TYPE      AlertsArrowsOn      = ALERT_MACD_CROSS_SIGNAL; // Alerts and arrows condition
input ENUM_APPLIED_PRICE   MacdPrice           = PRICE_CLOSE;             // Applied price
input bool                 Interpolate         = true;                    // MTF Interpolate

input group "--- Chart Arrows Settings ---"
input bool                 arrowsVisible       = false;                   // Show chart arrows
input string               arrowsIdentifier    = "ZLMacdArrows";          // Arrows identifier
input int                  UpArrowSymbolCode   = 241;                     // Up arrow Wingdings code
input int                  DownArrowSymbolCode = 242;                     // Down arrow Wingdings code
input color                arrowsUpColor       = clrDeepSkyBlue;          // Up arrow color
input color                arrowsDnColor       = clrRed;                  // Down arrow color
input int                  UpArrowSize         = 1;                       // Up arrow size
input int                  DownArrowSize       = 1;                       // Down arrow size

input group "--- Alerts Settings ---"
input bool                 alertsOn            = false;                   // Enable alerts
input bool                 alertsOnCurrent     = false;                   // Alerts on current (open) bar
input bool                 alertsMessage       = false;                   // Popup message alert
input bool                 alertsSound         = false;                   // Sound alert
input string               soundFile           = "alert2.wav";            // Sound file
input bool                 alertsEmail         = false;                   // Email alert
input bool                 alertsPush          = false;                   // Push notification alert

//Forex-Station copy & paste code; Button code start 11
input group "=== Toggle Button Settings ==="
input string             button_note1          = "------------------------------"; // ------------------------------
input int                btn_Subwindow         = 0;                  // What window to put the button on (0=Main Chart, -1=Indicator Subwindow)
input ENUM_BASE_CORNER   btn_corner            = CORNER_LEFT_UPPER;  // chart btn_corner for anchoring
input string             btn_text              = "zlMACD: ON";       // Display label
input string             btn_Font              = "Arial";            // button font name
input int                btn_FontSize          = 9;                  // btn__font size
input color              btn_text_ON_color     = clrLime;            // ON color when the button is turned on
input color              btn_text_OFF_color    = clrRed;             // OFF color when the button is turned off
input color              btn_background_color  = clrDimGray;         // background color of the button
input color              btn_border_color      = clrBlack;           // border color the button
input int                button_x              = 180;                // Horizontal location
input int                button_y              = 100;                // Vertical location
input int                btn_Width             = 80;                 // btn__width
input int                btn_Height            = 20;                 // btn__height
input string             UniqueButtonID        = "ZLMacdBT";         // Unique Button ID                      
input string             button_note2          = "------------------------------"; // ------------------------------
//Forex-Station copy & paste code; Button code end 11

//+------------------------------------------------------------------+
//| Button State Tracking Variables                                  |
//+------------------------------------------------------------------+
bool    IndicatorEnabled = true;
bool    IndicatorHidden  = false; // True when indicator is turned OFF via toggle button
string  ToggleButtonName;
string  g_gvHidden;
string  g_prefix;
string  IndicatorName, IndicatorObjPrefix, buttonId;

//--- Indicator buffers
double machBuffer[];
double osmaBuffer[];
double macdBuffer[];
double signBuffer[];
double trendBuffer[];

//--- Working structure for ZeroLag calculations
struct WorkData
{
   double ema11;
   double ema12;
   double ema21;
   double ema22;
   double ema31;
   double ema32;
};

WorkData workBuffer[];

//--- Global variables
string crossDescription = "";
int    atrHandle        = INVALID_HANDLE;

//+------------------------------------------------------------------+
//| Helper: Get price from OHLC arrays                               |
//+------------------------------------------------------------------+
double GetPrice(ENUM_APPLIED_PRICE price_type, const double &open[], const double &high[], const double &low[], const double &close[], int i)
{
   switch(price_type)
   {
      case PRICE_OPEN:     return open[i];
      case PRICE_HIGH:     return high[i];
      case PRICE_LOW:      return low[i];
      case PRICE_MEDIAN:   return (high[i] + low[i]) * 0.5;
      case PRICE_TYPICAL:  return (high[i] + low[i] + close[i]) / 3.0;
      case PRICE_WEIGHTED: return (high[i] + low[i] + 2.0 * close[i]) * 0.25;
      case PRICE_CLOSE:
      default:             return close[i];
   }
}

//+------------------------------------------------------------------+
//| Helper: Get price from MqlRates struct                           |
//+------------------------------------------------------------------+
double GetPriceFromRate(ENUM_APPLIED_PRICE price_type, const MqlRates &rate)
{
   switch(price_type)
   {
      case PRICE_OPEN:     return rate.open;
      case PRICE_HIGH:     return rate.high;
      case PRICE_LOW:      return rate.low;
      case PRICE_MEDIAN:   return (rate.high + rate.low) * 0.5;
      case PRICE_TYPICAL:  return (rate.high + rate.low + rate.close) / 3.0;
      case PRICE_WEIGHTED: return (rate.high + rate.low + 2.0 * rate.close) * 0.25;
      case PRICE_CLOSE:
      default:             return rate.close;
   }
}

//+------------------------------------------------------------------+
//| Helper: Timeframe string representation                          |
//+------------------------------------------------------------------+
string TimeframeToString(ENUM_TIMEFRAMES tf)
{
   switch(tf)
   {
      case PERIOD_CURRENT: return "Current";
      case PERIOD_M1:      return "M1";
      case PERIOD_M2:      return "M2";
      case PERIOD_M3:      return "M3";
      case PERIOD_M4:      return "M4";
      case PERIOD_M5:      return "M5";
      case PERIOD_M6:      return "M6";
      case PERIOD_M10:     return "M10";
      case PERIOD_M12:     return "M12";
      case PERIOD_M15:     return "M15";
      case PERIOD_M20:     return "M20";
      case PERIOD_M30:     return "M30";
      case PERIOD_H1:      return "H1";
      case PERIOD_H2:      return "H2";
      case PERIOD_H3:      return "H3";
      case PERIOD_H4:      return "H4";
      case PERIOD_H6:      return "H6";
      case PERIOD_H8:      return "H8";
      case PERIOD_H12:     return "H12";
      case PERIOD_D1:      return "D1";
      case PERIOD_W1:      return "W1";
      case PERIOD_MN1:     return "MN1";
      default:             return EnumToString(tf);
   }
}

//+------------------------------------------------------------------+
//| Helper to resolve button text based on state                     |
//+------------------------------------------------------------------+
string GetButtonText(bool isEnabled)
{
   string t = btn_text;
   if(isEnabled)
   {
      if(StringFind(t, "OFF") >= 0) StringReplace(t, "OFF", "ON");
      else if(StringFind(t, "Off") >= 0) StringReplace(t, "Off", "On");
      else if(StringFind(t, "off") >= 0) StringReplace(t, "off", "on");
   }
   else
   {
      if(StringFind(t, "ON") >= 0) StringReplace(t, "ON", "OFF");
      else if(StringFind(t, "On") >= 0) StringReplace(t, "On", "Off");
      else if(StringFind(t, "on") >= 0) StringReplace(t, "on", "off");
   }
   return t;
}

//+------------------------------------------------------------------+
//| Helper: Reliably find indicator subwindow number                 |
//+------------------------------------------------------------------+
int GetIndicatorSubwindow()
{
   int sub = ChartWindowFind();
   if(sub >= 0)
      return sub;
   if(IndicatorName != "")
   {
      sub = ChartWindowFind(0, IndicatorName);
      if(sub >= 0)
         return sub;
   }
   return 0;
}

//+------------------------------------------------------------------+
//| Create Toggle Button                                             |
//+------------------------------------------------------------------+
void CreateToggleButton()
{
   int btnWin = (btn_Subwindow < 0) ? GetIndicatorSubwindow() : btn_Subwindow;
   if(btnWin < 0) btnWin = 0;

   if(ObjectFind(0, buttonId) < 0)
   {
      int y_pos = button_y;
      int obj_total = ObjectsTotal(0, -1, OBJ_BUTTON);
      bool collision = true;
      while(collision)
      {
         collision = false;
         for(int i = 0; i < obj_total; i++)
         {
            string name = ObjectName(0, i, -1, OBJ_BUTTON);
            if(name == buttonId) continue;
            if((ENUM_BASE_CORNER)ObjectGetInteger(0, name, OBJPROP_CORNER) != btn_corner) continue;
            if(ObjectGetInteger(0, name, OBJPROP_XDISTANCE) == button_x &&
               ObjectGetInteger(0, name, OBJPROP_YDISTANCE) == y_pos)
            {
               y_pos += btn_Height + 4;
               collision = true;
               break;
            }
         }
      }

      ObjectCreate(0, buttonId, OBJ_BUTTON, btnWin, 0, 0);

      ObjectSetInteger(0, buttonId, OBJPROP_CORNER,       btn_corner);
      ObjectSetInteger(0, buttonId, OBJPROP_XDISTANCE,    button_x);
      ObjectSetInteger(0, buttonId, OBJPROP_YDISTANCE,    y_pos);
      ObjectSetInteger(0, buttonId, OBJPROP_XSIZE,        btn_Width);
      ObjectSetInteger(0, buttonId, OBJPROP_YSIZE,        btn_Height);
      ObjectSetString (0, buttonId, OBJPROP_FONT,         btn_Font);
      ObjectSetInteger(0, buttonId, OBJPROP_FONTSIZE,     btn_FontSize);
      ObjectSetInteger(0, buttonId, OBJPROP_BGCOLOR,      btn_background_color);
      ObjectSetInteger(0, buttonId, OBJPROP_BORDER_COLOR, btn_border_color);
      ObjectSetInteger(0, buttonId, OBJPROP_BORDER_TYPE,  BORDER_RAISED);
      ObjectSetInteger(0, buttonId, OBJPROP_SELECTABLE,   false);
      ObjectSetInteger(0, buttonId, OBJPROP_HIDDEN,       true);
      ObjectSetInteger(0, buttonId, OBJPROP_TIMEFRAMES,   OBJ_ALL_PERIODS);
      ObjectSetInteger(0, buttonId, OBJPROP_ZORDER,       100);
   }

   UpdateToggleButton();
}

//+------------------------------------------------------------------+
//| Update Toggle Button Appearance                                  |
//+------------------------------------------------------------------+
void UpdateToggleButton()
{
   if(ObjectFind(0, buttonId) < 0) return;

   IndicatorEnabled = !IndicatorHidden;

   string labelText  = GetButtonText(IndicatorEnabled);
   color  textColor  = IndicatorEnabled ? btn_text_ON_color : btn_text_OFF_color;

   ObjectSetString (0, buttonId, OBJPROP_TEXT,         labelText);
   ObjectSetString (0, buttonId, OBJPROP_FONT,         btn_Font);
   ObjectSetInteger(0, buttonId, OBJPROP_FONTSIZE,     btn_FontSize);
   ObjectSetInteger(0, buttonId, OBJPROP_COLOR,        textColor);
   ObjectSetInteger(0, buttonId, OBJPROP_BGCOLOR,      btn_background_color);
   ObjectSetInteger(0, buttonId, OBJPROP_BORDER_COLOR, btn_border_color);
   ObjectSetInteger(0, buttonId, OBJPROP_CORNER,       btn_corner);
   ObjectSetInteger(0, buttonId, OBJPROP_XDISTANCE,    button_x);
   ObjectSetInteger(0, buttonId, OBJPROP_XSIZE,        btn_Width);
   ObjectSetInteger(0, buttonId, OBJPROP_YSIZE,        btn_Height);
   ObjectSetInteger(0, buttonId, OBJPROP_STATE,        IndicatorEnabled);
   ObjectSetInteger(0, buttonId, OBJPROP_ZORDER,       100);

   ChartRedraw(0);
}

//+------------------------------------------------------------------+
//| Ensure all buttons on chart have higher ZORDER                   |
//+------------------------------------------------------------------+
void BringButtonsToFront()
{
   int total = ObjectsTotal(0, -1, OBJ_BUTTON);
   for(int i = 0; i < total; i++)
   {
      string name = ObjectName(0, i, -1, OBJ_BUTTON);
      if(name != "")
      {
         if(ObjectGetInteger(0, name, OBJPROP_ZORDER) < 100)
            ObjectSetInteger(0, name, OBJPROP_ZORDER, 100);
      }
   }
}

//+------------------------------------------------------------------+
//| Arrow management                                                 |
//+------------------------------------------------------------------+
void DrawArrow(int i, color theColor, int theCode, int arrowSize, bool up, const datetime &time[], const double &high[], const double &low[], double atrGap)
{
   string name = arrowsIdentifier + ":" + (string)((ulong)time[i]);
   ObjectDelete(0, name);

   double price = up ? (high[i] + atrGap) : (low[i] - atrGap);

   if(ObjectCreate(0, name, OBJ_ARROW, 0, time[i], price))
   {
      ObjectSetInteger(0, name, OBJPROP_ARROWCODE, theCode);
      ObjectSetInteger(0, name, OBJPROP_COLOR, theColor);
      ObjectSetInteger(0, name, OBJPROP_WIDTH, arrowSize);
      ObjectSetInteger(0, name, OBJPROP_ANCHOR, up ? ANCHOR_BOTTOM : ANCHOR_TOP);
      ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   }
}

void DeleteArrow(datetime t)
{
   string name = arrowsIdentifier + ":" + (string)((ulong)t);
   ObjectDelete(0, name);
}

void DeleteArrows()
{
   string lookFor = arrowsIdentifier + ":";
   int total = ObjectsTotal(0, -1, -1);
   for(int i = total - 1; i >= 0; i--)
   {
      string name = ObjectName(0, i, -1, -1);
      if(StringFind(name, lookFor) == 0)
         ObjectDelete(0, name);
   }
}

//+------------------------------------------------------------------+
//| Redraw arrows if indicator is enabled                            |
//+------------------------------------------------------------------+
void RedrawArrows()
{
   if(!arrowsVisible || IndicatorHidden)
      return;

   int total = ArraySize(trendBuffer);
   if(total < 2) return;

   datetime timeArr[];
   double highArr[], lowArr[];
   ArraySetAsSeries(timeArr, true);
   ArraySetAsSeries(highArr, true);
   ArraySetAsSeries(lowArr, true);

   int copied = CopyTime(_Symbol, _Period, 0, total, timeArr);
   int cHigh  = CopyHigh(_Symbol, _Period, 0, total, highArr);
   int cLow   = CopyLow(_Symbol, _Period, 0, total, lowArr);

   if(copied < 2 || cHigh < 2 || cLow < 2) return;

   int count = MathMin(copied, MathMin(cHigh, cLow));
   if(count > total) count = total;

   for(int i = count - 2; i >= 0; i--)
   {
      if(trendBuffer[i] != trendBuffer[i + 1])
      {
         double atrGap = 0.0;
         if(atrHandle != INVALID_HANDLE)
         {
            double atrArr[];
            if(CopyBuffer(atrHandle, 0, i, 1, atrArr) > 0)
               atrGap = atrArr[0];
         }
         if(atrGap <= 0.0)
            atrGap = (highArr[i] - lowArr[i]) > 0.0 ? (highArr[i] - lowArr[i]) * 0.5 : 10 * _Point;

         if(trendBuffer[i] == 1.0)
            DrawArrow(i, arrowsUpColor, UpArrowSymbolCode, UpArrowSize, false, timeArr, highArr, lowArr, atrGap);
         else if(trendBuffer[i] == -1.0)
            DrawArrow(i, arrowsDnColor, DownArrowSymbolCode, DownArrowSize, true, timeArr, highArr, lowArr, atrGap);
      }
   }
}

//+------------------------------------------------------------------+
//| Toggle Indicator Visibility                                      |
//+------------------------------------------------------------------+
void ToggleIndicatorVisibility()
{
   IndicatorHidden  = !IndicatorHidden;
   IndicatorEnabled = !IndicatorHidden;

   if(g_gvHidden != "")
      GlobalVariableSet(g_gvHidden, IndicatorHidden ? 1.0 : 0.0);

   if(IndicatorHidden)
   {
      PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE);
      PlotIndexSetInteger(1, PLOT_DRAW_TYPE, DRAW_NONE);
      PlotIndexSetInteger(2, PLOT_DRAW_TYPE, DRAW_NONE);
      PlotIndexSetInteger(3, PLOT_DRAW_TYPE, DRAW_NONE);
      PlotIndexSetInteger(0, PLOT_SHOW_DATA, false);
      PlotIndexSetInteger(1, PLOT_SHOW_DATA, false);
      PlotIndexSetInteger(2, PLOT_SHOW_DATA, false);
      PlotIndexSetInteger(3, PLOT_SHOW_DATA, false);
      DeleteArrows();
   }
   else
   {
      PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_HISTOGRAM);
      PlotIndexSetInteger(1, PLOT_DRAW_TYPE, DRAW_HISTOGRAM);
      PlotIndexSetInteger(2, PLOT_DRAW_TYPE, DRAW_LINE);
      PlotIndexSetInteger(3, PLOT_DRAW_TYPE, DRAW_LINE);
      PlotIndexSetInteger(0, PLOT_SHOW_DATA, true);
      PlotIndexSetInteger(1, PLOT_SHOW_DATA, true);
      PlotIndexSetInteger(2, PLOT_SHOW_DATA, true);
      PlotIndexSetInteger(3, PLOT_SHOW_DATA, true);
      if(arrowsVisible)
         RedrawArrows();
   }

   UpdateToggleButton();
   BringButtonsToFront();
   ChartRedraw(0);
}

//+------------------------------------------------------------------+
//| Alerts management                                                |
//+------------------------------------------------------------------+
void DoAlert(int forBar, string doWhat, ENUM_TIMEFRAMES tf, const datetime &time[])
{
   static string   previousAlert = "nothing";
   static datetime previousTime  = 0;

   if(forBar < 0 || forBar >= ArraySize(time))
      return;

   datetime barTime = time[forBar];

   if(previousAlert != doWhat || previousTime != barTime)
   {
      previousAlert = doWhat;
      previousTime  = barTime;

      string message = _Symbol + " " + TimeframeToString(tf) + " at " + TimeToString(TimeLocal(), TIME_SECONDS) + " " + crossDescription + doWhat;

      if(alertsMessage)
         Alert(message);
      if(alertsSound)
         PlaySound(soundFile);
      if(alertsEmail)
         SendMail(_Symbol + " Zero Lag MACD", message);
      if(alertsPush)
         SendNotification(message);
   }
}

void ManageAlerts(ENUM_TIMEFRAMES tf, const datetime &time[])
{
   if(!alertsOn)
      return;

   int whichBar = alertsOnCurrent ? 0 : 1;
   if(tf > _Period)
   {
      datetime htfTime = iTime(_Symbol, tf, whichBar);
      if(htfTime > 0)
         whichBar = iBarShift(_Symbol, _Period, htfTime, false);
      if(whichBar < 0)
         whichBar = alertsOnCurrent ? 0 : 1;
   }

   if(whichBar + 1 >= ArraySize(trendBuffer))
      return;

   if(trendBuffer[whichBar] != trendBuffer[whichBar + 1])
   {
      if(trendBuffer[whichBar] == 1.0)
         DoAlert(whichBar, "up", tf, time);
      else if(trendBuffer[whichBar] == -1.0)
         DoAlert(whichBar, "down", tf, time);
   }
}

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
   if(FastEMA <= 0 || SlowEMA <= 0 || SignalEMA <= 0)
   {
      Print("ZeroLag MACD: EMA periods must be greater than 0");
      return INIT_PARAMETERS_INCORRECT;
   }

   SetIndexBuffer(0, machBuffer, INDICATOR_DATA);
   SetIndexBuffer(1, osmaBuffer, INDICATOR_DATA);
   SetIndexBuffer(2, macdBuffer, INDICATOR_DATA);
   SetIndexBuffer(3, signBuffer, INDICATOR_DATA);
   SetIndexBuffer(4, trendBuffer, INDICATOR_CALCULATIONS);

   ArraySetAsSeries(machBuffer, true);
   ArraySetAsSeries(osmaBuffer, true);
   ArraySetAsSeries(macdBuffer, true);
   ArraySetAsSeries(signBuffer, true);
   ArraySetAsSeries(trendBuffer, true);

   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetDouble(2, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetDouble(3, PLOT_EMPTY_VALUE, EMPTY_VALUE);

   PlotIndexSetString(0, PLOT_LABEL, "Zero Lag MACD (Hist)");
   PlotIndexSetString(1, PLOT_LABEL, "Zero Lag OsMA");
   PlotIndexSetString(2, PLOT_LABEL, "Zero Lag MACD");
   PlotIndexSetString(3, PLOT_LABEL, "Signal line");

   switch(AlertsArrowsOn)
   {
      case ALERT_MACD_CROSS_SIGNAL: crossDescription = "Zero Lag MACD - MACD crossed signal line ";      break;
      case ALERT_MACD_CROSS_ZERO:   crossDescription = "Zero Lag MACD - MACD crossed zero line ";        break;
      case ALERT_SIGNAL_CROSS_ZERO: crossDescription = "Zero Lag MACD - Signal line crossed zero line "; break;
   }

   ENUM_TIMEFRAMES tf = (timeFrame == PERIOD_CURRENT || timeFrame < _Period) ? (ENUM_TIMEFRAMES)_Period : timeFrame;
   string tfStr = (tf == _Period) ? "" : " (" + TimeframeToString(tf) + ")";
   IndicatorName = "Zero Lag MACD (" + (string)FastEMA + "," + (string)SlowEMA + "," + (string)SignalEMA + ")" + tfStr;
   IndicatorSetString(INDICATOR_SHORTNAME, IndicatorName);
   IndicatorSetInteger(INDICATOR_DIGITS, _Digits + 1);

   atrHandle = iATR(_Symbol, _Period, 20);

//--- Set up unique IDs and global variables for state tracking
   IndicatorObjPrefix = "__" + btn_text + "__";
   buttonId           = IndicatorObjPrefix + UniqueButtonID;
   ToggleButtonName   = buttonId;
   g_prefix           = "ZLMacd_BT_" + UniqueButtonID + "_";
   g_gvHidden         = g_prefix + IntegerToString(ChartID()) + "_Hidden";

//--- Clean up any old duplicate or phantom buttons
   int total_objs = ObjectsTotal(0, -1, OBJ_BUTTON);
   for(int i = total_objs - 1; i >= 0; i--)
   {
      string bName = ObjectName(0, i, -1, OBJ_BUTTON);
      if(StringFind(bName, UniqueButtonID) >= 0 || StringFind(bName, "ZLMacd_BT") >= 0)
      {
         if(bName != buttonId)
            ObjectDelete(0, bName);
      }
   }

//--- When parameters are changed by user in properties dialog, delete old global variable and respect inputs
   bool isParamChange = (UninitializeReason() == REASON_PARAMETERS);
   if(isParamChange && g_gvHidden != "")
   {
      GlobalVariableDel(g_gvHidden);
   }

//--- Restore toggle state across timeframe/symbol changes
   bool hasSavedState = false;
   bool savedHidden   = false;

   if(!isParamChange)
   {
      if(ObjectFind(0, buttonId) >= 0)
      {
         string btnTxt = ObjectGetString(0, buttonId, OBJPROP_TEXT);
         long   btnSt  = ObjectGetInteger(0, buttonId, OBJPROP_STATE);

         if(StringFind(btnTxt, "OFF") >= 0 || StringFind(btnTxt, "Off") >= 0 || btnSt == 0)
            savedHidden = true;
         else if(StringFind(btnTxt, "ON") >= 0 || StringFind(btnTxt, "On") >= 0 || btnSt != 0)
            savedHidden = false;
         hasSavedState = true;
      }

      if(!hasSavedState && GlobalVariableCheck(g_gvHidden))
      {
         savedHidden   = (GlobalVariableGet(g_gvHidden) > 0.5);
         hasSavedState = true;
      }
   }

   if(hasSavedState)
      IndicatorHidden = savedHidden;
   else
      IndicatorHidden = false;

   IndicatorEnabled = !IndicatorHidden;

   if(g_gvHidden != "")
      GlobalVariableSet(g_gvHidden, IndicatorHidden ? 1.0 : 0.0);

   if(IndicatorHidden)
   {
      PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE);
      PlotIndexSetInteger(1, PLOT_DRAW_TYPE, DRAW_NONE);
      PlotIndexSetInteger(2, PLOT_DRAW_TYPE, DRAW_NONE);
      PlotIndexSetInteger(3, PLOT_DRAW_TYPE, DRAW_NONE);
      PlotIndexSetInteger(0, PLOT_SHOW_DATA, false);
      PlotIndexSetInteger(1, PLOT_SHOW_DATA, false);
      PlotIndexSetInteger(2, PLOT_SHOW_DATA, false);
      PlotIndexSetInteger(3, PLOT_SHOW_DATA, false);
   }

   CreateToggleButton();
   BringButtonsToFront();

   return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   if(reason == REASON_REMOVE || reason == REASON_CHARTCLOSE || reason == REASON_PARAMETERS)
   {
      ObjectDelete(0, buttonId);
      ObjectDelete(0, ToggleButtonName);
      if(g_gvHidden != "")
         GlobalVariableDel(g_gvHidden);
   }

   DeleteArrows();
   if(atrHandle != INVALID_HANDLE)
   {
      IndicatorRelease(atrHandle);
      atrHandle = INVALID_HANDLE;
   }
   ChartRedraw(0);
}

//+------------------------------------------------------------------+
//| Custom indicator chart's event handler                           |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
{
   if(id == CHARTEVENT_OBJECT_CLICK)
   {
      string altId = "_" + UniqueButtonID + IndicatorObjPrefix + "_BT_";
      if(sparam == buttonId || sparam == ToggleButtonName || sparam == altId)
      {
         ToggleIndicatorVisibility();
         return;
      }
   }

   if(id >= CHARTEVENT_CUSTOM)
      return;

   if(CHARTEVENT_CHART_CHANGE == id)
   {
      BringButtonsToFront();
   }
}

//+------------------------------------------------------------------+
//| 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(prev_calculated <= 0)
   {
      CreateToggleButton();
      BringButtonsToFront();
   }

   if(rates_total < 2)
      return 0;

   ArraySetAsSeries(time, true);
   ArraySetAsSeries(open, true);
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low, true);
   ArraySetAsSeries(close, true);

   ENUM_TIMEFRAMES tf = (timeFrame == PERIOD_CURRENT || timeFrame < _Period) ? (ENUM_TIMEFRAMES)_Period : timeFrame;

   if(!arrowsVisible || IndicatorHidden)
      DeleteArrows();

   double alpha1 = 2.0 / (1.0 + FastEMA);
   double alpha2 = 2.0 / (1.0 + SlowEMA);
   double alpha3 = 2.0 / (1.0 + SignalEMA);

   int limit;

   if(tf <= _Period)
   {
      // Current timeframe calculation
      if(prev_calculated == 0)
      {
         limit = rates_total - 1;
         ArrayResize(workBuffer, rates_total);
      }
      else
      {
         limit = rates_total - prev_calculated;
         if(limit > 0)
            ArrayResize(workBuffer, rates_total);
         limit++;
         if(limit >= rates_total)
            limit = rates_total - 1;
      }

      for(int i = limit; i >= 0; i--)
      {
         int r = rates_total - 1 - i;
         double price = GetPrice(MacdPrice, open, high, low, close, i);

         if(r == 0)
         {
            workBuffer[0].ema11 = price;
            workBuffer[0].ema12 = price;
            workBuffer[0].ema21 = price;
            workBuffer[0].ema22 = price;
            macdBuffer[i]       = 0.0;
            machBuffer[i]       = 0.0;
            workBuffer[0].ema31 = 0.0;
            workBuffer[0].ema32 = 0.0;
            signBuffer[i]       = 0.0;
            osmaBuffer[i]       = 0.0;
            trendBuffer[i]      = 0.0;
            continue;
         }

         workBuffer[r].ema11 = workBuffer[r-1].ema11 + alpha1 * (price - workBuffer[r-1].ema11);
         workBuffer[r].ema12 = workBuffer[r-1].ema12 + alpha1 * (workBuffer[r].ema11 - workBuffer[r-1].ema12);
         workBuffer[r].ema21 = workBuffer[r-1].ema21 + alpha2 * (price - workBuffer[r-1].ema21);
         workBuffer[r].ema22 = workBuffer[r-1].ema22 + alpha2 * (workBuffer[r].ema21 - workBuffer[r-1].ema22);

         macdBuffer[i] = (2.0 * workBuffer[r].ema11 - workBuffer[r].ema12) - (2.0 * workBuffer[r].ema21 - workBuffer[r].ema22);
         machBuffer[i] = macdBuffer[i];

         workBuffer[r].ema31 = workBuffer[r-1].ema31 + alpha3 * (macdBuffer[i] - workBuffer[r-1].ema31);
         workBuffer[r].ema32 = workBuffer[r-1].ema32 + alpha3 * (workBuffer[r].ema31 - workBuffer[r-1].ema32);

         signBuffer[i] = (2.0 * workBuffer[r].ema31 - workBuffer[r].ema32);
         osmaBuffer[i] = macdBuffer[i] - signBuffer[i];

         double prev_trend = (i < rates_total - 1) ? trendBuffer[i + 1] : 0.0;
         trendBuffer[i] = prev_trend;

         switch(AlertsArrowsOn)
         {
            case ALERT_MACD_CROSS_SIGNAL:
               if(osmaBuffer[i] > 0.0) trendBuffer[i] = 1.0;
               else if(osmaBuffer[i] < 0.0) trendBuffer[i] = -1.0;
               break;
            case ALERT_MACD_CROSS_ZERO:
               if(macdBuffer[i] > 0.0) trendBuffer[i] = 1.0;
               else if(macdBuffer[i] < 0.0) trendBuffer[i] = -1.0;
               break;
            case ALERT_SIGNAL_CROSS_ZERO:
               if(signBuffer[i] > 0.0) trendBuffer[i] = 1.0;
               else if(signBuffer[i] < 0.0) trendBuffer[i] = -1.0;
               break;
         }

         if(!IndicatorHidden && arrowsVisible && i < rates_total - 1)
         {
            DeleteArrow(time[i]);
            if(trendBuffer[i] != trendBuffer[i + 1])
            {
               double atrGap = 0.0;
               if(atrHandle != INVALID_HANDLE)
               {
                  double atrArr[];
                  if(CopyBuffer(atrHandle, 0, i, 1, atrArr) > 0)
                     atrGap = atrArr[0];
               }
               if(atrGap <= 0.0)
                  atrGap = (high[i] - low[i]) > 0.0 ? (high[i] - low[i]) * 0.5 : 10 * _Point;

               if(trendBuffer[i] == 1.0)
                  DrawArrow(i, arrowsUpColor, UpArrowSymbolCode, UpArrowSize, false, time, high, low, atrGap);
               else if(trendBuffer[i] == -1.0)
                  DrawArrow(i, arrowsDnColor, DownArrowSymbolCode, DownArrowSize, true, time, high, low, atrGap);
            }
         }
      }
   }
   else
   {
      // Multi-timeframe calculation
      int htf_shift_oldest = iBarShift(_Symbol, tf, time[rates_total - 1], false);
      if(htf_shift_oldest < 0)
         htf_shift_oldest = Bars(_Symbol, tf) - 1;
      int htf_needed = htf_shift_oldest + 50;
      int total_available = Bars(_Symbol, tf);
      if(htf_needed > total_available)
         htf_needed = total_available;
      if(htf_needed < 2)
         return 0;

      MqlRates htf_rates[];
      ArraySetAsSeries(htf_rates, true);
      int copied = CopyRates(_Symbol, tf, 0, htf_needed, htf_rates);
      if(copied < 2)
         return 0;

      double htf_macd[];
      double htf_sign[];
      ArrayResize(htf_macd, copied);
      ArrayResize(htf_sign, copied);
      ArraySetAsSeries(htf_macd, true);
      ArraySetAsSeries(htf_sign, true);

      WorkData htf_work[];
      ArrayResize(htf_work, copied);

      for(int k = copied - 1, r = 0; k >= 0; k--, r++)
      {
         double p = GetPriceFromRate(MacdPrice, htf_rates[k]);
         if(r == 0)
         {
            htf_work[0].ema11 = p;
            htf_work[0].ema12 = p;
            htf_work[0].ema21 = p;
            htf_work[0].ema22 = p;
            htf_macd[k]       = 0.0;
            htf_work[0].ema31 = 0.0;
            htf_work[0].ema32 = 0.0;
            htf_sign[k]       = 0.0;
            continue;
         }

         htf_work[r].ema11 = htf_work[r-1].ema11 + alpha1 * (p - htf_work[r-1].ema11);
         htf_work[r].ema12 = htf_work[r-1].ema12 + alpha1 * (htf_work[r].ema11 - htf_work[r-1].ema12);
         htf_work[r].ema21 = htf_work[r-1].ema21 + alpha2 * (p - htf_work[r-1].ema21);
         htf_work[r].ema22 = htf_work[r-1].ema22 + alpha2 * (htf_work[r].ema21 - htf_work[r-1].ema22);

         htf_macd[k] = (2.0 * htf_work[r].ema11 - htf_work[r].ema12) - (2.0 * htf_work[r].ema21 - htf_work[r].ema22);

         htf_work[r].ema31 = htf_work[r-1].ema31 + alpha3 * (htf_macd[k] - htf_work[r-1].ema31);
         htf_work[r].ema32 = htf_work[r-1].ema32 + alpha3 * (htf_work[r].ema31 - htf_work[r-1].ema32);

         htf_sign[k] = (2.0 * htf_work[r].ema31 - htf_work[r].ema32);
      }

      int htf_ratio = (int)(PeriodSeconds(tf) / PeriodSeconds(_Period));
      if(htf_ratio < 1) htf_ratio = 1;

      if(prev_calculated == 0)
         limit = rates_total - 1;
      else
      {
         limit = rates_total - prev_calculated + htf_ratio * 2 + 5;
         if(limit >= rates_total)
            limit = rates_total - 1;
      }

      for(int i = limit; i >= 0; i--)
      {
         int shift1 = iBarShift(_Symbol, tf, time[i], false);
         if(shift1 < 0) shift1 = 0;
         if(shift1 >= copied) shift1 = copied - 1;

         macdBuffer[i] = htf_macd[shift1];
         signBuffer[i] = htf_sign[shift1];
         machBuffer[i] = macdBuffer[i];
         osmaBuffer[i] = macdBuffer[i] - signBuffer[i];

         if(Interpolate && i > 0)
         {
            int next_shift = iBarShift(_Symbol, tf, time[i - 1], false);
            if(shift1 != next_shift)
            {
               datetime time1 = htf_rates[shift1].time;
               int n = 1;
               while(i + n < rates_total && time[i + n] >= time1)
                  n++;

               if(i + n < rates_total)
               {
                  double factor = 1.0 / n;
                  for(int k = 1; k < n; k++)
                  {
                     macdBuffer[i + k] = k * factor * macdBuffer[i + n] + (1.0 - k * factor) * macdBuffer[i];
                     signBuffer[i + k] = k * factor * signBuffer[i + n] + (1.0 - k * factor) * signBuffer[i];
                     machBuffer[i + k] = macdBuffer[i + k];
                     osmaBuffer[i + k] = macdBuffer[i + k] - signBuffer[i + k];
                  }
               }
            }
         }
      }

      for(int i = limit; i >= 0; i--)
      {
         double prev_trend = (i < rates_total - 1) ? trendBuffer[i + 1] : 0.0;
         trendBuffer[i] = prev_trend;

         switch(AlertsArrowsOn)
         {
            case ALERT_MACD_CROSS_SIGNAL:
               if(osmaBuffer[i] > 0.0) trendBuffer[i] = 1.0;
               else if(osmaBuffer[i] < 0.0) trendBuffer[i] = -1.0;
               break;
            case ALERT_MACD_CROSS_ZERO:
               if(macdBuffer[i] > 0.0) trendBuffer[i] = 1.0;
               else if(macdBuffer[i] < 0.0) trendBuffer[i] = -1.0;
               break;
            case ALERT_SIGNAL_CROSS_ZERO:
               if(signBuffer[i] > 0.0) trendBuffer[i] = 1.0;
               else if(signBuffer[i] < 0.0) trendBuffer[i] = -1.0;
               break;
         }

         if(!IndicatorHidden && arrowsVisible && i < rates_total - 1)
         {
            DeleteArrow(time[i]);
            if(trendBuffer[i] != trendBuffer[i + 1])
            {
               double atrGap = 0.0;
               if(atrHandle != INVALID_HANDLE)
               {
                  double atrArr[];
                  if(CopyBuffer(atrHandle, 0, i, 1, atrArr) > 0)
                     atrGap = atrArr[0];
               }
               if(atrGap <= 0.0)
                  atrGap = (high[i] - low[i]) > 0.0 ? (high[i] - low[i]) * 0.5 : 10 * _Point;

               if(trendBuffer[i] == 1.0)
                  DrawArrow(i, arrowsUpColor, UpArrowSymbolCode, UpArrowSize, false, time, high, low, atrGap);
               else if(trendBuffer[i] == -1.0)
                  DrawArrow(i, arrowsDnColor, DownArrowSymbolCode, DownArrowSize, true, time, high, low, atrGap);
            }
         }
      }
   }

   if(!IndicatorHidden)
      ManageAlerts(tf, time);

   return rates_total;
}
//+------------------------------------------------------------------+
