preview
MQL5 Trading Tools (Part 30): Class-Based Tool Palette Sidebar

MQL5 Trading Tools (Part 30): Class-Based Tool Palette Sidebar

MetaTrader 5Trading |
313 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Introduction

You already have a drawing tools panel on an MetaTrader 5 chart, but it is built flat: rendering, layout, theme values, and handlers are mixed together, so every new feature becomes a cross-cutting change. Want to add a category, and you edit the main render loop. Want a light or dark theme, and you hunt through scattered color values. Want clean rounded corners, and you trade off visual quality. The panel works functionally, but it does not scale — the more tools, themes, and modes you add, the greater the risk that a small change breaks layout, rendering, or behavior.

This article is for MetaQuotes Language 5 (MQL5) developers and algorithmic traders who want a practical path out of that mess. We refactor the panel into a layered, single-responsibility class architecture so that extending the palette becomes "register a definition" instead of "rewrite the renderer". You will get a clear statement of the problem, an implementation that compiles and runs on the chart, and a repeatable pattern for adding categories, themes, and visual improvements without spreading changes across the codebase.

In our previous article (Part 29), we animated a butterfly on the chart with progressive drawing, wing fading, and flight motion. In Part 30, we revisit the Tools Palette we originally built in Part 19 and rebuild it from the ground up using an object-oriented, class-layered sidebar architecture with supersampled canvas rendering, theme management, and a category registry. We will cover the following topics:

  1. From Flat Panel to Layered Sidebar
  2. Implementation in MQL5
  3. Backtesting
  4. Conclusion

By the end, you will have a cleanly structured MQL5 sidebar with anti-aliased rounded corners, dual-theme support, and a modular category system ready for tool selection, fly-out panels, and drag interaction in future parts.


From Flat Panel to Layered Sidebar

The original Tools Palette built in Part 19 placed all logic in standalone functions and global variables. The header, tool buttons, drawing handlers, and theme colors lived at the same level with no separation of concerns. This worked for a small set of eight tools, but scaling further meant that modifying one area often required adjusting several others, making the codebase increasingly difficult to maintain and extend.

The redesign replaces the flat structure with a vertical sidebar built on a layered class hierarchy. Each class has a single responsibility. For example: primitives handle anti-aliased drawing, the theme manager controls color sets, the registry stores categories, the canvas layer manages resizing, the layout class computes geometry, and the renderer composes the final sidebar. Adding a new tool category becomes a matter of registering a definition rather than reworking the entire rendering loop.

On the chart, this compact vertical sidebar gives you quick access to drawing categories during live sessions without obstructing price action. Snap alignment pins it flush to a chart edge, and theme toggling instantly matches the palette to your chart background. As you add custom tool groups for your own analysis, the sidebar grows cleanly without layout conflicts. We will define icon mappings and enumerations, declare input parameters, and construct the class hierarchy. The top-level sidebar shell will initialize, render, and forward events. In a nutshell, here is a visualization of what we will be creating.

SNAP TOOLS PALETTE ARCHITECTURE


Implementation in MQL5

Including the Canvas Library, Defining Icons, Enumerations, Inputs, and Structures

To begin the implementation, we include the canvas library and establish the foundational definitions that the entire sidebar architecture depends on, from icon mappings and enumerations to user inputs and data structures.

//+------------------------------------------------------------------+
//|                                         Tools Palette Part 2.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property strict

//--- Include canvas drawing library
#include <Canvas/Canvas.mqh>

//+------------------------------------------------------------------+
//| Icon definitions — category sidebar buttons only                 |
//+------------------------------------------------------------------+
struct SIconDefinition { string fontName; uchar charCode; }; // Store icon font and character code

//--- Define icon for each tool category using font/char pairs
SIconDefinition ICON_CATEGORY_CURSORS     = { "Wingdings",   (uchar)'v' }; // Cursors category icon
SIconDefinition ICON_CATEGORY_LINES       = { "Wingdings 3", (uchar)'&' }; // Lines category icon
SIconDefinition ICON_CATEGORY_CHANNELS    = { "Wingdings 3", (uchar)'2' }; // Channels category icon
SIconDefinition ICON_CATEGORY_PITCHFORK   = { "Wingdings 3", (uchar)'H' }; // Pitchfork category icon
SIconDefinition ICON_CATEGORY_GANN        = { "Wingdings",   (uchar)'T' }; // Gann category icon
SIconDefinition ICON_CATEGORY_FIBONACCI   = { "Wingdings",   (uchar)'z' }; // Fibonacci category icon
SIconDefinition ICON_CATEGORY_SHAPES      = { "Wingdings",   (uchar)'o' }; // Shapes category icon
SIconDefinition ICON_CATEGORY_ANNOTATIONS = { "Webdings",    (uchar)'>' }; // Annotations category icon

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ENUM_SNAP_STATE
  {
   SNAP_LEFT,  // Snap sidebar to left edge
   SNAP_RIGHT, // Snap sidebar to right edge
   SNAP_FLOAT  // Allow sidebar to float freely
  };

enum ENUM_CATEGORY
  {
   CAT_CURSORS     = 0, // Cursors category index
   CAT_LINES,           // Lines category index
   CAT_CHANNELS,        // Channels category index
   CAT_PITCHFORK,       // Pitchfork category index
   CAT_GANN,            // Gann category index
   CAT_FIBONACCI,       // Fibonacci category index
   CAT_SHAPES,          // Shapes category index
   CAT_ANNOTATIONS,     // Annotations category index
   CAT_COUNT            // Total number of categories
  };

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input int    CanvasY           = 50;   // Canvas Y Position
input double BackgroundOpacity = 0.92; // Background Opacity (0.0 - 1.0)
input bool   StartDark         = true; // Start In Dark Theme
input int    BorderWidth       = 1;    // Border Width (px)
input int    CategoryIconSize  = 26;   // Category Icon Size (pt)
input int    SnapThreshold     = 40;   // Edge Snap Threshold (px)

//+------------------------------------------------------------------+
//| Category definition structure                                    |
//+------------------------------------------------------------------+
struct CategoryDefinition
  {
   string categoryLabel;    // Display label for the category
   string iconFontName;     // Font name used to render the icon
   uchar  iconCharCode;     // Character code of the icon glyph
   bool   hasMultipleTools; // Flag indicating category has sub-tools
  };

//+------------------------------------------------------------------+
//| Theme color set structure                                        |
//+------------------------------------------------------------------+
struct ThemeColorSet
  {
   color sidebarBackground; // Background fill color of the sidebar panel
   color sidebarBorder;     // Outline border color of the sidebar panel
   color buttonIconColor;   // Color used to render category icons
   color gripDotsColor;     // Color of the drag-grip dot indicators
   color separatorColor;    // Color of the horizontal separator lines
  };

We start by including the "Canvas/Canvas.mqh" header file, which provides the CCanvas class used throughout the program for bitmap drawing, text rendering, and pixel manipulation on chart objects. Next, we define the "SIconDefinition" structure to pair a font name with a character code, giving each tool category a visual icon. We then declare eight global instances of this structure, one for each category, such as cursors, lines, channels, pitchfork, Gann, Fibonacci, shapes, and annotations, each mapped to a specific glyph from the Wingdings, Wingdings 3, or Webdings font families. You can use any font or characters you like. See the list we are using below.

SYMBOL FONTS

We then declare two enumerations. The "ENUM_SNAP_STATE" enumeration defines three snap modes controlling whether the sidebar pins to the left edge, right edge, or floats freely on the chart. The "ENUM_CATEGORY" enumeration assigns an index to each of the eight tool categories and includes a terminal "CAT_COUNT" entry that automatically holds the total number of categories, making loops and array sizing straightforward.

Following the enumerations, we declare the input parameters that allow the user to customize the sidebar at load time. These include the vertical position, background opacity, starting theme, border thickness, icon size, and the pixel threshold that determines how close the panel must be to a chart edge before it snaps into place.

Finally, we define two structures. The "CategoryDefinition" structure holds everything needed to describe a tool category: its display label, icon font, character code, and a flag indicating whether it contains multiple sub-tools. The "ThemeColorSet" structure groups the five colors that define a visual theme, covering the sidebar background, border, icon color, grip dot color, and separator line color. Together, these structures form the data backbone that the class hierarchy will populate and reference throughout the rendering pipeline. Next, we will declare the primitives class and define it.

Declaring the Canvas Primitives Class

The first class in our hierarchy serves as the low-level rendering engine, providing all the primitive drawing methods that the higher-level classes will rely on to produce anti-aliased, supersampled visuals.

