//+------------------------------------------------------------------+
//|                                MarketProfile Canvas v. 1.01 BT.mq5 |
//|                              Copyright 2009-2024, MetaQuotes Ltd |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright   "Copyright 2022, MetaQuotes Ltd."
#property link        "https://www.mql5.com/en/articles/16579"
#property version     "1.01"
#property description "MarketProfile Canvas with On/Off Button"
#property indicator_chart_window
#property indicator_plots 0

#include <Canvas\Canvas.mqh>
#include <Generic\ArrayList.mqh>

//--- input parameters
input group "=== Market Profile Settings ==="
input uint  InpStartDate       =0;           /* day number to start calculation */  // 0 - current, 1 - previous, etc.
input uint  InpShowDays        =7;           /* number of days to display */        // starting with and including the day in InpStartDate
input int   InpMultiplier      =1;           /* histogram length multiplier */
input color InpAsiaSession     =clrGold;     /* Asian session */
input color InpEuropeSession   =clrBlue;     /* European session */
input color InpAmericaSession  =clrViolet;   /* American session */
input uchar InpTransparency    =150;         /* Transparency, 0 = invisible */      // market profile transparency, 0 = fully transparent
input uint  InpEuropeStartHour =8;           /* European session opening hour */
input uint  InpAmericaStartHour=14;          /* American session opening hour */

//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
input ENUM_BASE_CORNER   btn_corner            = CORNER_LEFT_UPPER;  // chart btn_corner for anchoring
input string             btn_text              = "MP: 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              = 100;                // Horizontal location
input int                button_y              = 40;                 // Vertical location
input int                btn_Width             = 80;                 // btn__width
input int                btn_Height            = 20;                 // btn__height
input string             UniqueButtonID        = "MPCanvasBT";       // 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;

//--- unique prefix to identify graphical objects belonging to the indicator
string ExtPrefixUniq;

//--- declare CMarketProfile class
class CMarketProfile;
//--- declare a list of pointers to objects of the CMarketProfile class
CArrayList<CMarketProfile*> mp_list;

//+------------------------------------------------------------------+
//| 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;
}

//+------------------------------------------------------------------+
//| Create Toggle Button                                             |
//+------------------------------------------------------------------+
void CreateToggleButton()
{
   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, btn_Subwindow, 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);
}

//+------------------------------------------------------------------+
//| Update all profiles visibility based on toggle state             |
//+------------------------------------------------------------------+
void UpdateAllProfilesVisibility()
{
   int size = mp_list.Count();
   for(int i = 0; i < size; i++)
   {
      CMarketProfile *market_profile;
      mp_list.TryGetValue(i, market_profile);
      if(market_profile != NULL && CheckPointer(market_profile) != POINTER_INVALID)
      {
         market_profile.SetVisible(!IndicatorHidden, InpMultiplier);
      }
   }
}

//+------------------------------------------------------------------+
//| Toggle Indicator Visibility                                      |
//+------------------------------------------------------------------+
void ToggleIndicatorVisibility()
{
   IndicatorHidden  = !IndicatorHidden;
   IndicatorEnabled = !IndicatorHidden;

   if(g_gvHidden != "")
      GlobalVariableSet(g_gvHidden, IndicatorHidden ? 1.0 : 0.0);

   UpdateAllProfilesVisibility();
   UpdateToggleButton();
   BringButtonsToFront();
   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);
      }
   }
}

//+------------------------------------------------------------------+
//| Helper: Get timeframe as string                                  |
//+------------------------------------------------------------------+
string GetStringTimeframe(ENUM_TIMEFRAMES timeframe)
{
   return(StringSubstr(EnumToString(timeframe), 7));
}

//+------------------------------------------------------------------+
//| Forward declarations of helper functions                         |
//+------------------------------------------------------------------+
CMarketProfile* GetMarketProfileByDate(string prefix, datetime time);

