From Novice to Expert: Parameter Control Utility

0
2 317

Contents:


Introduction

Today, we continue building on the foundation established in our previous article. If you’ve been following along, you’ll recall that we developed an indicator designed to visualize higher-timeframe (HTF) periods directly on lower-timeframe charts. This concept turned out to be a powerful analytical tool, revealing the intricate price movements hidden within the larger bars of higher timeframes.

Such detail is invaluable to a trader—for example, what appears as a simple wick on a higher timeframe can, when examined at a lower timeframe, reveal distinct patterns, support and resistance levels, or even order blocks. Understanding this internal market structure allows traders to better anticipate future price behavior and refine their strategies. In the illustration in Figure 1 below, the H1 candlestick period A on the left shows a resistance level within its bullish wick—the rejection area—which was later tested and respected by period B on the right. This interaction highlights how past wick zones can serve as meaningful reference points for future price reactions.

Analysis H1 and M1 timeframes

Fig. 1. Insights into the H1 period at M1 using Marker Periods Synchronizer

However, one challenge that often arises when developing complex tools is the lack of intuitive, easily accessible controls. Traditionally, we have relied on the Inputs tab of an EA or indicator for parameter adjustments—a process that can quickly become tedious when tuning multiple values or experimenting with visual settings.

In this project, we tackle that challenge by creating a real-time control utility—an Expert Advisor (EA) that transforms static input parameters into interactive, on-chart controls. This “Market Period Synchronizer Control Utility” extends the earlier indicator concept into a dynamic dashboard with advanced features, offering immediate feedback and a more efficient analytical workflow.

Key Advantages of the New Utility

  1. Instant parameter access—Adjust key settings directly from the chart without opening the properties dialog.
  2. Real-time visual updates—See immediate changes to objects, colors, and timeframes as you modify controls.
  3. Accelerated analysis—Eliminate repetitive save-and-reload steps, streamlining your workflow.
  4. Modern visual interface—Uses CCanvas for smooth, semi-transparent, and visually appealing panels.
  5. Multi-timeframe synchronization—Seamlessly view and control major and minor timeframe structures.
  6. Interactive sliders—Quickly fine-tune values such as widths and refresh intervals.
  7. Toggle switches—Enable or disable features instantly with a single click.

Below is an illustration of what we aim to achieve by the end of this discussion. After that, we’ll dive into the implementation phase, where we’ll break down each development step, explain the core code structure, and conclude with deployment and performance testing.

Market Periods Synchronizer Control Utility

Fig. 2. A screenshot of parameter control features.


Implementation

We will implement a modular Expert Advisor that converts static inputs into an on-chart dashboard. The EA (1) reads HTF bars (time, open, close), (2) draws background HTF visuals (vertical lines, optional body fills, open/close horizontals), and (3) exposes runtime controls via a semi-transparent canvas and chart objects (buttons, labels, vertical sliders, pop-up TF dropdowns). The UI updates runtime state variables (mutable copies of inputs), and RefreshLines() reuses those values to update chart objects immediately. The design prioritizes clarity (UI container + labels), responsiveness (slider drag updates immediately), and non-interference (HTF objects are created with OBJPROP_BACK=true so UI stays clickable).

Key implementation notes for readers not to miss:

  • Inputs are treated as defaults only—the dashboard changes runtime copies (prefixed g_) so UI interactions persist while the EA runs.
  • Sliders are vertical and implemented with two chart buttons: a track (visual) and a knob (selectable)—dragging the knob updates the underlying value in real-time.
  • HTF shapes are drawn in the background (OBJPROP_BACK = true) to avoid stealing mouse events from the UI.
  • OnTick() checks only time changes (via iTime) to avoid needless redraws.

1) Header, includes, and user inputs—purpose & behavior

We begin by including Canvas.mqh and declaring all input parameters. The #include <Canvas/Canvas.mqh> line pulls in a small UI helper class (CCanvas) that we use to draw a semi-transparent background bitmap for our dashboard. We do this because chart objects alone (buttons/labels) look clunky on different chart themes; the canvas gives a single, consistent container we can style once and reuse. Note that Canvas.mqh must be present in the include path—otherwise the EA will fail to compile. We also use #property strict so the compiler enforces modern MQL type and signature rules.

The input block is intentionally organized into three practical categories so users immediately understand where to look and how the tool maps to trading concepts. The first category—Major timeframe and visuals—contains the timeframe that defines the major bars we mark (for example H1), a lookback depth (how many HTF bars we draw), a default color and default width for the major vertical separators, and a refresh interval in seconds. These settings define the primary structure: major bars are the frame within which everything else (fills, open/close horizontals, and minors) is contextualized. The lookback is particularly important because it controls how many major-period objects we may create; a large lookback can create many objects and slow charts, so we recommend sensible defaults (200) and maximums in a production build.

The second category addresses the open/close markers and body fills. We include toggles and colors for open and close horizontal markers (useful to see where each HTF bar opened and closed relative to the lower timeframe), width and line style for these horizontals, an offset in current-TF bars to control how far the horizontal extends, and booleans plus colors for body fills (bull/bear). These are optional but powerful: the fills provide a quick visual of bullish vs. bearish HTF bars, while the open/close lines help us see where intra-bar structure may have created support/resistance inside the HTF bar body or wicks. In code we store these as input defaults (immutable) and copy them into runtime variables so the dashboard can toggle them live.

The third category covers minor periods. We give two minor TF options (Minor1, Minor2), each with a toggle, a timeframe selection, a color, and a width. The purpose is to let us visualize intermediate intra-HTF structure: for instance, on an H1 major, an M15 minor can show the internal subdivisions. The EA draws minor verticals only when the minor time strictly falls between two consecutive major times (or within the current major bar)—that behavior replicates the original indicator logic. By enabling two separate minor layers, we can see nested structure (e.g., H1 major, M30 minor, M15 micro-minor) simultaneously.

A crucial architecture point and a frequent pitfall: input parameters in MQL are compile-time constants for runtime—they cannot be reassigned by the EA while running. To provide a truly interactive dashboard, we therefore copy each input into a corresponding g_ (global mutable) variable during OnInit. Our UI code updates the g_ variables (for example, g_WidthMajor or g_ShowFill) and RefreshLines() reads those g_ values to update chart objects immediately. This separation avoids confusion and lets us treat input as safe defaults while providing full runtime control. It also means if the user wants to persist settings across sessions, we must add explicit save/load logic—a future enhancement.