//+------------------------------------------------------------------+
//| CLASS 1 — Blend and draw low-level canvas primitives             |
//+------------------------------------------------------------------+
class CCanvasPrimitives
  {
protected:
   //--- Blend a single pixel onto the canvas using alpha compositing
   void BlendPixelSet(CCanvas &canvas, int x, int y, uint sourceARGB);
   //--- Downsample a high-res canvas into a lower-res destination
   void DownsampleCanvas(CCanvas &dst, CCanvas &src, int factor);
   //--- Fill a single corner quadrant of a rounded rectangle at high res
   void FillCornerQuadrantHR(CCanvas &canvas, int cx, int cy, int radius, uint argb, int signX, int signY);
   //--- Fill a full rounded rectangle at high resolution
   void FillRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb);
   //--- Fill a rounded rectangle with per-corner rounding control at high res
   void FillSelectiveRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb,
                                  bool rTL, bool rTR, bool rBL, bool rBR);
   //--- Fill a quadrilateral shape using scanline rasterization
   void FillQuadrilateralBorder(CCanvas &canvas, double &vx[], double &vy[], uint argb);
   //--- Draw a single thick border edge between two points
   void DrawBorderEdge(CCanvas &canvas, double x0, double y0, double x1, double y1, int thickness, uint argb);
   //--- Check whether an angle falls within a given arc range
   bool IsAngleBetween(double angle, double startAngle, double endAngle);
   //--- Draw a corner arc segment with specified thickness and angle range
   void DrawCornerArc(CCanvas &canvas, int cx, int cy, int radius, int thickness, uint argb, double startAngle, double endAngle);
   //--- Draw a rounded rectangle border with per-corner rounding control at high res
   void DrawSelectiveRoundRectBorderHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb, int thickness,
                                        bool rTL, bool rTR, bool rBL, bool rBR);
  };

We declare the "CCanvasPrimitives" class with all its methods under the protected access level, meaning they are available to derived classes but not accessible from outside the hierarchy. The class groups ten core rendering methods that cover the full range of drawing operations the sidebar needs.

The "BlendPixelSet" method handles alpha compositing of a single pixel onto the canvas, blending a source color over an existing destination pixel. The "DownsampleCanvas" method averages pixel blocks to convert the high-resolution canvas into the final size. This is the key step that produces smooth edges. The "FillCornerQuadrantHR" method fills one quadrant of a rounded corner using sub-pixel sampling for anti-aliased curves, while "FillRoundRectHR" combines four of these quadrants with rectangular strips to produce a fully rounded rectangle at high resolution. Building on that, "FillSelectiveRoundRectHR" adds per-corner control so we can selectively round only specific corners, which is essential when the sidebar snaps flush against a chart edge.

For border rendering, the "FillQuadrilateralBorder" method rasterizes an arbitrary four-sided shape using scanline filling, and "DrawBorderEdge" uses it to draw a thick line segment between two points by constructing a perpendicular quad. The "IsAngleBetween" method is a utility that checks whether an angle falls within an arc range, supporting the "DrawCornerArc" method, which renders anti-aliased arc segments at specified thickness for rounded border corners. Finally, "DrawSelectiveRoundRectBorderHR" ties these border methods together to draw a complete rounded rectangle outline with per-corner rounding control. Together, these primitives form the rendering foundation that every visual element in the sidebar is built upon. We will now define these methods using the scope resolution operator. We use double colons (::) to access the defined members. Here is an illustration example:

SCOPE RESOLUTION OPERATOR ILLUSTRATION

Implementing the Canvas Primitives Methods

These methods form the rendering core of the sidebar, handling pixel blending, supersampled and downscaling, anti-aliased rounded rectangle fills, scanline rasterization, and arc-based border drawing.

//+------------------------------------------------------------------+
//| Blend source pixel onto canvas using alpha compositing           |
//+------------------------------------------------------------------+
void CCanvasPrimitives::BlendPixelSet(CCanvas &canvas, int x, int y, uint src)
  {
   //--- Skip pixels outside canvas bounds
   if (x < 0 || x >= canvas.Width() || y < 0 || y >= canvas.Height()) return;
   //--- Read existing destination pixel
   uint dst = canvas.PixelGet(x, y);
   //--- Unpack source ARGB channels to normalized floats
   double sA = ((src >> 24) & 0xFF) / 255.0, sR = ((src >> 16) & 0xFF) / 255.0;
   double sG = ((src >>  8) & 0xFF) / 255.0, sB = ( src        & 0xFF) / 255.0;
   //--- Unpack destination ARGB channels to normalized floats
   double dA = ((dst >> 24) & 0xFF) / 255.0, dR = ((dst >> 16) & 0xFF) / 255.0;
   double dG = ((dst >>  8) & 0xFF) / 255.0, dB = ( dst        & 0xFF) / 255.0;
   //--- Compute output alpha using standard over-compositing formula
   double oA = sA + dA * (1.0 - sA);
   //--- Write fully transparent pixel and exit if output alpha is zero
   if (oA == 0.0) { canvas.PixelSet(x, y, 0); return; }
   //--- Write blended ARGB pixel to canvas
   canvas.PixelSet(x, y,
      ((uint)(uchar)(oA * 255 + 0.5) << 24) |
      ((uint)(uchar)((sR * sA + dR * dA * (1.0 - sA)) / oA * 255 + 0.5) << 16) |
      ((uint)(uchar)((sG * sA + dG * dA * (1.0 - sA)) / oA * 255 + 0.5) <<  8) |
       (uint)(uchar)((sB * sA + dB * dA * (1.0 - sA)) / oA * 255 + 0.5));
  }

//+------------------------------------------------------------------+
//| Downsample high-res canvas into destination by averaging pixels  |
//+------------------------------------------------------------------+
void CCanvasPrimitives::DownsampleCanvas(CCanvas &dst, CCanvas &src, int factor)
  {
   //--- Cache destination dimensions and squared sample count
   int dW = dst.Width(), dH = dst.Height(), ss2 = factor * factor;
   //--- Iterate over every destination pixel
   for (int py = 0; py < dH; py++)
      for (int px = 0; px < dW; px++)
        {
         //--- Accumulate channel sums across the source sample block
         double sA = 0, sR = 0, sG = 0, sB = 0, wc = 0;
         for (int dy = 0; dy < factor; dy++)
            for (int dx = 0; dx < factor; dx++)
              {
               //--- Compute source sample coordinates
               int sx = px * factor + dx, sy = py * factor + dy;
               //--- Skip samples outside source bounds
               if (sx >= src.Width() || sy >= src.Height()) continue;
               //--- Read source pixel and extract alpha
               uint p = src.PixelGet(sx, sy); uchar a = (uchar)((p >> 24) & 0xFF);
               //--- Accumulate alpha unconditionally
               sA += a;
               //--- Accumulate color channels only for non-transparent samples
               if (a > 0) { sR += (p >> 16) & 0xFF; sG += (p >> 8) & 0xFF; sB += p & 0xFF; wc += 1.0; }
              }
         //--- Compute averaged output alpha
         uchar fa = (uchar)(sA / ss2);
         //--- Write transparent pixel and skip if result is fully transparent
         if (fa == 0 || wc == 0) { dst.PixelSet(px, py, 0); continue; }
         //--- Write averaged ARGB pixel to destination canvas
         dst.PixelSet(px, py, ((uint)fa << 24) | ((uint)(uchar)(sR / wc) << 16) |
                               ((uint)(uchar)(sG / wc) << 8) | (uint)(uchar)(sB / wc));
        }
  }

//+------------------------------------------------------------------+
//| Fill one corner quadrant of a rounded rect at high resolution    |
//+------------------------------------------------------------------+
void CCanvasPrimitives::FillCornerQuadrantHR(CCanvas &canvas, int cx, int cy, int radius, uint argb, int signX, int signY)
  {
   //--- Cache radius as double and extract alpha and RGB components
   double rd = (double)radius;
   uchar  bA = (uchar)((argb >> 24) & 0xFF);
   uint   rgb = argb & 0x00FFFFFF;
   //--- Set sub-pixel sample count and derived values
   int sub = 4; double step = 1.0 / sub; int subSq = sub * sub;
   //--- Iterate over pixel neighbourhood around corner center
   for (int dy = -(radius + 1); dy <= (radius + 1); dy++)
      for (int dx = -(radius + 1); dx <= (radius + 1); dx++)
        {
         //--- Check pixel belongs to the target quadrant
         bool inQ = ((signX > 0) ? (dx >= 0) : (dx <= 0)) && ((signY > 0) ? (dy >= 0) : (dy <= 0));
         if (!inQ) continue;
         //--- Compute distance from corner center
         double dist = MathSqrt((double)(dx * dx + dy * dy));
         //--- Skip pixels too far outside radius
         if (dist > rd + 1.0) continue;
         //--- Fill pixels fully inside radius without anti-aliasing
         if (dist <= rd - 1.0) { canvas.PixelSet(cx + dx, cy + dy, argb); continue; }
         //--- Count sub-pixel samples falling inside the circle
         int inside = 0;
         for (int sy = 0; sy < sub; sy++)
            for (int sx = 0; sx < sub; sx++)
              {
               //--- Compute sub-pixel offsets from pixel center
               double sdx = (double)dx - 0.5 + (sx + 0.5) * step;
               double sdy = (double)dy - 0.5 + (sy + 0.5) * step;
               if (sdx * sdx + sdy * sdy <= rd * rd) inside++;
              }
         //--- Skip pixel if no sub-samples are inside
         if (inside == 0) continue;
         //--- Blend anti-aliased pixel using coverage fraction
         BlendPixelSet(canvas, cx + dx, cy + dy, (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb);
        }
  }

//+------------------------------------------------------------------+
//| Fill a fully rounded rectangle at high resolution                |
//+------------------------------------------------------------------+
void CCanvasPrimitives::FillRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb)
  {
   //--- Clamp radius to half the smallest dimension
   radius = MathMin(radius, MathMin(w / 2, h / 2));
   //--- Fall back to plain rectangle when radius is zero or negative
   if (radius <= 0) { canvas.FillRectangle(x, y, x + w - 1, y + h - 1, argb); return; }
   //--- Fill horizontal center strip
   canvas.FillRectangle(x + radius, y, x + w - radius - 1, y + h - 1, argb);
   //--- Fill left vertical strip
   canvas.FillRectangle(x, y + radius, x + radius - 1, y + h - radius - 1, argb);
   //--- Fill right vertical strip
   canvas.FillRectangle(x + w - radius, y + radius, x + w - 1, y + h - radius - 1, argb);
   //--- Fill top-left corner quadrant
   FillCornerQuadrantHR(canvas, x + radius,     y + radius,     radius, argb, -1, -1);
   //--- Fill top-right corner quadrant
   FillCornerQuadrantHR(canvas, x + w - radius, y + radius,     radius, argb,  1, -1);
   //--- Fill bottom-left corner quadrant
   FillCornerQuadrantHR(canvas, x + radius,     y + h - radius, radius, argb, -1,  1);
   //--- Fill bottom-right corner quadrant
   FillCornerQuadrantHR(canvas, x + w - radius, y + h - radius, radius, argb,  1,  1);
  }