//+------------------------------------------------------------------+
//| Class to store and draw Market Profile for the daily bar         |
//+------------------------------------------------------------------+
class CMarketProfile
  {
public:
                     CMarketProfile() {};
                     CMarketProfile(string prefix, datetime time1, datetime time2, double high, double low, MqlRates &bars[]);
                    ~CMarketProfile(void);

   //--- check if the object was created for the specified date
   bool              Check(string prefix, datetime time);
   //--- set high/low and array of intraday bars
   void              SetHiLoBars(double high, double low, MqlRates &bars[]);
   //--- set canvas dimensions and drawing options
   void              UpdateSizes(void);
   //--- is the profile in the visible part of the chart?
   bool              isVisibleOnChart(void);
   //--- has the chart scale changed?
   bool              isChartScaleChanged(void);
   //--- calculate profile by sessions
   bool              CalculateSessions(void);
   //--- draw a profile
   void              Draw(double multiplier=1.0);
   //--- set profile visibility
   void              SetVisible(bool visible, double multiplier=1.0);

protected:
   CCanvas           m_canvas;      // CCanvas class object for drawing profile
   uchar             m_alpha;       // alpha channel value that sets transparency
   string            m_prefix;      // unique prefix of OBJ_BITMAP object
   string            m_name;        // name of the OBJ_BITMAP object used in m_canvas
   double            m_high;        // day High
   double            m_low;         // day Low
   datetime          m_time1;       // start time of the day
   datetime          m_time2;       // end time of the day
   int               m_day_size_pt; // daily bar height in points
   int               m_height;      // daily bar height in pixels on the chart
   int               m_width;       // daily bar width in pixels on the chart
   MqlRates          m_bars[];      // array of bars of the current timeframe between m_time1 and m_time2
   vector            m_asia;        // array of bar counters for the Asian session
   vector            m_europe;      // array of bar counters for the European session
   vector            m_america;     // array of bar counters for the American session
   double            m_vert_scale;  // vertical scaling factor
   double            m_hor_scale;   // horizontal scaling factor
  };

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
void CMarketProfile::CMarketProfile(string prefix, datetime time1, datetime time2, double high, double low, MqlRates &bars[]):
   m_prefix(prefix),
   m_time1(time1),
   m_time2(time2),
   m_high(high),
   m_low(low),
   m_vert_scale(NULL),
   m_hor_scale(NULL)
  {
//--- copy the array of intraday bars to the array of MqlRates structures,
//--- create a name for the graphical object and define the size of the daily candle
   ArrayCopy(m_bars, bars);
   m_name=ExtPrefixUniq+"_MP_"+TimeToString(time1, TIME_DATE);
   m_day_size_pt=(int)((m_high-m_low)/SymbolInfoDouble(Symbol(), SYMBOL_POINT));
//--- set vector sizes for trading sessions
   m_asia=vector::Zeros(m_day_size_pt);
   m_europe=vector::Zeros(m_day_size_pt);
   m_america=vector::Zeros(m_day_size_pt);
//--- set the width and height of the canvas
   UpdateSizes();
//--- if this is the first tick at the beginning of the day, then the canvas dimensions will be zero - set the dimensions to 1 pixel in height and width
   m_height=m_height?m_height:1;
   m_width=m_width?m_width:1;
//--- create a graphical object
   if(m_canvas.CreateBitmap(m_name, m_time1, m_high, m_width, m_height, COLOR_FORMAT_ARGB_NORMALIZE))
      ObjectSetInteger(0, m_name, OBJPROP_BACK, true);
   else
     {
      Print("Error creating canvas: ", GetLastError());
      Print("time1=", m_time1, "  high=", m_high, "  width=", m_width, "  height=", m_height);
     }
  }

//+------------------------------------------------------------------+
//| Checks if CMarketProfile object is for the specified 'time' date |
//+------------------------------------------------------------------+
bool CMarketProfile::Check(string prefix, datetime time)
  {
   string calculated= prefix+"_MP_"+TimeToString(time, TIME_DATE);
   return (m_name==(calculated));
  }