Finally, we use naming conventions and prefixes aggressively so the program can manage, update, and garbage-collect chart objects deterministically. Prefixes like HTF_MAJ_, HTF_MIN1_, and MPS_UI_ allow us to find, update, or delete only the objects we created without touching other drawings the user might have on the chart. This is a small but vital engineering detail: without consistent naming it is easy to accidentally delete or overwrite unrelated objects. We also keep an internal tf_list[] array (the set of allowed timeframes) so dropdowns and cycling logic are consistent and localized.

#include <Canvas/Canvas.mqh>   // Canvas helper library (expects Canvas.mqh to be present) // --------------------------- USER INPUTS --------------------------- // Major timeframe + lookback + default visuals input ENUM_TIMEFRAMES InpHigherTF      = PERIOD_H1;   // Major higher timeframe input int            InpLookback       = 200;         // Lookback (bars) input color          InpColorMajor     = clrRed;      // Major line color input int            InpWidthMajor     = 2;           // Major line width input int            InpRefreshSec     = 5;           // Refresh interval (seconds)  // Open/Close marker settings input bool           InpShowOpenClose  = true;        // show open/close markers input color          InpColorOpen      = clrGreen; input color          InpColorClose     = clrLime; input int            InpWidthOC        = 1; input ENUM_LINE_STYLE InpStyleOC       = STYLE_DASH; input int            InpHorizOffsetBars= 3; // Body fill for majors input bool           InpShowFill       = true; input color          InpFillBull       = clrLime; input color          InpFillBear       = clrPink; // Minor periods input bool           InpShowMinor1     = false; input ENUM_TIMEFRAMES InpMinor1TF     = PERIOD_M30; input color          InpColorMin1      = clrOrange; input int            InpWidthMin1      = 1; input bool           InpShowMinor2     = false; input ENUM_TIMEFRAMES InpMinor2TF     = PERIOD_M15; input color          InpColorMin2      = clrYellow; input int            InpWidthMin2      = 1;

2) Globals and runtime copies—why separate inputs & runtime state?

We declare arrays and g_ variables for mutable runtime state, color palettes, slider infrastructure, and UI name strings. This separation is key: UI operations write to g_* values; RefreshLines() reads them. We also prepare arrays that hold slider metadata so each slider can be created/updated generically.

// --------------------------- GLOBALS ------------------------------- enum SliderIndex { SLIDER_MAJ_WIDTH = 0, SLIDER_REFRESH = 1 }; const int SLIDER_COUNT = 2; int Y_OFFSET = 50; // top offset for UI container // runtime (mutable) copies of inputs (dashboard will change these) ENUM_TIMEFRAMES g_HigherTF; int             g_Lookback; color           g_ColorMajor; int             g_WidthMajor; int             g_RefreshSec; // ... (other g_ variables for toggles & minors) // slider infrastructure (vertical sliders) string g_slider_track_names[]; string g_slider_knob_names[]; int    g_slider_min[]; int    g_slider_max[]; int    g_slider_left_x[]; int    g_slider_top_y[]; int    g_vslider_height_px = 110; int    g_vslider_width_px  = 14; int    g_slider_knob_w     = 12; bool   g_slider_drag = false; int    g_current_slider = -1; 

3) TF helper utilities—consistent labels & cycling

TFToString() centralizes timeframe labeling for buttons and object names (so dropdowns and labels always match). FindNextTFIndex() implements a simple cycle to move to the next timeframe in tf_list—useful for quick button cycling.

string TFToString(ENUM_TIMEFRAMES tf)   {    switch(tf)      {       case PERIOD_M1:  return "M1";       case PERIOD_M5:  return "M5";       case PERIOD_M15: return "M15";       case PERIOD_M30: return "M30";       case PERIOD_H1:  return "H1";       case PERIOD_H4:  return "H4";       case PERIOD_D1:  return "D1";       case PERIOD_W1:  return "W1";       case PERIOD_MN1: return "MN";      }    return IntegerToString((int)tf);   } int FindNextTFIndex(ENUM_TIMEFRAMES current)   {    int n = ArraySize(tf_list);    for(int i=0;i<n;i++) if(tf_list[i] == current) return (i+1)%n;    return 0;   } 

4) OnInit()—prepare runtime state, UI names, canvas, and initial draw

OnInit is the orchestration step: copy input values into g_*, compute last bar times for change detection, build UI name strings, prepare slider arrays, create the canvas background, create UI widgets (buttons, labels, color buttons), create the vertical sliders and labels, and then start the timer. Note how we call RefreshLines() at the end so the chart is immediately populated with HTF objects.

After compiling and attaching the EA, we expect to see a semi-transparent UI panel with controls; HTF lines and fills appear on the chart reflecting the input defaults.