//+------------------------------------------------------------------+
//| Fill rounded rectangle with per-corner rounding at high res      |
//+------------------------------------------------------------------+
void CCanvasPrimitives::FillSelectiveRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb,
                                                  bool rTL, bool rTR, bool rBL, bool rBR)
  {
   //--- Clamp radius to half the smallest dimension
   radius = MathMin(radius, MathMin(w / 2, h / 2));
   //--- Fall back to plain rectangle when radius is zero or negative
   if (radius <= 0) { canvas.FillRectangle(x, y, x + w - 1, y + h - 1, argb); return; }
   //--- Fill horizontal center strip spanning full width
   canvas.FillRectangle(x + radius, y, x + w - radius - 1, y + h - 1, argb);
   //--- Fill left strip, respecting top-left and bottom-left rounding
   canvas.FillRectangle(x,          y + (rTL ? radius : 0), x + radius - 1,  y + h - 1 - (rBL ? radius : 0), argb);
   //--- Fill right strip, respecting top-right and bottom-right rounding
   canvas.FillRectangle(x + w - radius, y + (rTR ? radius : 0), x + w - 1,   y + h - 1 - (rBR ? radius : 0), argb);
   //--- Fill or square top-left corner based on rounding flag
   if (rTL) FillCornerQuadrantHR(canvas, x + radius,     y + radius,     radius, argb, -1, -1);
   else     canvas.FillRectangle(x, y, x + radius - 1, y + radius - 1, argb);
   //--- Fill or square top-right corner based on rounding flag
   if (rTR) FillCornerQuadrantHR(canvas, x + w - radius, y + radius,     radius, argb,  1, -1);
   else     canvas.FillRectangle(x + w - radius, y, x + w - 1, y + radius - 1, argb);
   //--- Fill or square bottom-left corner based on rounding flag
   if (rBL) FillCornerQuadrantHR(canvas, x + radius,     y + h - radius, radius, argb, -1,  1);
   else     canvas.FillRectangle(x, y + h - radius, x + radius - 1, y + h - 1, argb);
   //--- Fill or square bottom-right corner based on rounding flag
   if (rBR) FillCornerQuadrantHR(canvas, x + w - radius, y + h - radius, radius, argb,  1,  1);
   else     canvas.FillRectangle(x + w - radius, y + h - radius, x + w - 1, y + h - 1, argb);
  }

//+------------------------------------------------------------------+
//| Fill a quadrilateral using scanline rasterization                |
//+------------------------------------------------------------------+
void CCanvasPrimitives::FillQuadrilateralBorder(CCanvas &canvas, double &vx[], double &vy[], uint argb)
  {
   //--- Find vertical bounding extent of the quad
   double minY = vy[0], maxY = vy[0];
   for (int i = 1; i < 4; i++) { if (vy[i] < minY) minY = vy[i]; if (vy[i] > maxY) maxY = vy[i]; }
   //--- Iterate over each horizontal scanline within the bounding box
   for (int scanY = (int)MathCeil(minY); scanY <= (int)MathCeil(maxY) - 1; scanY++)
     {
      //--- Compute scanline center Y and prepare intersection buffer
      double cy = (double)scanY + 0.5; double xi[8]; int nc = 0;
      //--- Compute X intersections with each edge of the quad
      for (int i = 0; i < 4; i++)
        {
         int ni = (i + 1) % 4;
         //--- Determine edge vertical extents
         double eMin = (vy[i] < vy[ni]) ? vy[i] : vy[ni], eMax = (vy[i] > vy[ni]) ? vy[i] : vy[ni];
         //--- Skip edges that do not cross the current scanline
         if (cy < eMin || cy > eMax || MathAbs(vy[ni] - vy[i]) < 1e-12) continue;
         //--- Compute intersection parameter along the edge
         double t = (cy - vy[i]) / (vy[ni] - vy[i]);
         if (t < 0.0 || t > 1.0) continue;
         //--- Record intersection X coordinate
         xi[nc++] = vx[i] + t * (vx[ni] - vx[i]);
        }
      //--- Sort intersections left to right
      for (int a = 0; a < nc - 1; a++)
         for (int b = a + 1; b < nc; b++)
            if (xi[a] > xi[b]) { double tmp = xi[a]; xi[a] = xi[b]; xi[b] = tmp; }
      //--- Fill pixels between paired intersection spans
      for (int p = 0; p + 1 < nc; p += 2)
         for (int fx = (int)MathCeil(xi[p]); fx <= (int)MathCeil(xi[p + 1]) - 1; fx++)
            canvas.PixelSet(fx, scanY, argb);
     }
  }

//+------------------------------------------------------------------+
//| Draw a thick border edge between two endpoints                   |
//+------------------------------------------------------------------+
void CCanvasPrimitives::DrawBorderEdge(CCanvas &canvas, double x0, double y0, double x1, double y1, int thickness, uint argb)
  {
   //--- Compute edge direction vector and length
   double dx = x1 - x0, dy = y1 - y0, len = MathSqrt(dx * dx + dy * dy);
   //--- Skip degenerate edges with near-zero length
   if (len < 1e-6) return;
   //--- Compute perpendicular and unit direction vectors
   double px = -dy / len, py = dx / len, ex = dx / len, ey = dy / len;
   //--- Compute half-thickness and end cap extension
   double ht = thickness / 2.0, ext = 0.23 * thickness;
   //--- Extend start and end points slightly for mitre cap effect
   double sx = x0 - ex * ext, sy = y0 - ey * ext, ex2 = x1 + ex * ext, ey2 = y1 + ey * ext;
   //--- Build quad vertices offset perpendicular to edge direction
   double tvx[4] = { sx - px*ht, sx + px*ht, ex2 + px*ht, ex2 - px*ht };
   double tvy[4] = { sy - py*ht, sy + py*ht, ey2 + py*ht, ey2 - py*ht };
   //--- Fill the resulting quad as the border edge shape
   FillQuadrilateralBorder(canvas, tvx, tvy, argb);
  }

//+------------------------------------------------------------------+
//| Check whether an angle falls within a start-to-end arc range     |
//+------------------------------------------------------------------+
bool CCanvasPrimitives::IsAngleBetween(double angle, double start, double end)
  {
   //--- Normalize all angles to the [0, 2π) range
   double tp = 2.0 * M_PI;
   angle = MathMod(angle + tp, tp); start = MathMod(start + tp, tp); end = MathMod(end + tp, tp);
   //--- Return true if angle lies within the arc from start to end
   return MathMod(angle - start + tp, tp) <= MathMod(end - start + tp, tp);
  }