//+------------------------------------------------------------------+
//| Sets High/Low and a set of current-timeframe bars                |
//+------------------------------------------------------------------+
void CMarketProfile::SetHiLoBars(double high, double low, MqlRates &bars[])
  {
//--- if the maximum of the day has changed, move the OBJ_BITMAP object to the new Y coordinate
   if(high>m_high)
     {
      m_high=high;
      if(!ObjectSetDouble(0, m_name, OBJPROP_PRICE, m_high))
         PrintFormat("Failed to update canvas for %s, error %d", TimeToString(m_time1, TIME_DATE), GetLastError());
     }
   ArrayCopy(m_bars, bars);
   m_high=high;
   m_low=low;
//--- daily range in points
   m_day_size_pt=(int)((m_high-m_low)/SymbolInfoDouble(Symbol(), SYMBOL_POINT));
//--- reset vector sizes for trading sessions
   m_asia=vector::Zeros(m_day_size_pt);
   m_europe=vector::Zeros(m_day_size_pt);
   m_america=vector::Zeros(m_day_size_pt);
  }

//+------------------------------------------------------------------+
//|  Sets drawing parameters                                         |
//+------------------------------------------------------------------+
void CMarketProfile::UpdateSizes(void)
  {
//--- convert time/price to x/y coordinates
   int x1, y1, x2, y2;
   ChartTimePriceToXY(0, 0, m_time1, m_high, x1, y1);
   ChartTimePriceToXY(0, 0, m_time2, m_low,  x2, y2);
//--- calculate canvas dimensions
   m_height=y2-y1;
   m_width =x2-x1;
//--- calculate ratios for transforming vertical price levels
//--- and horizontal bar counters to chart pixels
   m_vert_scale=double(m_height)/(m_day_size_pt > 0 ? m_day_size_pt : 1);
   m_hor_scale =double(m_width*PeriodSeconds(PERIOD_CURRENT))/PeriodSeconds(PERIOD_D1);
   
//--- change the canvas size
   m_canvas.Resize(m_width, m_height);
  }

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CMarketProfile::~CMarketProfile(void)
  {
//--- delete all graphical objects after use
   m_canvas.Destroy();
   ObjectDelete(0, m_name);
   ObjectsDeleteAll(0, m_prefix, 0, OBJ_BITMAP);
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//|  Checks that the profile is in the visible part of the chart     |
//+------------------------------------------------------------------+
bool CMarketProfile::isVisibleOnChart(void)
  {
   long last_bar=ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR);        // last visible bar on the chart on the left
   long first_bar=last_bar-ChartGetInteger(0, CHART_VISIBLE_BARS);   // first visible bar on the chart on the right
   first_bar=first_bar>0?first_bar:0;
   datetime left =iTime(Symbol(), Period(), (int)last_bar);          // time of the left visible bar on the chart
   datetime right=iTime(Symbol(), Period(), (int)first_bar);         // time of the right visible bar on the chart
   
//--- return a flag that the canvas is located inside the left and right visible bars of the chart
   return((m_time1>= left && m_time1 <=right) || (m_time2>= left && m_time2 <=right));
  }