int OnInit()   {    main_chart_id = ChartID();    // copy inputs -> runtime    g_HigherTF        = InpHigherTF;    g_Lookback        = MathMax(10, InpLookback);    g_ColorMajor      = InpColorMajor;    g_WidthMajor      = MathMax(1, InpWidthMajor);    g_RefreshSec      = MathMax(1, InpRefreshSec);    // ... (copy the rest of Inp->g_ variables)    // initialize last bar times    g_last_major_time = iTime(_Symbol, g_HigherTF, 0);    if(g_ShowMinor1) g_last_minor1_time = iTime(_Symbol, g_Minor1TF, 0);    if(g_ShowMinor2) g_last_minor2_time = iTime(_Symbol, g_Minor2TF, 0);    // UI prefix and object names    UI_PREFIX = StringFormat("MPS_UI_%d_", main_chart_id);    lbl_title = UI_PREFIX + "LBL_TITLE";    btn_major_tf = UI_PREFIX + "BTN_MAJ_TF";    // ... (initialize other UI name strings)    // prepare slider arrays    ArrayResize(g_slider_track_names, SLIDER_COUNT);    ArrayResize(g_slider_knob_names,  SLIDER_COUNT);    ArrayResize(g_slider_min,        SLIDER_COUNT);    ArrayResize(g_slider_max,        SLIDER_COUNT);    ArrayResize(g_slider_left_x,     SLIDER_COUNT);    ArrayResize(g_slider_top_y,      SLIDER_COUNT);    for(int i=0;i<SLIDER_COUNT;i++)      {       g_slider_track_names[i] = UI_PREFIX + StringFormat("SL_TRK_%d", i);       g_slider_knob_names[i]  = UI_PREFIX + StringFormat("SL_KNB_%d", i);      }    // background area coords and create UI    g_bg_y = Y_OFFSET - 6;    g_bg_h = 250;    CreateUIBackground();    CreateLabel(lbl_title, 12, 4 + Y_OFFSET, "Market Period Synchronizer Control Utility", 12);    ObjectSetInteger(main_chart_id, lbl_title, OBJPROP_COLOR, XRGB(230,230,230));    // create many UI buttons/labels and sliders...    CreateButton(btn_major_tf, 12, 34 + Y_OFFSET, 70, 24, TFToString(g_HigherTF));    CreateLabel(lbl_major_tf, 92, 36 + Y_OFFSET, "Major TF");    // ... more buttons and color swatches    // Major width slider (vertical)    g_slider_left_x[SLIDER_MAJ_WIDTH] = 190;    g_slider_top_y[SLIDER_MAJ_WIDTH]  = slider_base_top;    g_slider_min[SLIDER_MAJ_WIDTH] = 1; g_slider_max[SLIDER_MAJ_WIDTH] = 10;    CreateVerticalSliderAt(SLIDER_MAJ_WIDTH, g_slider_left_x[SLIDER_MAJ_WIDTH], g_slider_top_y[SLIDER_MAJ_WIDTH],                           g_slider_track_names[SLIDER_MAJ_WIDTH], g_slider_knob_names[SLIDER_MAJ_WIDTH],                           g_WidthMajor, g_slider_min[SLIDER_MAJ_WIDTH], g_slider_max[SLIDER_MAJ_WIDTH], g_vslider_height_px);    // start events & timer    ChartSetInteger(main_chart_id, CHART_EVENT_MOUSE_MOVE, true);    EventSetTimer(g_RefreshSec);    // initial drawing of HTF objects    RefreshLines();    return INIT_SUCCEEDED;   } 

5) Canvas background creation—visual grouping & click behavior

CreateUIBackground() uses the CCanvas helper to create a bitmap label that acts as the dark, semi-transparent panel behind UI elements. Important design decision: we set the canvas OBJPROP_BACK = false and OBJPROP_SELECTABLE = false so the canvas is visible but does not intercept clicks—buttons drawn in front still receive mouse events.

A polished dark panel improves legibility across symbols and chart themes.

void CreateUIBackground()   {    g_bg_name = UI_PREFIX + "BG";    ObjectDelete(main_chart_id, g_bg_name);    bool ok = g_bgCanvas.CreateBitmapLabel(main_chart_id, 0, g_bg_name, g_bg_x, g_bg_y, g_bg_w, g_bg_h, COLOR_FORMAT_ARGB_RAW);    if(!ok) { PrintFormat("CreateUIBackground: CreateBitmapLabel failed err=%d", GetLastError()); return; }    uint dark_grey = ARGB(180, 30, 30, 30);    uint border_col = XRGB(80, 80, 80);    uint top_strip  = ARGB(210, 24, 24, 24);    g_bgCanvas.FillRectangle(0, 0, g_bg_w - 1, g_bg_h - 1, dark_grey);    g_bgCanvas.Rectangle(0, 0, g_bg_w - 1, g_bg_h - 1, border_col);    g_bgCanvas.FillRectangle(0, 0, g_bg_w - 1, 28, top_strip);    g_bgCanvas.Update(true);    ObjectSetInteger(main_chart_id, g_bg_name, OBJPROP_BACK, false);    ObjectSetInteger(main_chart_id, g_bg_name, OBJPROP_SELECTABLE, false);    ObjectSetInteger(main_chart_id, g_bg_name, OBJPROP_HIDDEN, false);   } 

6) UI creation helpers—consistent button/label styling

CreateButton, CreateLabel, and CreateColorButton centralize UI creation so layout and styling remain consistent. Buttons are created as selectable and drawn in front (OBJPROP_BACK=false), labels are non-selectable. This ensures a predictable event model and consistent look.

void CreateButton(string name,int x,int y,int w,int h,string text)   {    if(StringLen(name) == 0) return;    ObjectDelete(main_chart_id, name);    if(!ObjectCreate(main_chart_id, name, OBJ_BUTTON, 0, 0, 0))      { PrintFormat("CreateButton: failed to create %s err=%d", name, GetLastError()); return; }    ObjectSetInteger(main_chart_id, name, OBJPROP_BACK, false);     // draw in front    ObjectSetInteger(main_chart_id, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);    ObjectSetInteger(main_chart_id, name, OBJPROP_XDISTANCE, x);    ObjectSetInteger(main_chart_id, name, OBJPROP_YDISTANCE, y);    ObjectSetInteger(main_chart_id, name, OBJPROP_XSIZE, w);    ObjectSetInteger(main_chart_id, name, OBJPROP_YSIZE, h);    ObjectSetString(main_chart_id, name, OBJPROP_TEXT, text);    ObjectSetInteger(main_chart_id, name, OBJPROP_FONTSIZE, 10);    ObjectSetInteger(main_chart_id, name, OBJPROP_SELECTABLE, true);    ObjectSetInteger(main_chart_id, name, OBJPROP_HIDDEN, false);   } void CreateLabel(string name,int x,int y,string text, int fontsize=9)   {    if(StringLen(name) == 0) return;    ObjectDelete(main_chart_id, name);    if(!ObjectCreate(main_chart_id, name, OBJ_LABEL, 0, 0, 0))      { PrintFormat("CreateLabel: failed to create %s err=%d", name, GetLastError()); return; }    ObjectSetInteger(main_chart_id, name, OBJPROP_BACK, false);    ObjectSetInteger(main_chart_id, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);    ObjectSetInteger(main_chart_id, name, OBJPROP_XDISTANCE, x);    ObjectSetInteger(main_chart_id, name, OBJPROP_YDISTANCE, y);    ObjectSetString(main_chart_id, name, OBJPROP_TEXT, text);    ObjectSetInteger(main_chart_id, name, OBJPROP_FONTSIZE, fontsize);    ObjectSetInteger(main_chart_id, name, OBJPROP_SELECTABLE, false);    ObjectSetInteger(main_chart_id, name, OBJPROP_HIDDEN, false);   } void CreateColorButton(string name, int x, int y, int w, int h, color col)   {    CreateButton(name, x, y, w, h, "");    ObjectSetInteger(main_chart_id, name, OBJPROP_BGCOLOR, col);    ObjectSetInteger(main_chart_id, name, OBJPROP_FONTSIZE, 8);   } 

7) Vertical sliders—architecture & real-time dragging