//+------------------------------------------------------------------+
//| Draw an anti-aliased corner arc with specified thickness         |
//+------------------------------------------------------------------+
void CCanvasPrimitives::DrawCornerArc(CCanvas &canvas, int cx, int cy, int radius, int thickness, uint argb, double startAngle, double endAngle)
  {
   //--- Compute outer and inner radii for the arc ring
   double oR = (double)radius, iR = MathMax(0.0, (double)radius - thickness);
   //--- Extract alpha and RGB components from packed color
   uchar  bA = (uchar)((argb >> 24) & 0xFF); uint rgb = argb & 0x00FFFFFF;
   //--- Set sub-pixel sample count and pixel scan radius
   int sub = 4; double step = 1.0 / sub; int subSq = sub * sub, pr = (int)(oR + 2.0);
   //--- Iterate over pixels in the bounding box of the arc
   for (int dy = -pr; dy <= pr; dy++)
      for (int dx = -pr; dx <= pr; dx++)
        {
         //--- Compute distance from arc center
         double dist = MathSqrt((double)(dx * dx + dy * dy));
         //--- Skip pixels clearly outside the ring or wrong angle
         if (dist > oR + 1.0 || dist < iR - 1.0) continue;
         if (!IsAngleBetween(MathArctan2((double)dy, (double)dx), startAngle, endAngle)) continue;
         //--- Fill pixels fully inside the ring without anti-aliasing
         if (dist <= oR - 1.0 && dist >= iR + 1.0) { canvas.PixelSet(cx + dx, cy + dy, argb); continue; }
         //--- Count sub-pixel samples inside the arc ring and angle range
         int inside = 0;
         for (int sy = 0; sy < sub; sy++)
            for (int sx = 0; sx < sub; sx++)
              {
               double sdx = (double)dx - 0.5 + (sx + 0.5) * step, sdy = (double)dy - 0.5 + (sy + 0.5) * step;
               double sd = MathSqrt(sdx * sdx + sdy * sdy);
               if (sd >= iR && sd <= oR && IsAngleBetween(MathArctan2(sdy, sdx), startAngle, endAngle)) inside++;
              }
         //--- Skip pixel if no sub-samples qualify
         if (inside == 0) continue;
         //--- Write fully opaque pixel if all sub-samples qualify
         if (inside >= subSq) canvas.PixelSet(cx + dx, cy + dy, argb);
         //--- Blend anti-aliased pixel using coverage fraction
         else BlendPixelSet(canvas, cx + dx, cy + dy, (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb);
        }
  }

//+------------------------------------------------------------------+
//| Draw rounded rect border with per-corner rounding at high res    |
//+------------------------------------------------------------------+
void CCanvasPrimitives::DrawSelectiveRoundRectBorderHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb, int thickness,
                                                        bool rTL, bool rTR, bool rBL, bool rBR)
  {
   //--- Skip drawing when border width is disabled
   if (BorderWidth <= 0) return;
   //--- Clamp radius to half the smallest dimension
   radius = MathMin(radius, MathMin(w / 2, h / 2));
   //--- Compute per-corner radii based on rounding flags and half-thickness offset
   int tlR = rTL ? radius : 0, trR = rTR ? radius : 0, blR = rBL ? radius : 0, brR = rBR ? radius : 0, h2 = thickness / 2;
   //--- Draw top edge
   DrawBorderEdge(canvas, x + tlR,     y + h2,     x + w - trR, y + h2,     thickness, argb);
   //--- Draw right edge if any right corner is rounded
   if (rTR || rBR) DrawBorderEdge(canvas, x + w - h2, y + trR, x + w - h2, y + h - brR, thickness, argb);
   //--- Draw bottom edge
   DrawBorderEdge(canvas, x + w - brR, y + h - h2, x + blR,     y + h - h2, thickness, argb);
   //--- Draw left edge if any left corner is rounded
   if (rTL || rBL) DrawBorderEdge(canvas, x + h2, y + h - blR, x + h2, y + tlR, thickness, argb);
   //--- Draw top-left corner arc if rounded
   if (rTL) DrawCornerArc(canvas, x + radius,     y + radius,     radius, thickness, argb, M_PI,       M_PI * 1.5);
   //--- Draw top-right corner arc if rounded
   if (rTR) DrawCornerArc(canvas, x + w - radius, y + radius,     radius, thickness, argb, M_PI * 1.5, M_PI * 2.0);
   //--- Draw bottom-left corner arc if rounded
   if (rBL) DrawCornerArc(canvas, x + radius,     y + h - radius, radius, thickness, argb, M_PI * 0.5, M_PI);
   //--- Draw bottom-right corner arc if rounded
   if (rBR) DrawCornerArc(canvas, x + w - radius, y + h - radius, radius, thickness, argb, 0.0,        M_PI * 0.5);
  }

We begin with the "BlendPixelSet" method, which performs alpha compositing on a single pixel. It bounds-checks the coordinates, unpacks both the source and destination pixels into normalized float channels, applies the standard over-compositing formula, and writes the blended result back using the PixelSet method. Every anti-aliased edge in the sidebar relies on this method to merge partially transparent pixels smoothly.

The "DownsampleCanvas" method handles the second half of the supersampling pipeline. For each destination pixel, it averages a square block of samples from the high-resolution source canvas, accumulating alpha unconditionally while only counting color channels from non-transparent pixels. This averaging converts the oversized rendering into a smooth, anti-aliased result at display resolution.

Next, "FillCornerQuadrantHR" fills a single quadrant of a rounded corner by scanning pixels around the corner center, filtering by quadrant using sign parameters, and applying four-by-four sub-pixel sampling along the curved edge to determine coverage. The coverage fraction scales the alpha, producing smooth curves without jagged steps.

The "FillRoundRectHR" method assembles a complete rounded rectangle from three rectangular strips and four corner quadrants, while "FillSelectiveRoundRectHR" extends it with four boolean flags controlling whether each corner is rounded or squared off. This selective rounding is how the sidebar achieves flush edges when snapped against the chart boundary.

For border rendering, "FillQuadrilateralBorder" rasterizes a four-sided polygon using scanline sweeping with sorted edge intersections, and "DrawBorderEdge" builds a perpendicular quad from two endpoints to render a thick line segment through it. The "IsAngleBetween" utility supports "DrawCornerArc", which renders thick arc segments using an inner and outer radius ring with sub-pixel sampling for anti-aliased curved borders. Finally, "DrawSelectiveRoundRectBorderHR" ties everything together, drawing four straight edges and four conditional corner arcs to produce a complete rounded border respecting per-corner rounding flags. With that done, we can define a second class to manage the theme modes of the tool.

Declaring and Implementing the Theme Manager Class

The second class in the hierarchy centralizes all theme-related state and color assignments into a single manageable layer, inheriting the rendering capabilities from the primitives class below it.

//+------------------------------------------------------------------+
//| CLASS 2 — Manage and apply light and dark theme color sets       |
//+------------------------------------------------------------------+
class CThemeManager : public CCanvasPrimitives
  {
protected:
   bool          m_isDarkTheme;   // Active theme flag: true = dark, false = light
   ThemeColorSet m_themeColors;   // Active color set for the current theme
protected:
   //--- Apply color values matching the current theme state
   void ApplyTheme();
  };

We declare the "CThemeManager" class, which inherits from "CCanvasPrimitives" and introduces two protected members: a boolean flag tracking whether the dark theme is active, and a "ThemeColorSet" instance holding the current color values. It also declares a single protected method, "ApplyTheme", responsible for populating the color set based on the active theme state. The method is implemented as follows:

//+------------------------------------------------------------------+
//| Apply color values matching the current theme state              |
//+------------------------------------------------------------------+
void CThemeManager::ApplyTheme()
  {
   //--- Apply dark theme color assignments
   if (m_isDarkTheme)
     {
      m_themeColors.sidebarBackground = C'30,34,45';  // Dark navy background
      m_themeColors.sidebarBorder     = C'200,210,225'; // Light blue-gray border
      m_themeColors.buttonIconColor   = C'220,225,235'; // Near-white icon color
      m_themeColors.gripDotsColor     = C'90,100,120';  // Muted slate grip dots
      m_themeColors.separatorColor    = C'44,50,64';    // Dark separator line
     }
   else
     {
      //--- Apply light theme color assignments
      m_themeColors.sidebarBackground = clrWhite;        // White background
      m_themeColors.sidebarBorder     = C'30,35,45';     // Dark border
      m_themeColors.buttonIconColor   = C'40,45,58';     // Dark icon color
      m_themeColors.gripDotsColor     = C'160,170,185';  // Light gray grip dots
      m_themeColors.separatorColor    = C'210,215,225';  // Light separator line
     }
  }

We implement the "ApplyTheme" method, which checks the dark theme flag and assigns five color values to the theme color set. When dark mode is active, the sidebar receives a dark navy background, light blue-gray border, near-white icons, muted slate grip dots, and a dark separator line. When light mode is active, the colors switch to a white background with darker tones for the border, icons, grip dots, and separators. Any class further up the hierarchy simply reads from this color set when rendering, so toggling the theme is just a matter of flipping the flag and calling this method again. To make the tools usable, we will need to register them. Let's define a class for that too.

Declaring the Category Registry Class

The third class in the hierarchy manages the registration of all tool categories, giving the sidebar a structured way to store and access each category's definition.

//+------------------------------------------------------------------+
//| CLASS 3 — Register and initialise all tool category definitions  |
//+------------------------------------------------------------------+
class CCategoryRegistry : public CThemeManager
  {
protected:
   CategoryDefinition m_categories[CAT_COUNT]; // Array of all category definitions
protected:
   //--- Populate all category definitions with labels, icons, and tool flags
   void InitAllCategories();
  };

We declare the "CCategoryRegistry" class, which inherits from "CThemeManager" and introduces a protected array of "CategoryDefinition" structures sized by "CAT_COUNT" to hold all category entries. We also declare the "InitAllCategories" method, which will be responsible for populating each entry in the array with its label, icon font, character code, and multi-tool flag. Implementation of this method is as follows:

//+------------------------------------------------------------------+
//| Populate all category definitions with labels, icons, flags      |
//+------------------------------------------------------------------+
void CCategoryRegistry::InitAllCategories()
  {
   //--- Assign Cursors category definition
   m_categories[CAT_CURSORS].categoryLabel     = "Cursors";
   m_categories[CAT_CURSORS].iconFontName      = ICON_CATEGORY_CURSORS.fontName;
   m_categories[CAT_CURSORS].iconCharCode      = ICON_CATEGORY_CURSORS.charCode;
   m_categories[CAT_CURSORS].hasMultipleTools  = false;
   //--- Assign Lines category definition
   m_categories[CAT_LINES].categoryLabel       = "Lines";
   m_categories[CAT_LINES].iconFontName        = ICON_CATEGORY_LINES.fontName;
   m_categories[CAT_LINES].iconCharCode        = ICON_CATEGORY_LINES.charCode;
   m_categories[CAT_LINES].hasMultipleTools    = true;
   //--- Assign Channels category definition
   m_categories[CAT_CHANNELS].categoryLabel    = "Channels";
   m_categories[CAT_CHANNELS].iconFontName     = ICON_CATEGORY_CHANNELS.fontName;
   m_categories[CAT_CHANNELS].iconCharCode     = ICON_CATEGORY_CHANNELS.charCode;
   m_categories[CAT_CHANNELS].hasMultipleTools = true;
   //--- Assign Pitchfork category definition
   m_categories[CAT_PITCHFORK].categoryLabel    = "Pitchfork";
   m_categories[CAT_PITCHFORK].iconFontName     = ICON_CATEGORY_PITCHFORK.fontName;
   m_categories[CAT_PITCHFORK].iconCharCode     = ICON_CATEGORY_PITCHFORK.charCode;
   m_categories[CAT_PITCHFORK].hasMultipleTools = true;
   //--- Assign Gann category definition
   m_categories[CAT_GANN].categoryLabel        = "Gann";
   m_categories[CAT_GANN].iconFontName         = ICON_CATEGORY_GANN.fontName;
   m_categories[CAT_GANN].iconCharCode         = ICON_CATEGORY_GANN.charCode;
   m_categories[CAT_GANN].hasMultipleTools     = true;
   //--- Assign Fibonacci category definition
   m_categories[CAT_FIBONACCI].categoryLabel    = "Fibonacci";
   m_categories[CAT_FIBONACCI].iconFontName     = ICON_CATEGORY_FIBONACCI.fontName;
   m_categories[CAT_FIBONACCI].iconCharCode     = ICON_CATEGORY_FIBONACCI.charCode;
   m_categories[CAT_FIBONACCI].hasMultipleTools = true;
   //--- Assign Shapes category definition
   m_categories[CAT_SHAPES].categoryLabel      = "Shapes";
   m_categories[CAT_SHAPES].iconFontName       = ICON_CATEGORY_SHAPES.fontName;
   m_categories[CAT_SHAPES].iconCharCode       = ICON_CATEGORY_SHAPES.charCode;
   m_categories[CAT_SHAPES].hasMultipleTools   = true;
   //--- Assign Annotations category definition
   m_categories[CAT_ANNOTATIONS].categoryLabel    = "Annotate";
   m_categories[CAT_ANNOTATIONS].iconFontName     = ICON_CATEGORY_ANNOTATIONS.fontName;
   m_categories[CAT_ANNOTATIONS].iconCharCode     = ICON_CATEGORY_ANNOTATIONS.charCode;
   m_categories[CAT_ANNOTATIONS].hasMultipleTools = true;
  }

Here, we implement the "InitAllCategories" method, where we populate each slot in the categories array with its corresponding definition. For every category, we assign a display label, pull the icon font name and character code from the global icon definitions we declared earlier, and set the multi-tool flag. The cursors category is the only one marked as single-tool since it does not expand into sub-tools, while all remaining categories, including lines, channels, pitchfork, Gann, Fibonacci, shapes, and annotations, are flagged as having multiple tools. This means that when we later render the sidebar, those categories will display a small indicator dot signaling that they contain additional tools the user can expand into. This indicator will be useful in the future when we are expanding the palette, but you can ignore it if you like. We can move on to the actual category registration now.

Declaring the Canvas Layer Class

The fourth class in the hierarchy owns the canvas objects and manages their lifecycle, providing the drawing surfaces that the renderer will paint onto.

//+------------------------------------------------------------------+
//| CLASS 4 — Create, destroy, and resize all canvas layers          |
//+------------------------------------------------------------------+
class CCanvasLayer : public CCategoryRegistry
  {
protected:
   int     m_supersampleFactor;       // Supersampling multiplier for high-res rendering
   long    m_chartId;                 // Chart identifier this layer belongs to
   CCanvas m_canvasSidebar;           // Final display-resolution sidebar canvas
   CCanvas m_canvasSidebarHighRes;    // High-resolution sidebar canvas for supersampling
   string  m_nameSidebar;             // Object name of the sidebar bitmap label

protected:
   //--- Create all canvas objects at the given dimensions
   bool CreateAllCanvases(int w, int h);
   //--- Destroy all canvas objects and remove chart objects
   void DestroyAllCanvases();
   //--- Resize both sidebar canvases to the given dimensions
   void ResizeSidebarCanvases(int w, int h);
  };

We declare the "CCanvasLayer" class, which inherits from "CCategoryRegistry" and introduces the protected members needed for canvas management. These include the supersampling multiplier that controls the high-resolution scaling factor, the chart identifier, two CCanvas instances for the display-resolution sidebar and its high-resolution counterpart used during supersampled rendering, and a string holding the bitmap label object name. We also declare three protected methods: "CreateAllCanvases" for building both canvases at given dimensions, "DestroyAllCanvases" for cleaning up the canvases and removing their chart objects, and "ResizeSidebarCanvases" for adjusting both canvases when the panel dimensions change. The implementation of these methods is as follows:

//+------------------------------------------------------------------+
//| Create all canvas objects at the given dimensions                |
//+------------------------------------------------------------------+
bool CCanvasLayer::CreateAllCanvases(int w, int h)
  {
   //--- Create the display-resolution sidebar bitmap label canvas
   if (!m_canvasSidebar.CreateBitmapLabel(0, 0, m_nameSidebar, 0, 0, w, h, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("CCanvasLayer: Failed to create sidebar canvas"); return false; }
   //--- Create the high-resolution sidebar canvas for supersampled drawing
   if (!m_canvasSidebarHighRes.Create("ToolsPalette_SidebarHR", w * m_supersampleFactor, h * m_supersampleFactor, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("CCanvasLayer: Failed to create sidebar HR canvas"); return false; }
   return true;
  }

//+------------------------------------------------------------------+
//| Destroy all canvas objects and remove chart objects              |
//+------------------------------------------------------------------+
void CCanvasLayer::DestroyAllCanvases()
  {
   //--- Destroy display canvas and remove its chart object
   m_canvasSidebar.Destroy();
   ObjectDelete(0, m_nameSidebar);
   //--- Destroy the high-resolution working canvas
   m_canvasSidebarHighRes.Destroy();
  }

//+------------------------------------------------------------------+
//| Resize both sidebar canvases to the given dimensions             |
//+------------------------------------------------------------------+
void CCanvasLayer::ResizeSidebarCanvases(int w, int h)
  {
   //--- Resize the display-resolution sidebar canvas
   m_canvasSidebar.Resize(w, h);
   //--- Update the chart object dimensions to match
   ObjectSetInteger(0, m_nameSidebar, OBJPROP_XSIZE, w);
   ObjectSetInteger(0, m_nameSidebar, OBJPROP_YSIZE, h);
   //--- Resize the high-resolution canvas scaled by the supersample factor
   m_canvasSidebarHighRes.Resize(w * m_supersampleFactor, h * m_supersampleFactor);
  }

We implement the "CreateAllCanvases" method, where we first create the display-resolution sidebar canvas as a bitmap label on the chart using "CreateBitmapLabel". If that fails, we print an error message and return false. We then create the high-resolution canvas using the Create method, scaling the dimensions by the supersample factor so it serves as the oversized working surface for anti-aliased rendering. If either creation fails, the method exits early to prevent the program from running with incomplete canvases.

The "DestroyAllCanvases" method handles cleanup by destroying the display canvas, removing its corresponding chart object with ObjectDelete, and then destroying the high-resolution canvas. This ensures no orphaned objects remain on the chart when the program is removed.

Finally, we implement "ResizeSidebarCanvases", where we resize the display canvas with Resize, update the chart object dimensions using ObjectSetInteger to keep them in sync, and resize the high-resolution canvas by the supersample factor. This allows the sidebar to adapt its drawing surfaces dynamically whenever the panel dimensions change. Next, we will need to manage the created palette, so we create a class for the layout and geometry management.

Declaring the Sidebar Layout Class

The fifth class in the hierarchy is responsible for computing and maintaining all the spatial dimensions and positioning that define how the sidebar appears on the chart.

//+------------------------------------------------------------------+
//| CLASS 5 — Compute and maintain sidebar layout and geometry       |
//+------------------------------------------------------------------+
class CSidebarLayout : public CCanvasLayer
  {
protected:
   int             m_panelX;                  // Horizontal position of the sidebar panel
   int             m_panelY;                  // Vertical position of the sidebar panel
   int             m_sidebarWidth;            // Width of the sidebar panel in pixels
   int             m_sidebarHeight;           // Height of the sidebar panel in pixels
   int             m_categoryButtonSize;      // Size of each category button in pixels
   int             m_categoryButtonPadding;   // Vertical gap between category buttons
   int             m_panelCornerRadius;       // Corner rounding radius of the panel
   int             m_headerGripHeight;        // Height of the top header and grip area
   ENUM_SNAP_STATE m_snapState;               // Current snap alignment state
   int             m_sidebarMaxVisibleCats;   // Maximum number of visible category buttons

protected:
   //--- Compute and set the sidebar panel height based on available chart space
   void CalcSidebarHeight();
   //--- Compute the Y pixel position of a category button by index
   int  CalcCategoryButtonY(int idx);
   //--- Compute the top clipping boundary for the category button area
   int  CalcClipTop();
   //--- Compute the bottom clipping boundary for the category button area
   int  CalcClipBottom();
  };

We declare the "CSidebarLayout" class, which inherits from "CCanvasLayer" and introduces the protected members that define the sidebar geometry. These include the panel position, sidebar width and height, category button size and vertical padding between them, the corner rounding radius, the header grip area height, the current snap alignment state, and a count of the maximum visible category buttons that fit within the available space. We also declare four protected methods: "CalcSidebarHeight" for computing the panel height based on chart space, "CalcCategoryButtonY" for determining the vertical position of a button by its index, and "CalcClipTop" and "CalcClipBottom" for defining the top and bottom clipping boundaries of the category button area. The implementation of these methods is as follows:

//+------------------------------------------------------------------+
//| Compute and set sidebar height based on available chart space    |
//+------------------------------------------------------------------+
void CSidebarLayout::CalcSidebarHeight()
  {
   //--- Get current chart height in pixels
   int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   //--- Define vertical padding constants
   int topPad = 8, botPad = 10;
   //--- Set button gap spacing
   m_categoryButtonPadding = 6;
   //--- Pin panel to fixed Y offset below chart top
   m_panelY = 30;
   ObjectSetInteger(0, m_nameSidebar, OBJPROP_YDISTANCE, m_panelY);
   //--- Compute maximum available height below panel top offset
   int availH   = chartH - m_panelY - 8;
   //--- Compute ideal natural height to fit all category buttons
   int naturalH = m_headerGripHeight + topPad + CAT_COUNT * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding + botPad;
   //--- Compute minimum height to show at least three category buttons
   int minH     = m_headerGripHeight + topPad + 3 * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding + botPad;
   //--- Clamp sidebar height between minimum and available space
   m_sidebarHeight = MathMax(minH, MathMin(naturalH, availH));
   //--- Compute usable height for the button area
   int btnAreaH = m_sidebarHeight - m_headerGripHeight - topPad - botPad;
   //--- Compute total height needed for all buttons at natural spacing
   int fullBtnH = CAT_COUNT * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding;
   //--- Compute how many buttons fit within the available button area
   m_sidebarMaxVisibleCats = (fullBtnH <= btnAreaH) ? CAT_COUNT
      : MathMax(3, MathMin(CAT_COUNT, btnAreaH / (m_categoryButtonSize + m_categoryButtonPadding)));
  }

//+------------------------------------------------------------------+
//| Compute Y pixel position of a category button by index           |
//+------------------------------------------------------------------+
int CSidebarLayout::CalcCategoryButtonY(int idx)
  {
   //--- Return Y offset below the header grip area
   return m_headerGripHeight + 8 + idx * (m_categoryButtonSize + m_categoryButtonPadding);
  }

//+------------------------------------------------------------------+
//| Compute top clip boundary for the category button area           |
//+------------------------------------------------------------------+
int CSidebarLayout::CalcClipTop()
  {
   //--- Return Y position just below the header grip bottom edge
   return m_headerGripHeight + 8;
  }

//+------------------------------------------------------------------+
//| Compute bottom clip boundary for the category button area        |
//+------------------------------------------------------------------+
int CSidebarLayout::CalcClipBottom()
  {
   //--- Return Y position leaving bottom padding inside the panel
   return m_sidebarHeight - 10;
  }

We implement the "CalcSidebarHeight" method, where we first retrieve the current chart height using ChartGetInteger, then define vertical padding constants and set the button gap spacing. We pin the panel to a fixed vertical offset and update the chart object position accordingly. From there, we compute three height values: the ideal natural height needed to fit all category buttons, the minimum height that accommodates at least three buttons, and the maximum available height based on remaining chart space. We clamp the sidebar height between the minimum and available values using MathMax and MathMin, ensuring the panel always fits on the chart without overflowing. Finally, we calculate how many category buttons can fit within the usable button area and store that count for later use during rendering.

The "CalcCategoryButtonY" method returns the vertical pixel position of a category button by its index, computed as an offset below the header grip area plus the cumulative button size and padding. The "CalcClipTop" and "CalcClipBottom" methods define the top and bottom clipping boundaries of the button area, ensuring that rendering stays within the visible region of the panel. Next, we draw the tool elements.

Declaring and Implementing the Sidebar Renderer Class

The sixth class in the hierarchy brings all the visual elements together, compositing the background, header, category buttons, icons, and borders into the final sidebar display.

//+------------------------------------------------------------------+
//| CLASS 6 — Draw and composite all sidebar visual elements         |
//+------------------------------------------------------------------+
class CSidebarRenderer : public CSidebarLayout
  {
protected:
   //--- Draw and composite the full sidebar onto its canvas
   void DrawSidebar();
   //--- Draw the header grip strip at high resolution
   void DrawHeaderStripHR(int canvasW, int canvasH);
   //--- Draw a single category button indicator at high resolution
   void DrawCategoryButtonHR(CCanvas &target, int xHR, int yHR, int sizeHR, bool hasDot);
   //--- Draw icon glyphs and separator lines onto the display canvas
   void DrawSidebarIconLabels();
  };

//+------------------------------------------------------------------+
//| Draw and composite the full sidebar onto its canvas              |
//+------------------------------------------------------------------+
void CSidebarRenderer::DrawSidebar()
  {
   //--- Compute high-resolution canvas dimensions
   int ws = m_sidebarWidth * m_supersampleFactor, hs = m_sidebarHeight * m_supersampleFactor;
   //--- Resize high-res canvas if dimensions have changed
   if (m_canvasSidebarHighRes.Width() != ws || m_canvasSidebarHighRes.Height() != hs)
      m_canvasSidebarHighRes.Resize(ws, hs);
   //--- Clear the high-res canvas to fully transparent
   m_canvasSidebarHighRes.Erase(0x00000000);
   //--- Compute background alpha from opacity input
   uchar bgA = (uchar)(255 * BackgroundOpacity);
   //--- Determine which corners are rounded based on snap state
   bool rTL = (m_snapState != SNAP_LEFT), rBL = rTL;
   bool rTR = (m_snapState != SNAP_RIGHT), rBR = rTR;
   //--- Fill sidebar background with selective rounded corners
   FillSelectiveRoundRectHR(m_canvasSidebarHighRes, 0, 0, ws, hs,
                             m_panelCornerRadius * m_supersampleFactor,
                             ColorToARGB(m_themeColors.sidebarBackground, bgA), rTL, rTR, rBL, rBR);
   //--- Draw left snap flush edge using foreground chart color
   if (m_snapState == SNAP_LEFT)
      m_canvasSidebarHighRes.FillRectangle(0, 0, m_supersampleFactor - 1, hs - 1,
         ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 255));
   //--- Draw right snap flush edge using foreground chart color
   else if (m_snapState == SNAP_RIGHT)
      m_canvasSidebarHighRes.FillRectangle(ws - m_supersampleFactor, 0, ws - 1, hs - 1,
         ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 255));
   //--- Draw the header grip strip onto the high-res canvas
   DrawHeaderStripHR(ws, hs);
   //--- Draw each category button indicator at high resolution
   for (int c = 0; c < CAT_COUNT; c++)
      DrawCategoryButtonHR(m_canvasSidebarHighRes,
         (m_sidebarWidth - m_categoryButtonSize) / 2 * m_supersampleFactor,
         CalcCategoryButtonY(c) * m_supersampleFactor,
         m_categoryButtonSize * m_supersampleFactor,
         m_categories[c].hasMultipleTools);
   //--- Draw the panel border at high resolution if enabled
   if (BorderWidth > 0)
      DrawSelectiveRoundRectBorderHR(m_canvasSidebarHighRes, 0, 0, ws, hs,
                                      m_panelCornerRadius * m_supersampleFactor,
                                      ColorToARGB(m_themeColors.sidebarBorder, 255),
                                      BorderWidth * m_supersampleFactor, rTL, rTR, rBL, rBR);
   //--- Downsample high-res canvas into the display-resolution canvas
   DownsampleCanvas(m_canvasSidebar, m_canvasSidebarHighRes, m_supersampleFactor);
   //--- Overlay icon glyphs and separator lines onto the display canvas
   DrawSidebarIconLabels();
   //--- Flush the display canvas to the chart
   m_canvasSidebar.Update();
  }

//+------------------------------------------------------------------+
//| Draw the header grip strip at high resolution                    |
//+------------------------------------------------------------------+
void CSidebarRenderer::DrawHeaderStripHR(int canvasW, int canvasH)
  {
   //--- Compute scaled header height and border inset
   int headerH = m_headerGripHeight * m_supersampleFactor;
   int brd = BorderWidth * m_supersampleFactor;
   //--- Compute horizontal insets based on snap state
   int inL = (m_snapState == SNAP_LEFT) ? 0 : brd, inR = (m_snapState == SNAP_RIGHT) ? 0 : brd;
   //--- Compute header fill rectangle position and size
   int hx = inL, hy = brd, hw = canvasW - inL - inR, hh = headerH - brd;
   //--- Compute inner corner radius inset from border
   int innerR = MathMax(0, m_panelCornerRadius * m_supersampleFactor - brd);
   //--- Determine top corner rounding based on snap state
   bool rTL = (m_snapState != SNAP_LEFT), rTR = (m_snapState != SNAP_RIGHT);
   //--- Select header fill color based on active theme
   color hdrFill = m_isDarkTheme ? C'25,29,40' : C'245,247,252';
   //--- Fill the upper portion of the header strip with rounded top corners
   FillSelectiveRoundRectHR(m_canvasSidebarHighRes, hx, hy, hw, hh, innerR, ColorToARGB(hdrFill, 255), rTL, rTR, false, false);
   //--- Fill the lower half of the header to square off the bottom
   m_canvasSidebarHighRes.FillRectangle(hx, hy + hh / 2, hx + hw - 1, headerH - 1, ColorToARGB(hdrFill, 255));
   //--- Compute grip dot row position and height
   int row2Y = m_categoryButtonSize * m_supersampleFactor, row2H = 20 * m_supersampleFactor;
   //--- Compute dot spacing and radius at high resolution
   int gapX = 6 * m_supersampleFactor, dotR = 2 * m_supersampleFactor;
   uint dotColor = ColorToARGB(m_themeColors.gripDotsColor, 255);
   //--- Draw three horizontally centered grip dots
   for (int col = 0; col < 3; col++)
      m_canvasSidebarHighRes.FillCircle(canvasW / 2 + (col - 1) * gapX, row2Y + row2H / 2, dotR, dotColor);
  }