//+------------------------------------------------------------------+
//| Prepares profile arrays by sessions                              |
//+------------------------------------------------------------------+
bool CMarketProfile::CalculateSessions(void)
  {
   double point=SymbolInfoDouble(Symbol(), SYMBOL_POINT);   // one point value
//--- if the array of intraday bars is not filled, leave
   if(ArraySize(m_bars)==0)
      return(false);
//---- iterate over all the bars of the current day and mark the cells of the arrays (vectors) that contain the bars being iterated over in the loop
   int size=ArraySize(m_bars);
   for(int i=0; i<size; i++)
     {
      //--- get the bar hour
      MqlDateTime bar_time;
      TimeToStruct(m_bars[i].time, bar_time);
      uint        hour     =bar_time.hour;
      //--- calculate price levels in points from the Low of the day reached by the price on each bar of the loop
      int         start_box=(int)((m_bars[i].low-m_low)/point);   // index of the beginning of price levels reached by the price on the bar
      int         stop_box =(int)((m_bars[i].high-m_low)/point);  // index of the end of price levels reached by the price on the bar

      //--- American session
      if(hour>=InpAmericaStartHour)
        {
         //--- in the loop from the beginning to the end of price levels, fill the counters of bars where the price was at this level
         for(int ind=start_box; ind<stop_box; ind++)
            m_america[ind]++;
        }
      else
        {
         //--- European session
         if(hour>=InpEuropeStartHour && hour<InpAmericaStartHour)
            //--- in the loop from the beginning to the end of price levels, fill the counters of bars where the price was at this level
            for(int ind=start_box; ind<stop_box; ind++)
               m_europe[ind]++;
         //--- Asian session
         else
            //--- in the loop from the beginning to the end of price levels, fill the counters of bars where the price was at this level
            for(int ind=start_box; ind<stop_box; ind++)
               m_asia[ind]++;
        }
     }
//--- vectors of all sessions are ready
   return(true);
  }

//+------------------------------------------------------------------+
//|  Draw Market Profile on the canvas                               |
//+------------------------------------------------------------------+
void CMarketProfile::Draw(double multiplier=1.0)
  {
//--- sum up all sessions for rendering
   vector total_profile=m_asia+m_europe+m_america;   // profile that combines all sessions
   vector europe_asia=m_asia+m_europe;               // profile that combines only the European and Asian sessions

//--- set a completely transparent background for the canvas
   m_canvas.Erase(ColorToARGB(clrBlack, 0));

//--- variables for drawing rectangles
   int x1=0;                           // X coordinate of the left corner of the rectangle always starts at zero
   int y1, x2, y2;                     // rectangle coordinates
   int size=(int)total_profile.Size(); // size of all sessions
   
//--- render the American session with filled rectangles
   for(int i=0; i<size; i++)
     {
      //--- skip zero vector values
      if(total_profile[i]==0)
         continue;
      //--- calculate two points to draw a rectangle, x1 is always 0 (X of the lower left corner of the rectangle)
      y1=m_height-int(i*m_vert_scale);                    // Y coordinate of the lower left corner of the rectangle
      y2=(int)(y1+m_vert_scale);                          // Y coordinate of the upper right corner of the rectangle
      x2=(int)(total_profile[i]*m_hor_scale*multiplier);  // X coordinate of the upper right corner of the rectangle
      //--- draw a rectangle at the calculated coordinates with the color and transparency set for the American session
      m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpAmericaSession, InpTransparency));
     }

//--- render the European session with filled rectangles
   for(int i=0; i<size; i++)
     {
      //--- skip zero vector values
      if(total_profile[i]==0)
         continue;
      //--- calculate two points to draw a rectangle
      y1=m_height-int(i*m_vert_scale);
      y2=(int)(y1+m_vert_scale);
      x2=(int)(europe_asia[i]*m_hor_scale*multiplier);
      //--- draw a rectangle over the rendered American session using the calculated coordinates
      //--- with color and transparency set for the European session
      m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpEuropeSession, InpTransparency));
     }

//--- draw the Asian session with filled rectangles
   for(int i=0; i<size; i++)
     {
      //--- skip zero vector values
      if(total_profile[i]==0)
         continue;
      //--- calculate two points to draw a rectangle
      y1=m_height-int(i*m_vert_scale);
      y2=(int)(y1+m_vert_scale);
      x2=(int)(m_asia[i]*m_hor_scale*multiplier);
      //--- draw a rectangle over the rendered European session using the calculated coordinates
      //--- with color and transparency set for the Asian session
      m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpAsiaSession, InpTransparency));
     }
//--- update the OBJ_BITMAP object without redrawing the chart
   m_canvas.Update(false);
  }

//+------------------------------------------------------------------+
//| Sets visibility of the Market Profile canvas                     |
//+------------------------------------------------------------------+
void CMarketProfile::SetVisible(bool visible, double multiplier=1.0)
  {
   if(!visible)
     {
      m_canvas.Erase(ColorToARGB(clrBlack, 0));
      m_canvas.Update(false);
      ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
     }
   else
     {
      ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS);
      UpdateSizes();
      Draw(multiplier);
     }
  }