Vertical sliders are assembled from a non-selectable track (visual) and a selectable knob (button). CreateVerticalSliderAt() computes the knob position from the value range, stores slider metadata in arrays, and places the knob. Dragging logic (handled in OnChartEvent) uses CHARTEVENT_MOUSE_MOVE to set knob Y, compute a ratio, and map it back to the value range. When a slider changes, the code updates corresponding g_ variables (e.g., g_WidthMajor, g_RefreshSec) and immediately calls RefreshLines().

The Y coordinate is inverted for value mapping—top = max value.

void CreateVerticalSliderAt(int id, int base_x, int base_y, string track_name, string knob_name, int current_value, int min_val, int max_val, int track_height)   {    // track    ObjectDelete(main_chart_id, track_name);    if(!ObjectCreate(main_chart_id, track_name, OBJ_BUTTON, 0, 0, 0))      { PrintFormat("CreateVerticalSliderAt: failed track %s err=%d", track_name, GetLastError()); }    ObjectSetInteger(main_chart_id, track_name, OBJPROP_BACK, false);    ObjectSetInteger(main_chart_id, track_name, OBJPROP_CORNER, CORNER_LEFT_UPPER);    ObjectSetInteger(main_chart_id, track_name, OBJPROP_XDISTANCE, base_x);    ObjectSetInteger(main_chart_id, track_name, OBJPROP_YDISTANCE, base_y);    ObjectSetInteger(main_chart_id, track_name, OBJPROP_XSIZE, g_vslider_width_px);    ObjectSetInteger(main_chart_id, track_name, OBJPROP_YSIZE, track_height);    ObjectSetString(main_chart_id, track_name, OBJPROP_TEXT, "");    ObjectSetInteger(main_chart_id, track_name, OBJPROP_SELECTABLE, false);    ObjectSetInteger(main_chart_id, track_name, OBJPROP_HIDDEN, false);    // knob    ObjectDelete(main_chart_id, knob_name);    if(!ObjectCreate(main_chart_id, knob_name, OBJ_BUTTON, 0, 0, 0))      { PrintFormat("CreateVerticalSliderAt: failed knob %s err=%d", knob_name, GetLastError()); }    ObjectSetInteger(main_chart_id, knob_name, OBJPROP_BACK, false);    ObjectSetInteger(main_chart_id, knob_name, OBJPROP_CORNER, CORNER_LEFT_UPPER);    double ratio = 0.0;    if(max_val > min_val) ratio = double(current_value - min_val) / double(max_val - min_val);    int knob_y = base_y + (int)MathRound((1.0 - ratio) * (track_height - g_slider_knob_w));    int knob_x = base_x - (g_slider_knob_w/2) + (g_vslider_width_px/2);    ObjectSetInteger(main_chart_id, knob_name, OBJPROP_XDISTANCE, knob_x);    ObjectSetInteger(main_chart_id, knob_name, OBJPROP_YDISTANCE, knob_y);    ObjectSetInteger(main_chart_id, knob_name, OBJPROP_XSIZE, g_slider_knob_w);    ObjectSetInteger(main_chart_id, knob_name, OBJPROP_YSIZE, g_slider_knob_w);    ObjectSetString(main_chart_id, knob_name, OBJPROP_TEXT, "");    ObjectSetInteger(main_chart_id, knob_name, OBJPROP_SELECTABLE, true);    ObjectSetInteger(main_chart_id, knob_name, OBJPROP_HIDDEN, false);    ObjectSetInteger(main_chart_id, knob_name, OBJPROP_FONTSIZE, 1);    // store slider params    g_slider_min[id] = min_val;    g_slider_max[id] = max_val;    g_slider_left_x[id] = base_x;    g_slider_top_y[id] = base_y;   } 

8) Slider label update—keep UI in sync

UpdateLabelsAfterSliderChange() updates the text labels below the sliders whenever the value changes, keeping visual information consistent.

void UpdateLabelsAfterSliderChange()   {    if(ObjectFind(main_chart_id, lbl_major_width) >= 0)       ObjectSetString(main_chart_id, lbl_major_width, OBJPROP_TEXT, StringFormat("Maj W:%d", g_WidthMajor));           if(ObjectFind(main_chart_id, lbl_refresh_label) >= 0)       ObjectSetString(main_chart_id, lbl_refresh_label, OBJPROP_TEXT, StringFormat("Refresh:%ds", g_RefreshSec));          } 

9) TF dropdown logic—small, reliable popup lists

ShowTFDropdownFor() creates a stack of small buttons immediately under the requested TF button (major / minor1 / minor2). HideTFDropdown() removes them. The dropdown uses a composed prefix so selection events can be parsed easily in OnChartEvent.

This is intentionally simple and robust—using chart buttons avoids building a full custom combobox class while providing expected dropdown behavior.