//+------------------------------------------------------------------+
//| Draw a single category button dot indicator at high resolution   |
//+------------------------------------------------------------------+
void CSidebarRenderer::DrawCategoryButtonHR(CCanvas &target, int xHR, int yHR, int sizeHR, bool hasDot)
  {
   //--- Draw the multi-tool indicator dot in the bottom-right corner if applicable
   if (hasDot)
      target.FillCircle(xHR + sizeHR - 6 * m_supersampleFactor, yHR + sizeHR - 6 * m_supersampleFactor,
                         2 * m_supersampleFactor, ColorToARGB(m_themeColors.gripDotsColor, 180));
  }

//+------------------------------------------------------------------+
//| Draw icon glyphs and separator lines onto the display canvas     |
//+------------------------------------------------------------------+
void CSidebarRenderer::DrawSidebarIconLabels()
  {
   //--- Iterate over all category slots to draw icons
   for (int c = 0; c < CAT_COUNT; c++)
     {
      //--- Compute button top-left position in display-resolution coordinates
      int btnY = CalcCategoryButtonY(c), btnX = (m_sidebarWidth - m_categoryButtonSize) / 2;
      //--- Set font to the category icon font at configured size
      m_canvasSidebar.FontSet(m_categories[c].iconFontName, CategoryIconSize);
      //--- Build the icon glyph string from its character code
      string sym = CharToString(m_categories[c].iconCharCode);
      //--- Measure icon glyph dimensions for centring
      int iw = m_canvasSidebar.TextWidth(sym), ih = m_canvasSidebar.TextHeight(sym);
      //--- Draw the icon centred within the button area
      m_canvasSidebar.TextOut(btnX + (m_categoryButtonSize - iw) / 2, btnY + (m_categoryButtonSize - ih) / 2, sym, ColorToARGB(m_themeColors.buttonIconColor, 255));
     }
   //--- Compute separator line left and right extents based on snap and border
   int brd = BorderWidth;
   int sepL = (m_snapState == SNAP_LEFT) ? 0 : brd, sepR = m_sidebarWidth - 1 - ((m_snapState == SNAP_RIGHT) ? 0 : brd);
   //--- Pack separator line colors for primary and secondary lines
   uint sepCol  = ColorToARGB(m_themeColors.separatorColor, 255);
   uint sepCol2 = ColorToARGB(m_isDarkTheme ? C'45,52,66' : C'195,202,215', 255);
   //--- Draw primary separator below the header strip
   m_canvasSidebar.Line(sepL, m_headerGripHeight - 1,    sepR, m_headerGripHeight - 1,    sepCol);
   //--- Draw first secondary separator below the header row
   m_canvasSidebar.Line(sepL, m_categoryButtonSize,      sepR, m_categoryButtonSize,      sepCol2);
   //--- Draw second secondary separator below the theme toggle row
   m_canvasSidebar.Line(sepL, m_categoryButtonSize + 20, sepR, m_categoryButtonSize + 20, sepCol2);
   //--- Set font to Webdings for the close button icon
   m_canvasSidebar.FontSet("Webdings", CategoryIconSize);
   string closeSym = CharToString((uchar)114); // Webdings char 114 = close/X glyph
   //--- Measure close icon dimensions for centring
   int clW = m_canvasSidebar.TextWidth(closeSym), clH = m_canvasSidebar.TextHeight(closeSym);
   //--- Draw the close icon centred in the top header button slot
   m_canvasSidebar.TextOut((m_sidebarWidth - clW) / 2, (m_categoryButtonSize - clH) / 2, closeSym, ColorToARGB(m_themeColors.buttonIconColor, 255));
   //--- Compute row extents for the theme toggle button slot
   int row3Y = m_categoryButtonSize + 20, row3H = m_headerGripHeight - m_categoryButtonSize - 20;
   //--- Set font to Wingdings for the theme toggle icon
   m_canvasSidebar.FontSet("Wingdings", CategoryIconSize);
   string themeSym = CharToString((uchar)91); // Wingdings char 91 = sun/moon glyph
   //--- Measure theme icon dimensions for centring
   int thW = m_canvasSidebar.TextWidth(themeSym), thH = m_canvasSidebar.TextHeight(themeSym);
   //--- Draw the theme toggle icon centred in its row slot
   m_canvasSidebar.TextOut((m_sidebarWidth - thW) / 2, row3Y + (row3H - thH) / 2, themeSym, ColorToARGB(m_themeColors.buttonIconColor, 255));
  }

First, we declare the "CSidebarRenderer" class, which inherits from "CSidebarLayout" and introduces four protected rendering methods: "DrawSidebar" for the full compositing pipeline, "DrawHeaderStripHR" for the header grip area, "DrawCategoryButtonHR" for individual button indicators, and "DrawSidebarIconLabels" for overlaying icon glyphs and separator lines onto the display canvas.

Then, we implement the "DrawSidebar" method as the main rendering entry point. We compute the high-resolution canvas dimensions, resize it if needed, and clear it to fully transparent. We then determine which corners should be rounded based on the current snap state, so that a side snapped flush against the chart edge gets square corners while the opposite side stays rounded. We fill the sidebar background using "FillSelectiveRoundRectHR" with the theme background color and opacity, and if the panel is snapped, we draw a thin flush edge line using the chart foreground color. From there, we call the header strip drawing method, loop through all categories to draw their button indicators, and draw the panel border if enabled. Finally, we downsample the high-resolution canvas into the display canvas, overlay the icon glyphs and separators, and flush the result to the chart with the Update method.

The "DrawHeaderStripHR" method renders the header grip area at high resolution. We compute the scaled header dimensions and border insets adjusted for snap state, fill the upper portion with rounded top corners using a theme-appropriate header color, then square off the lower half with a plain rectangle fill. We finish by drawing three horizontally centered grip dots using FillCircle to give the user a visual drag handle.

The "DrawCategoryButtonHR" method is a small utility that draws a multi-tool indicator dot in the bottom-right corner of a category button when the category contains sub-tools. Categories without multiple tools simply receive no dot.

The "DrawSidebarIconLabels" method works on the display-resolution canvas directly. We loop through all categories, setting the font to each category's icon font using FontSet, measuring the glyph dimensions, and drawing the icon centered within its button area using the TextOut method. We then draw three separator lines at key positions using the Line method, with colors pulled from the theme. Finally, we draw the close button icon centered in the top header slot and the theme toggle icon centered in its row below, giving the sidebar its interactive header controls. With all that done, we now need to initialize and render the entire palette with all the elements.

Declaring the Top-Level Sidebar Shell Class

The seventh and final class in the hierarchy serves as the public-facing shell that ties the entire class chain together, exposing the interface that the rest of the program interacts with.

//+------------------------------------------------------------------+
//| CLASS 7 — Top-level sidebar shell exposing the public interface  |
//+------------------------------------------------------------------+
class CToolsSidebar : public CSidebarRenderer
  {
public:
   CToolsSidebar()  { InitDefaults(); }  // Construct and apply default state
   ~CToolsSidebar() { Destroy(); }       // Destruct and clean up all resources

   //--- Initialise the sidebar and build all canvas objects
   bool Init(long chartId);
   //--- Destroy all canvas objects and release resources
   void Destroy();
   //--- Handle incoming chart events (reserved for future interaction)
   void OnEvent(const int id, const long &lp, const double &dp, const string &sp) {}

private:
   //--- Set all member variables to their compile-time default values
   void InitDefaults();
  };

We declare the "CToolsSidebar" class, which inherits from "CSidebarRenderer" and provides the public entry points for the sidebar lifecycle. The constructor calls "InitDefaults" to set all member variables to their compile-time defaults, while the destructor calls "Destroy" to clean up all resources. We declare the public "Init" method for initializing the sidebar and building the canvas objects, "Destroy" for tearing everything down, and "OnEvent" for handling incoming chart events, which is currently empty and reserved for future interaction logic. A private "InitDefaults" method handles resetting all members to their default values before initialization begins. We define these methods using the following logic:

//+------------------------------------------------------------------+
//| Set all member variables to their compile-time default values    |
//+------------------------------------------------------------------+
void CToolsSidebar::InitDefaults()
  {
   //--- Reset chart reference to default
   m_chartId               = 0;
   //--- Set the sidebar bitmap label object name
   m_nameSidebar           = "ToolsPalette_Sidebar";
   //--- Set supersampling factor for high-res rendering
   m_supersampleFactor     = 4;
   //--- Set default button size and spacing
   m_categoryButtonSize    = 36;
   m_categoryButtonPadding = 6;
   //--- Set panel corner rounding radius
   m_panelCornerRadius     = 10;
   //--- Set header and grip strip height
   m_headerGripHeight      = 92;
   //--- Set sidebar panel width
   m_sidebarWidth          = 48;
   //--- Reset computed height and visible category count
   m_sidebarHeight         = 0;
   m_sidebarMaxVisibleCats = 0;
   //--- Reset panel position to origin
   m_panelX                = 0;
   m_panelY                = CanvasY;
   //--- Default snap state to left edge
   m_snapState             = SNAP_LEFT;
   //--- Apply the starting theme from user input
   m_isDarkTheme           = StartDark;
  }