//+------------------------------------------------------------------+
//| Returns CMarketProfile or NULL by the date                       |
//+------------------------------------------------------------------+
CMarketProfile* GetMarketProfileByDate(string prefix, datetime time)
  {
//--- in a loop by the number of CMarketProfile objects in the list
   int size=mp_list.Count();
   for(int i=0; i<size; i++)
     {
      //--- get the pointer to the CMarketProfile object by the loop index
      CMarketProfile *market_profile;
      mp_list.TryGetValue(i, market_profile);
      //--- if the pointer is valid and the object exists,
      if(market_profile!=NULL)
         if(CheckPointer(market_profile)!=POINTER_INVALID)
           {
            //--- if the Market Profile object obtained by the pointer was created for the required time, return the pointer
            if(market_profile.Check(prefix, time))
               return(market_profile);
           }
     }
//--- nothing found - return NULL
   return(NULL);
  }

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- create a prefix for object names
   string number=StringFormat("%I64d", GetTickCount64());
   ExtPrefixUniq=StringSubstr(number, StringLen(number)-4);
   Print("Indicator \"Market Profile Canvas\" started, prefix=", ExtPrefixUniq);

//--- Set up unique IDs and global variables for state tracking
   IndicatorObjPrefix = "__" + btn_text + "__";
   buttonId           = IndicatorObjPrefix + UniqueButtonID;
   ToggleButtonName   = buttonId;
   IndicatorName      = "[" + Symbol() + "] [" + GetStringTimeframe(_Period) + "] Market Profile";
   g_prefix           = "MPCanvas_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, "MPCanvas_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);

   CreateToggleButton();
   BringButtonsToFront();

   IndicatorSetString(INDICATOR_SHORTNAME, IndicatorName);

   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- delete all Market Profile graphical objects after use
   Print("Indicator \"Market Profile Canvas\" stopped, delete all objects CMarketProfile with prefix=", ExtPrefixUniq);

   if(reason == REASON_REMOVE || reason == REASON_CHARTCLOSE || reason == REASON_PARAMETERS)
     {
      ObjectDelete(0, buttonId);
      ObjectDelete(0, ToggleButtonName);
      if(g_gvHidden != "")
         GlobalVariableDel(g_gvHidden);
     }

//--- in a loop by the number of CMarketProfile objects in the list
   int size=mp_list.Count();
   for(int i=0; i<size; i++)
     {
      //--- get the pointer to the CMarketProfile object from the list by the loop index
      CMarketProfile *market_profile;
      mp_list.TryGetValue(i, market_profile);
      //--- if the pointer is valid and the object exists, delete it
      if(market_profile!=NULL)
         if(CheckPointer(market_profile)!=POINTER_INVALID)
            delete market_profile;
     }
   mp_list.Clear();

//--- delete any leftover bitmaps with ExtPrefixUniq
   ObjectsDeleteAll(0, ExtPrefixUniq, 0, OBJ_BITMAP);

//--- redraw the chart to display the result immediately
   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 this is a user event, leave
   if(id>=CHARTEVENT_CUSTOM)
      return;

//--- if there is a chart change, update the sizes of all objects of the CMarketProfile class with redrawing the chart
   if(CHARTEVENT_CHART_CHANGE==id)
     {
      BringButtonsToFront();

      if(!IndicatorHidden)
        {
         //--- in a loop by the number of CMarketProfile objects in the list
         int size=mp_list.Count();
         for(int i=0; i<size; i++)
           {
            //--- get the pointer to the CMarketProfile object by the loop index
            CMarketProfile *market_profile;
            mp_list.TryGetValue(i, market_profile);
            //--- if the object is received and if it is in the visible area of the chart
            if(market_profile)
               if(market_profile.isVisibleOnChart())
                 {
                  //--- update canvas dimensions and redraw market profile histograms
                  market_profile.UpdateSizes();
                  market_profile.Draw(InpMultiplier);
                 }
           }
         //--- update the chart after recalculating all Profiles
         ChartRedraw();
        }
     }
  }