void ShowTFDropdownFor(int target)   {    if(g_tf_dropdown_visible && g_tf_dropdown_target == target) { HideTFDropdown(); return; }    if(g_tf_dropdown_visible) HideTFDropdown();    string target_btn = (target == 0 ? btn_major_tf : (target == 1 ? btn_minor1_tf : btn_minor2_tf));    if(ObjectFind(main_chart_id, target_btn) < 0) return;    int bx = (int)ObjectGetInteger(main_chart_id, target_btn, OBJPROP_XDISTANCE);    int by = (int)ObjectGetInteger(main_chart_id, target_btn, OBJPROP_YDISTANCE);    int bh = (int)ObjectGetInteger(main_chart_id, target_btn, OBJPROP_YSIZE);    int base_x = bx;    int base_y = by + bh + 4;    int w = 60;    int h = 20;    for(int i=0;i<ArraySize(tf_list);i++)      {       string oname = TF_DROPDOWN_PREFIX + IntegerToString(target) + "_" + IntegerToString(i);       CreateButton(oname, base_x, base_y + i*(h+2), w, h, TFToString(tf_list[i]));      }    g_tf_dropdown_visible = true;    g_tf_dropdown_target = target;   } void HideTFDropdown()   {    if(!g_tf_dropdown_visible) return;    for(int i=0; i<ArraySize(tf_list); i++)      {       string oname = TF_DROPDOWN_PREFIX + IntegerToString(g_tf_dropdown_target) + "_" + IntegerToString(i);       ObjectDelete(main_chart_id, oname);      }    g_tf_dropdown_visible = false;    g_tf_dropdown_target = -1;   } 

10) OnChartEvent()—event model & drag handling

This is the interaction brain. It handles:

  • Starting and stopping knob drag (knob clicks begin drag; any subsequent object click ends it).
  • Mouse movement while dragging: compute knob pixel position, map to numeric value, update g_ variable, and call RefreshLines() if changed.
  • Button clicks: toggles, lookback +/- buttons, color picks, TF dropdown show/hide.
  • TF dropdown selections: parse the clicked button name and apply the chosen timeframe to major/minor accordingly, then redraw.
  • Design choices: stopping drag on any object click is a simple cross-platform pattern that avoids needing to detect global mouse button up events (which aren’t always available as a dedicated chart event in MQL5).

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)   {    // stop dragging if an object click happens    if(id == CHARTEVENT_OBJECT_CLICK && g_slider_drag)      {       g_slider_drag = false;       g_current_slider = -1;       return;      }    // dragging motion: use mouse Y from dparam    if(id == CHARTEVENT_MOUSE_MOVE && g_slider_drag && g_current_slider >= 0)      {       int my = (int)dparam;       int s = g_current_slider;       string track_name = g_slider_track_names[s];       string knob_name  = g_slider_knob_names[s];       int track_top = (int)ObjectGetInteger(main_chart_id, track_name, OBJPROP_YDISTANCE);       int track_h   = (int)ObjectGetInteger(main_chart_id, track_name, OBJPROP_YSIZE);       int track_bottom = track_top + track_h - g_slider_knob_w;       int new_knob_y = my;       if(new_knob_y < track_top) new_knob_y = track_top;       if(new_knob_y > track_bottom) new_knob_y = track_bottom;       ObjectSetInteger(main_chart_id, knob_name, OBJPROP_YDISTANCE, new_knob_y);       double ratio = 1.0 - double(new_knob_y - track_top) / double(track_h - g_slider_knob_w);       int new_val = g_slider_min[s] + (int)MathRound(ratio * (g_slider_max[s] - g_slider_min[s]));       if(new_val < g_slider_min[s]) new_val = g_slider_min[s];       if(new_val > g_slider_max[s]) new_val = g_slider_max[s];       bool changed = false;       if(s == SLIDER_MAJ_WIDTH)         { if(g_WidthMajor != new_val) { g_WidthMajor = new_val; changed = true; } }       else if(s == SLIDER_REFRESH)         { if(g_RefreshSec != new_val) { g_RefreshSec = new_val; EventSetTimer(g_RefreshSec); changed = true; } }       if(changed) { UpdateLabelsAfterSliderChange(); RefreshLines(); }       return;      }    // TF dropdown option clicked    if(StringFind(sparam, TF_DROPDOWN_PREFIX) == 0 && id == CHARTEVENT_OBJECT_CLICK)      {       string rest = StringSubstr(sparam, StringLen(TF_DROPDOWN_PREFIX));       int sep = StringFind(rest, "_");       if(sep >= 0)         {          int target = (int)StringToInteger(StringSubstr(rest, 0, sep));          int idx    = (int)StringToInteger(StringSubstr(rest, sep+1));          if(idx >= 0 && idx < ArraySize(tf_list))            {             ENUM_TIMEFRAMES chosen = tf_list[idx];             if(target == 0) { g_HigherTF = chosen; g_last_major_time = iTime(_Symbol, g_HigherTF, 0); ObjectSetString(main_chart_id, btn_major_tf, OBJPROP_TEXT, TFToString(g_HigherTF)); }             else if(target == 1) { g_Minor1TF = chosen; if(g_ShowMinor1) g_last_minor1_time = iTime(_Symbol, g_Minor1TF, 0); ObjectSetString(main_chart_id, btn_minor1_tf, OBJPROP_TEXT, TFToString(g_Minor1TF)); }             else if(target == 2) { g_Minor2TF = chosen; if(g_ShowMinor2) g_last_minor2_time = iTime(_Symbol, g_Minor2TF, 0); ObjectSetString(main_chart_id, btn_minor2_tf, OBJPROP_TEXT, TFToString(g_Minor2TF)); }             HideTFDropdown();             RefreshLines();            }         }       return;      }    // If dropdown visible and user clicked elsewhere => hide    if(g_tf_dropdown_visible && id == CHARTEVENT_OBJECT_CLICK && StringFind(sparam, TF_DROPDOWN_PREFIX) != 0)      {       HideTFDropdown();      }    // Handle other button clicks & knob starts...    if(id == CHARTEVENT_OBJECT_CLICK)      {       string obj = sparam;       if(obj == btn_major_tf) { ShowTFDropdownFor(0); return; }       if(obj == btn_lookback_minus) { g_Lookback = MathMax(10, g_Lookback - 10); ObjectSetString(main_chart_id, lbl_lookback, OBJPROP_TEXT, StringFormat("Lookback:%d", g_Lookback)); RefreshLines(); return; }       if(obj == btn_lookback_plus)  { g_Lookback += 10; ObjectSetString(main_chart_id, lbl_lookback, OBJPROP_TEXT, StringFormat("Lookback:%d", g_Lookback)); RefreshLines(); return; }       if(obj == btn_toggle_openclose) { g_ShowOpenClose = !g_ShowOpenClose; ObjectSetString(main_chart_id, btn_toggle_openclose, OBJPROP_TEXT, g_ShowOpenClose ? "Open/Close: ON" : "Open/Close: OFF"); RefreshLines(); return; }       if(obj == btn_toggle_fill)      { g_ShowFill = !g_ShowFill; ObjectSetString(main_chart_id, btn_toggle_fill, OBJPROP_TEXT, g_ShowFill ? "Fill: ON" : "Fill: OFF"); RefreshLines(); return; }       // major colors       if(obj == btn_major_col1) { g_ColorMajor = major_colors[0]; RefreshLines(); return; }       // ... other colors       // minors toggles / tf dropdown       if(obj == btn_minor1_toggle) { g_ShowMinor1 = !g_ShowMinor1; if(g_ShowMinor1) g_last_minor1_time = iTime(_Symbol, g_Minor1TF, 0); ObjectSetString(main_chart_id, btn_minor1_toggle, OBJPROP_TEXT, g_ShowMinor1 ? "Min1: ON" : "Min1: OFF"); RefreshLines(); return; }       if(obj == btn_minor1_tf)     { ShowTFDropdownFor(1); return; }       if(obj == btn_minor2_toggle) { g_ShowMinor2 = !g_ShowMinor2; if(g_ShowMinor2) g_last_minor2_time = iTime(_Symbol, g_Minor2TF, 0); ObjectSetString(main_chart_id, btn_minor2_toggle, OBJPROP_TEXT, g_ShowMinor2 ? "Min2: ON" : "Min2: OFF"); RefreshLines(); return; }       if(obj == btn_minor2_tf)     { ShowTFDropdownFor(2); return; }       if(obj == btn_clear_all) { DeleteAllHTFLines(); return; }       // slider knob clicked -> begin dragging       for(int s=0; s<SLIDER_COUNT; s++)         { if(obj == g_slider_knob_names[s]) { g_current_slider = s; g_slider_drag = true; return; } }      }   } 

11) OnTick() and OnTimer()—efficient refresh triggers