//+------------------------------------------------------------------+
//| Initialise the sidebar and build all canvas objects              |
//+------------------------------------------------------------------+
bool CToolsSidebar::Init(long chartId)
  {
   //--- Reset all members to defaults before initialising
   InitDefaults();
   //--- Store the target chart identifier
   m_chartId = chartId;
   //--- Populate all category definitions
   InitAllCategories();
   //--- Apply the active theme color set
   ApplyTheme();
   //--- Compute and set the sidebar panel height
   CalcSidebarHeight();
   //--- Create all canvas layers; abort on failure
   if (!CreateAllCanvases(m_sidebarWidth, m_sidebarHeight)) return false;
   //--- Position the sidebar panel on the chart
   ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX);
   ObjectSetInteger(0, m_nameSidebar, OBJPROP_YDISTANCE, m_panelY);
   //--- Set the sidebar Z-order to render above chart objects
   ObjectSetInteger(0, m_nameSidebar, OBJPROP_ZORDER, 100);
   //--- Draw the initial sidebar frame
   DrawSidebar();
   return true;
  }

//+------------------------------------------------------------------+
//| Destroy all canvas objects and release resources                 |
//+------------------------------------------------------------------+
void CToolsSidebar::Destroy()
  {
   //--- Delegate canvas cleanup to the layer base class
   DestroyAllCanvases();
  }

We implement the "InitDefaults" method, where we reset all member variables to their baseline values. We clear the chart identifier, set the bitmap label object name, configure a supersampling factor of four for high-resolution rendering, and define the default button size, padding, corner radius, header grip height, and sidebar width. We reset the computed height and visible category count to zero, position the panel at the left origin with the vertical offset from the user input, default the snap state to the left edge, and apply the starting theme preference.

The "Init" method orchestrates the full startup sequence. We first call "InitDefaults" to ensure a clean state, then store the chart identifier and call "InitAllCategories" to populate the category registry. We apply the active theme colors, compute the sidebar height based on available chart space, and create both canvas layers. If canvas creation fails, we return false to signal the failure. Otherwise, we position the sidebar on the chart using ObjectSetInteger, set its z-order so it renders above other chart objects, and call "DrawSidebar" to paint the initial frame.

The Destroy method simply delegates to "DestroyAllCanvases" to clean up both canvases and remove the chart object, keeping the teardown path straightforward. That marks the end of our classes' architecture, and to make the classes usable, we will need to instantiate a global instance of the class, which will give us access to the class's members and methods we defined.

Declaring the Global Sidebar Instance

Here we create the single instance that the event handlers will interact with throughout the program's lifetime.

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
CToolsSidebar g_sidebar; // Global sidebar instance

We declare a global instance of the "CToolsSidebar" class, which serves as the sole sidebar object for the entire program. Because the constructor automatically calls "InitDefaults", the instance is ready for initialization as soon as the program loads. We can use this instance to get access to all class members using a dot operator. Here is an illustration sample.

DOT OPERATOR ACCESS ILLUSTRATION

With that in mind, we can now move on to initializing the program.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   //--- Initialise the sidebar and fail if canvas creation fails
   if (!g_sidebar.Init(ChartID())) return INIT_FAILED;
   //--- Force a chart redraw to display the sidebar immediately
   ChartRedraw();
//---
   return(INIT_SUCCEEDED);
  }

We call the "Init" method on our global sidebar instance, passing in the chart identifier from ChartID. If initialization fails, we return "INIT_FAILED" to prevent the program from running with incomplete canvases. Otherwise, we force a chart redraw with ChartRedraw to display the sidebar immediately and return INIT_SUCCEEDED to confirm proper setup. Upon compilation, we get the following outcome.

PALETTE INITIALIZATION

With that done, we will need to handle the chart events and deinitialization logic.

Handling Deinitialization and Chart Events

These event handlers manage the program's cleanup and forward user interactions to the sidebar.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   //--- Destroy the sidebar and remove all canvas objects
   g_sidebar.Destroy();
   //--- Force a chart redraw to clear the sidebar from view
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Expert chart event function                                      |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lp, const double &dp, const string &sp)
  {
//---
   //--- Forward the chart event to the sidebar event handler
   g_sidebar.OnEvent(id, lp, dp, sp);
  }

In the OnDeinit event handler, we call "Destroy" on the global sidebar instance to clean up all canvas objects and remove them from the chart. We then force a chart redraw with "ChartRedraw" to clear the sidebar from view.

The OnChartEvent event handler forwards all incoming chart events directly to the sidebar's "OnEvent" method, which is currently reserved for future interaction logic such as tool selection, dragging, and theme toggling. This delegation keeps the event handler clean and ensures all interaction logic will live inside the class hierarchy as we expand the sidebar in upcoming parts. We handle testing the program in the next section.


Backtesting

We compiled the program and attached it to the chart. Below is the resulting visualization in a single Graphics Interchange Format (GIF) image.

TOOLS PALETTE BACKTEST GIF

During testing, the sidebar rendered cleanly with anti-aliased rounded corners on both dark and light themes, the category icons displayed correctly in their centered positions, and the snap alignment kept the panel flush against the left chart edge with squared-off corners on the snapped side.


Conclusion

In conclusion, we have replaced the ad-hoc, function-heavy palette with a layered, class-based sidebar architecture that enforces clear responsibilities and a clean lifecycle. The end result is a compilable MQL5 module that initializes in the OnInit event handler and fully tears down in the OnDeinit event handler, rendering a vertical sidebar palette on the chart with supersampled high-resolution rendering and downsampling for anti-aliased rounded corners and borders, selective per-corner rounding when snapped to the left or right chart edge, centralized light and dark theme sets applied from a single manager, a category registry where adding a new tool group is a single definition entry rather than multiple edits across files, and predictable canvas creation and resizing under a single public shell class that owns initialization and drawing.

These outcomes are measurable: the project compiles, creates bitmap labels, displays the sidebar with the features above, responds to chart redraw, and removes its objects cleanly on deinitialization. Architecturally, each layer exposes only what the next requires — primitives feed into the theme manager, which feeds into the registry, then into the canvas layer, the layout, the renderer, and finally the shell. After reading this article, you will be able to:

  • Structure canvas-based chart utilities using a layered class hierarchy where each class owns a single rendering or layout concern
  • Produce anti-aliased rounded corners and borders through supersampled high-resolution rendering with selective per-corner rounding
  • Implement a category registry system that makes adding new tool groups a matter of filling in a definition rather than reworking rendering logic

In the next part, we will build directly on this stable foundation by adding full interactivity, including flyout panels for tool selection, mouse-driven dragging and resizing, scrollable lists with thumb pill indicators, hover highlights, active state accent bars, and a chart drawing engine that places objects from single-click, two-click, and three-click tools. Stay tuned.

Attached files |
MetaTrader 5 Machine Learning Blueprint (Part 14): Transaction Cost Modeling for Triple-Barrier Labels in MQL5 MetaTrader 5 Machine Learning Blueprint (Part 14): Transaction Cost Modeling for Triple-Barrier Labels in MQL5
The article replaces hardcoded cost assumptions in triple-barrier labeling with measured inputs. An MQL5 script captures spread distribution, swap rates, and symbol metadata from your broker, and a Python model converts them into a broker-calibrated min ret you can pass to get events. Labels then reflect the actual round-trip friction for your instrument and holding period.
Market Microstructure in MQL5: Robust Foundation (Part 1) Market Microstructure in MQL5: Robust Foundation (Part 1)
This article builds the foundation layer of a twelve-part MQL5 market microstructure toolkit. It implements guarded math helpers (SafeDivide, SafeLog, SafeSqrt, SafeExp, SafeTanh), robust data validation (ValidateSymbolV2, SafeCopyClose), trimmed statistical estimators (robust mean var), a linear regression slope, shared structs, and an FFT. You compile a single include file that hardens indicators and expert advisors against silent numerical failures and standardizes data flow for later parts.
MetaTrader 5 and the MQL5 Economic Calendar: How to Turn News into a Reproducible Trading System MetaTrader 5 and the MQL5 Economic Calendar: How to Turn News into a Reproducible Trading System
The article presents a systematic approach to news trading in MetaTrader 5 using the built-in economic calendar: data structure, API functions, time synchronization rules, and event filtering. Methods of caching and incremental updating without overloading the server are described. The article also provides a working mechanism for exporting history to an .EX5 resource for deterministic testing using the same algorithm.
Three MACD Filters on US_TECH100: Five Years of Broker Data Three MACD Filters on US_TECH100: Five Years of Broker Data
This article tests three common filters on a standard MACD crossover for US_TECH100 H1 using five years of broker-native data. Filters are layered incrementally: regime, higher timeframe (HTF) alignment, and US session timing, to isolate each one's marginal impact. Results show session timing contributes far more than indicator refinements, while regime and HTF add little on their own. Includes a reproducible MQL5 regime classifier.