//+------------------------------------------------------------------+
//| 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();
     }

//--- opening time of the current daily bar
   datetime static open_time=0;

//--- number of the last day for calculations
//--- (if InpStartDate = 0 and InpShowDays = 3, lastday = 3)
//--- (if InpStartDate = 1 and InpShowDays = 3, lastday = 4) etc ...
   uint lastday=InpStartDate+InpShowDays;

//--- if the first calculation has already been made
   if(prev_calculated!=0)
     {
      //--- get the opening time of the current daily bar
      datetime current_open=iTime(Symbol(), PERIOD_D1, 0);
      
      //--- if we do not calculate the current day
      if(InpStartDate!=0)
        {
         //--- if the opening time was not received, leave
         if(open_time==current_open)
            return(rates_total);
        }
      //--- update opening time
      open_time=current_open;
      //--- we will only calculate one day from now on, since all other days have already been calculated during the first run
      lastday=InpStartDate+1;
     }

//--- in a loop for the specified number of days (either InpStartDate+InpShowDays on first run, or InpStartDate+1 on each tick)
   for(uint day=InpStartDate; day<lastday; day++)
     {
      //--- get the data of the day with the 'day' index into the structure
      MqlRates day_rate[];
      //--- if the indicator is launched on weekends or holidays when there are no ticks, you should first open the daily chart of the symbol
      //--- if we have not received bar data for the day index of the daily period, we leave until the next call to OnCalculate()
      if(CopyRates(Symbol(), PERIOD_D1, day, 1, day_rate)==-1)
         return(prev_calculated);

      //--- get day start and end time
      datetime start_time=day_rate[0].time;
      datetime stop_time=start_time+PeriodSeconds(PERIOD_D1)-1;

      //--- get all intraday bars of the current day
      MqlRates bars_in_day[];
      if(CopyRates(Symbol(), PERIOD_CURRENT, start_time, stop_time, bars_in_day)==-1)
         return(prev_calculated);

      CMarketProfile *market_profile;
      //--- if the Market Profile has already been created and its drawing has been performed earlier
      if(prev_calculated>0)
        {
         //--- find the Market Profile object (CMarketProfile class) in the list by the opening time of the day with the 'day' index
         market_profile=GetMarketProfileByDate(ExtPrefixUniq, start_time);
         //--- if the object is not found, return zero to completely recalculate the indicator
         if(market_profile==NULL)
           {
            PrintFormat("Market Profile not found for %s. Indicator will be recalculated for all specified days",
                        TimeToString(start_time, TIME_DATE));
            return(0);
           }
         //--- CMarketProfile object is found in the list; set it to High and Low values of the day and pass the array of intraday bars
         //--- in this case, the object is shifted to a new coordinate corresponding to the High of the daily candle, and all arrays (vectors) are reinitialized
         market_profile.SetHiLoBars(day_rate[0].high, day_rate[0].low, bars_in_day);
        }
      //--- if this is the first calculation
      else
        {
         //--- create a new object of the CMarketProfile class to store the Market Profile of the day with 'day' index
         market_profile = new CMarketProfile(ExtPrefixUniq, start_time, stop_time, day_rate[0].high, day_rate[0].low, bars_in_day);
         //--- add a pointer to the created CMarketProfile object to the list
         mp_list.Add(market_profile);
        }
      //--- calculate profiles for each trading session
      market_profile.CalculateSessions();

      //--- set visibility and draw if enabled
      if(!IndicatorHidden)
         market_profile.SetVisible(true, InpMultiplier);
      else
         market_profile.SetVisible(false);
     }
//--- redraw the chart after the loop has been completed and all objects have been created and updated
   ChartRedraw(0);

//--- return the number of bars for the next OnCalculate call
   return(rates_total);
  }
//+------------------------------------------------------------------+