OnTick() checks the latest iTime for the configured higher and minor timeframes and sets need_refresh only when a new bar appears—this prevents unnecessary object churn. OnTimer() simply calls RefreshLines() at interval g_RefreshSec (which the slider can modify live).

Quick but not wasteful updates; UI-driven changes (e.g., toggling fill) will call RefreshLines() immediately, while periodic checks handle cases where new HTF bars appear.

void OnTimer()   {    RefreshLines();   } void OnTick()   {    bool need_refresh = false;    datetime curr;    curr = iTime(_Symbol, g_HigherTF, 0);    if(curr != g_last_major_time && curr != 0) { g_last_major_time = curr; need_refresh = true; }    if(g_ShowMinor1)      {       curr = iTime(_Symbol, g_Minor1TF, 0);       if(curr != g_last_minor1_time && curr != 0) { g_last_minor1_time = curr; need_refresh = true; }      }    if(g_ShowMinor2)      {       curr = iTime(_Symbol, g_Minor2TF, 0);       if(curr != g_last_minor2_time && curr != 0) { g_last_minor2_time = curr; need_refresh = true; }      }    if(need_refresh) RefreshLines();   } 

12) RefreshLines()—core drawing & garbage collection

This is the main routine. It:

  • Copies HTF times, opens and closes for the requested lookback.
  • Reverses arrays to ascending order for simple interval comparisons.
  • For each major time, creates or updates vertical line, optional fill rectangle, open/close horizontals and labels.
  • For minors, calls DrawMinorsBetweenIntervals() which draws minor verticals only if they strictly fall between consecutive majors (plus current ongoing interval).
  • Builds keepNames[] listing all desired HTF objects; at the end it iterates through all chart objects and deletes any HTF objects not in keepNames[] (garbage collection).

 Every pass we update properties of existing objects (color, width etc.) so UI changes take effect immediately without needing to delete/recreate everything.

void RefreshLines()   {    datetime major_times[]; ArrayFree(major_times);    double major_opens[]; ArrayFree(major_opens);    double major_closes[]; ArrayFree(major_closes);    int copiedMaj = CopyTime(_Symbol, g_HigherTF, 0, g_Lookback, major_times);    if(copiedMaj <= 0) { PrintFormat("RefreshLines: CopyTime majors returned %d for %s", copiedMaj, TFToString(g_HigherTF)); return; }    if(CopyOpen(_Symbol, g_HigherTF, 0, copiedMaj, major_opens) != copiedMaj) { Print("RefreshLines: CopyOpen failed"); return; }    if(CopyClose(_Symbol, g_HigherTF, 0, copiedMaj, major_closes) != copiedMaj) { Print("RefreshLines: CopyClose failed"); return; }    // reverse to ascending order (oldest first)    int n = copiedMaj;    datetime sorted_times[]; ArrayResize(sorted_times, n);    double sorted_opens[]; ArrayResize(sorted_opens, n);    double sorted_closes[]; ArrayResize(sorted_closes, n);    for(int k = 0; k < n; k++)      {       sorted_times[k]  = major_times[n - 1 - k];       sorted_opens[k]  = major_opens[n - 1 - k];       sorted_closes[k] = major_closes[n - 1 - k];      }    string keepNames[]; ArrayResize(keepNames, 0);    // create/update major objects    for(int i = 0; i < n; ++i)      {       datetime t = sorted_times[i];       double p_open = sorted_opens[i];       double p_close = sorted_closes[i];       // Major vertical       string name = PREFIX_MAJ + TFToString(g_HigherTF) + "_" + IntegerToString((int)t);       if(ObjectFind(0, name) == -1)         {          double dummy_price = 0.0;          if(!ObjectCreate(0, name, OBJ_VLINE, 0, t, dummy_price))             PrintFormat("Failed to create major %s error %d", name, GetLastError());         }       // update props each pass so UI changes apply immediately       ObjectSetInteger(0, name, OBJPROP_COLOR, g_ColorMajor);       ObjectSetInteger(0, name, OBJPROP_WIDTH, g_WidthMajor);       ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_SOLID);       ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);       ObjectSetInteger(0, name, OBJPROP_HIDDEN, false);       ObjectSetInteger(0, name, OBJPROP_BACK, true); // draw in background       int sz = ArraySize(keepNames); ArrayResize(keepNames, sz + 1); keepNames[sz] = name;       // Optional fill / open-close code follows (omitted for brevity)      }    if(n >= 2)      {       if(g_ShowMinor1) DrawMinorsBetweenIntervals(PREFIX_MIN1, g_Minor1TF, g_ColorMin1, g_WidthMin1, sorted_times, keepNames);       if(g_ShowMinor2) DrawMinorsBetweenIntervals(PREFIX_MIN2, g_Minor2TF, g_ColorMin2, g_WidthMin2, sorted_times, keepNames);      }    // cleanup old HTF objects not in keepNames    int total = ObjectsTotal(0);    for(int idx = total - 1; idx >= 0; --idx)      {       string oname = ObjectName(0, idx);       if(StringFind(oname, "HTF_") != -1)         {          bool found = false;          for(int k=0;k<ArraySize(keepNames);k++) if(oname == keepNames[k]) { found = true; break; }          if(!found) ObjectDelete(0, oname);         }      }    UpdateLabelsAfterSliderChange();    ChartRedraw(main_chart_id);   } 

13) DrawMinorsBetweenIntervals()—precise minor placement logic

This function computes how many minor bars to request (based on the timespan between the first major and now), copies minor times, reverses to ascending order, and places vertical minor lines only when a minor time falls strictly between consecutive major times—this mirrors the indicator behavior. It also treats minors that fall into the current ongoing major interval (after the last major time).

Design detail: we add a margin (+20) to approx_bars to be robust against alignment; we also at least request g_Lookback bars.

void DrawMinorsBetweenIntervals(const string prefix,                                 const ENUM_TIMEFRAMES minorTF,                                 const color c,                                 const int width,                                 const datetime &major_times[],                                 string &keepNames[])   {    datetime current_minor_time = iTime(_Symbol, minorTF, 0);    if(current_minor_time == 0) return;    int time_span = (int)(current_minor_time - major_times[0]);    int minor_sec = PeriodSeconds(minorTF);    int approx_bars = (time_span / minor_sec) + 20;    if(approx_bars < g_Lookback) approx_bars = g_Lookback;    datetime minor_times[]; ArrayFree(minor_times);    int copiedMin = CopyTime(_Symbol, minorTF, 0, approx_bars, minor_times);    PrintFormat("DrawMinorsBetweenIntervals: TF=%s copiedMin=%d majors=%d", TFToString(minorTF), copiedMin, ArraySize(major_times));    if(copiedMin <= 0) return;    datetime sorted_minor_times[]; ArrayResize(sorted_minor_times, copiedMin);    for(int k = 0; k < copiedMin; k++) sorted_minor_times[k] = minor_times[copiedMin - 1 - k];    int num_maj = ArraySize(major_times);    for(int m = 0; m < copiedMin; ++m)      {       datetime mt = sorted_minor_times[m];       bool equals_major = false;       for(int kk = 0; kk < num_maj; ++kk) if(major_times[kk] == mt) { equals_major = true; break; }       if(equals_major) continue;       bool placed = false;       for(int j = 0; j < num_maj - 1; ++j)         {          if( major_times[j] < mt && mt < major_times[j+1] )            {             string name = prefix + TFToString(minorTF) + "_" + IntegerToString((int)mt);             if(ObjectFind(0, name) == -1)               {                double dummy_price = 0.0;                if(!ObjectCreate(0, name, OBJ_VLINE, 0, mt, dummy_price))                  PrintFormat("Failed to create minor %s error %d", name, GetLastError());                else                  {                   ObjectSetInteger(0, name, OBJPROP_COLOR, c);                   ObjectSetInteger(0, name, OBJPROP_WIDTH, width);                   ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_DOT);                   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);                   ObjectSetInteger(0, name, OBJPROP_HIDDEN, false);                   ObjectSetInteger(0, name, OBJPROP_BACK, true);                  }               }             int sz = ArraySize(keepNames); ArrayResize(keepNames, sz + 1); keepNames[sz] = name;             placed = true;             break;            }         }       if(!placed && mt > major_times[num_maj - 1])         {          // create a minor in the ongoing major interval          string name = prefix + TFToString(minorTF) + "_" + IntegerToString((int)mt);          if(ObjectFind(0, name) == -1)            {             double dummy_price = 0.0;             if(!ObjectCreate(0, name, OBJ_VLINE, 0, mt, dummy_price))               PrintFormat("Failed to create minor (current) %s error %d", name, GetLastError());             else               {                ObjectSetInteger(0, name, OBJPROP_COLOR, c);                ObjectSetInteger(0, name, OBJPROP_WIDTH, width);                ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_DOT);                ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);                ObjectSetInteger(0, name, OBJPROP_HIDDEN, false);                ObjectSetInteger(0, name, OBJPROP_BACK, true);               }            }          int sz = ArraySize(keepNames); ArrayResize(keepNames, sz + 1); keepNames[sz] = name;          placed = true;         }      }    ChartRedraw(main_chart_id);   } 

14) Deletion & cleanup—DeleteAllHTFLines() and OnDeinit()

DeleteAllHTFLines() removes only HTF objects; OnDeinit() removes UI objects, and slider components, hides dropdowns and destroys the canvas. We intentionally do not delete HTF objects by default on deinit (except when Clear HTF is pressed), so the user can keep drawings if they prefer.

void DeleteAllHTFLines()   {    int total = ObjectsTotal(0);    for(int idx = total - 1; idx >= 0; --idx)      {       string oname = ObjectName(0, idx);       if(StringFind(oname, "HTF_") != -1)          ObjectDelete(0, oname);      }   } void OnDeinit(const int reason)   {    EventKillTimer();    string names[] = {      lbl_title, btn_major_tf, lbl_major_tf, btn_lookback_minus, lbl_lookback, btn_lookback_plus,      btn_toggle_openclose, btn_toggle_fill, btn_major_col1, btn_major_col2, btn_major_col3, btn_major_col4,      btn_minor1_toggle, btn_minor1_tf, btn_minor2_toggle, btn_minor2_tf,      btn_clear_all, lbl_major_width, lbl_refresh_label    };    for(int i=0;i<ArraySize(names);i++) if(StringLen(names[i])>0) ObjectDelete(main_chart_id, names[i]);    for(int s=0; s<SLIDER_COUNT; s++)      {       if(StringLen(g_slider_track_names[s])>0) ObjectDelete(main_chart_id, g_slider_track_names[s]);       if(StringLen(g_slider_knob_names[s])>0)  ObjectDelete(main_chart_id, g_slider_knob_names[s]);      }    if(g_tf_dropdown_visible) HideTFDropdown();    // destroy canvas    g_bgCanvas.Destroy();   } 


Testing

After a successful compilation, it was time to test the Expert Advisor on a live MetaTrader 5 chart. One important note is that the CCanvas class is part of the MQL5 Standard Library, so it’s essential to define the include path correctly to avoid compilation errors. Once the EA was attached to the chart, it initialized smoothly, and the control dashboard appeared as expected.

The interface rendered neatly, with all switches, sliders, and labels properly aligned on the canvas background. During testing, each control responded in real time—the sliders dynamically adjusted visual parameters such as width and refresh rate, while the switches instantly toggled visibility and fill features. The result was a highly responsive, interactive experience that brought the concept of real-time parameter control to life.

Below is an image illustrating the successful deployment and the EA’s active control processes, confirming the design’s stability and precision in execution.

Fig. 3. Testing the controls on a live chart

One particularly interesting outcome of this project was observing the real-time body fill effect of higher-timeframe candles forming directly on the lower-timeframe chart. Unlike the earlier version, which required reinitializing the indicator to update visuals, this implementation allows traders to watch higher-timeframe structures evolve dynamically. It offers a clear and continuous view of how each lower-timeframe movement contributes to the formation of a higher-timeframe bar, providing more in-depth insight into market structure and momentum as it develops.


Conclusion

This exploration has been both a technical and creative milestone in our continuing journey with the MQL5 language. What began as a simple curiosity about improving chart interaction evolved into a full-fledged system that redefines how we approach input tuning and visualization. Through this development, we have learned that MQL5 is not only a language for trading automation but also a canvas for user experience design—one that empowers us to transform static input parameters into dynamic, responsive, real-time control environments.

By harnessing object manipulation and graphical interfaces through Expert Advisors, we demonstrated that it is entirely possible to control visual and logical components of a system directly from the chart. This breakthrough bridges the gap between analysis and interaction, giving traders and developers a new dimension of speed, precision, and creativity. With real-time control, testing multiple settings, timeframes, and visualization modes becomes fluid—helping users make decisions faster while maintaining context.

The Market Periods Synchronizer Control Utility takes this philosophy further by enabling traders to observe how smaller timeframe behaviors influence higher timeframe structures. It brings depth to multi-timeframe analysis, allowing users to study the heartbeat of the market across different scales and better understand how minor price fluctuations contribute to the overall form of major candles. In essence, it encourages a more scientific approach to price-action interpretation—seeing not just what happened, but why and how it formed.

However, this is just the beginning. The ideas explored here can be expanded far beyond the scope of this utility. Future improvements could include custom chart controls, multi-symbol synchronization, data export for analytics, or AI-driven visualization tuning. The beauty of MQL5 lies in its openness—it rewards experimentation, creativity, and the willingness to explore the unconventional.

Go ahead, experiment with this idea in your own projects. Adapt it, modify it, integrate it with your existing tools—and discover what new frontiers it might unlock for your trading systems. Your feedback, ideas, and contributions are invaluable, so please share your thoughts and suggestions in the comments below. Together, we can continue making the MQL5 community a more vibrant, innovative, and collaborative space for learning and growth.

Finally, find a summary table of key takeaways from this exploration below, along with the attached source files for you to download, study, and extend. Remember—the best way to master something is to build upon it. Experiment boldly, study deeply, and let your work inspire others.


Key Lessons

LessonDescription
1. Real-time Control is Possible.Through structured event handling and object updates, it is possible to create interfaces that react instantly to user input, allowing immediate feedback and visual updates on the chart.
2. CCanvas Unlocks Visual CreativityThe Canvas class provides the ability to design professional and visually appealing dashboards. It enables background painting, transparency, and layering to improve user interaction.
3. UI Responsiveness Depends on Object Management.Properly handling creation, updating, and deletion of chart objects ensures smooth performance and prevents clutter or lag during real-time updates.
4. Dynamic Parameters Improve Experimentation:Allowing traders to adjust settings such as timeframes, colors, and widths without reopening the properties window, accelerates experimentation and analysis efficiency.
5. Multi-Timeframe Synchronization Adds Depth.Combining higher and lower timeframe data visually helps traders understand internal market structure, identify micro-movements, and link smaller trends to larger formations.
6. Event-driven Design is Central to Interactive Tools.Building interactive systems requires a strong grasp of chart event handling. Each user action must be mapped to a specific program response for intuitive control behavior.
7. Object Layering Enhances Usability.By carefully setting background and foreground properties, one can ensure the interface remains responsive and objects never block essential user interactions.
8. Modular Code Increases Maintainability.Breaking the project into logical sections—such as UI creation, event handling, and drawing functions—makes the system easier to expand, debug, and reuse in future projects.
9. Testing and Debugging Visualization Tools.Real-time visual testing revealed issues like non-updating objects and overlap conflicts, teaching us systematic debugging methods using logs and event tracking.
10. Optimizing Refresh and Drawing Intervals.We learned how adjusting refresh timers and redraw strategies can improve performance and responsiveness, especially for real-time chart updates.
11. Visual Feedback Improves Analytical Confidence.Real-time visual adjustments help traders observe cause and effect instantly, building confidence in the accuracy and interpretation of chart data.
12. Experimentation Drives Innovation.This project proves that exploring unconventional approaches in MQL5 leads to new tools and ideas. Creativity combined with technical understanding produces progress for the entire community.


Attachments

FilenameVersionDescription
MarketPeriodsSynchronizer_EA.mq5
1.00This Expert Advisor provides a real-time control dashboard for synchronizing and visualizing multiple timeframe periods directly on the chart. It expands the functionality of the original Market Periods Synchronizer indicator by integrating interactive controls, sliders, and switches for instant parameter adjustment without reopening input settings.
Next article >>