preview
Developing a Quantitative Session Analysis Tool (Part 1): Building a Data-Driven View of Market Sessions

Developing a Quantitative Session Analysis Tool (Part 1): Building a Data-Driven View of Market Sessions

MetaTrader 5 — Examples |
190 0
Francis Nyoike Thumbi
Francis Nyoike Thumbi

Introduction

Market sessions provide a natural way to divide the trading day into distinct periods, yet their behavior is often reduced to simple visual markers on a chart. A session may have a defined opening and closing time, but its range, direction, and the order in which its high and low develop can vary considerably from one occurrence to another.

This raises a more useful question: can a trading session be represented as structured data rather than simply a box on the chart?

In this article, we begin developing a quantitative session analysis tool in MQL5. The goal is not to predict session behavior, but to establish a foundation for measuring it consistently. Each session will be converted into a structured record containing its price characteristics and selected measurements of how its range develops over time.

The resulting data can then be presented through chart markers, a comparison panel, and an interactive session inspector, allowing the same information to be viewed from both a broader and a more detailed perspective.

What We Will Build

Throughout this article, we will develop the foundation in several stages:

  • Define a structured representation for session configuration and session data.
  • Identify the price bars belonging to each session.
  • Calculate the session's open, close, high, low, range, and net movement.
  • Record the order in which the session high and low occurred.
  • Measure range development at 25%, 50%, 75%, and 100% of the session duration.
  • Present the resulting measurements on the chart.
  • Add an interactive inspector for examining individual sessions.


Defining the Session Data Model

Before calculating anything, the tool needs a consistent representation of what a session is and what information should be retained.

Two structures are used for this purpose. SessionConfig stores the definition of each session, while SessionData stores the measurements produced from its price data.

SessionConfig contains the session name, starting time, and display color.

struct SessionConfig
  {
   string            name;                      // Identifier name for the trading session
   int               start_hour;                // Opening hour in 24-hour server time (0-23)
   int               start_min;                 // Opening minute (0-59)
   color             clr;                       // Visual display/highlight color for the session
  };

SessionData represents an individual occurrence of a session. Alongside its boundaries and OHLC values, it stores the session range, net movement, timestamps of the high and low, their order of occurrence, and four measurements describing range development.

struct SessionData
  {
   //--- Identification & Display
   string            name;                       // Session identifier (e.g., "Asian", "London", "NY")
   datetime          start_time;                 // Session opening timestamp
   datetime          end_time;                   // Session closing timestamp
   color             clr;                        // Visual display color for charts/boxes

   //--- OHLC Price Levels
   double            open;                       // Opening price of the session
   double            close;                      // Closing (or current latest) price of the session
   double            high;                       // Highest price reached during the session
   double            low;                        // Lowest price reached during the session

   //--- Extremum Timestamps
   datetime          high_time;                  // Bar timestamp containing the session high
   datetime          low_time;                   // Bar timestamp containing the session low

   //--- Volatility & Directional Metrics
   double            range;                      // Total amplitude (High - Low)
   double            net_move;                   // Net directional displacement (Close - Open)

   //--- Sequence & Price Action Order
   int               high_first;                 // 1 = high first, 0 = low first, -1 = same-bar order unknown

   //--- Cumulative Range Development
   double            range_25;                   // Cumulative range at 25% of configured session duration
   double            range_50;                   // Cumulative range at 50% of configured session duration
   double            range_75;                   // Cumulative range at 75% of configured session duration
   double            range_100;                  // Cumulative range across available session bars

   //--- State Tracking
   bool              completed;                  // Flag indicating whether the session has finalized
  };

This separation keeps the session definition independent from the observations collected from each occurrence. The configuration tells the analyzer when and where a session exists, while the data structure records what happened during it.


Global Engine State

The data structures define the session metrics. The indicator also needs persistent state to maintain runtime context. Event handlers like OnCalculate and OnChartEvent run independently. A centralized global state preserves configuration, stores session history, debounces chart clicks, and tracks the UI inspection state. The global state acts as the central memory across execution cycles.

//--- Global Engine State
SessionConfig g_configs[3];                       // Fixed definitions for the 3 active market sessions
int           g_end_hour               = 17;      // Final session close hour in server time (24h format)
int           g_end_min                = 0;       // Final session close minute
SessionData   g_sessions[];                       // Dynamic history storage for all identified session records
int           g_selectedSessionIdx     = -1;      // Array index of the currently clicked/inspected session (-1 = none)
ulong         g_lastObjectClickTime    = 0;       // Millisecond timestamp of the last chart click to prevent event spam
bool          g_isInspectingHistory    = false;   // State flag indicating if historical review mode is active
datetime      g_lastProcessedBarTime   = 0;       // Open time of the latest bar used to throttle visual marker updates
datetime      g_selectedSessionTime     = 0;      // Stable start time of the currently inspected session

These variables serve specific roles across the lifecycle of the engine:

  • Session Definitions(g_configs, g_end_hour, g_end_min): Store the timing boundaries and display attributes for the three core sessions alongside the terminal daily closing time.
  • Persistent History(g_sessions[]): Stores the session metrics reconstructed for the configured history window, feeding the dashboard and the session inspector.
  • Interactive UI State(g_selectedSessionIdx, g_lastObjectClickTime, g_isInspectingHistory): Preserves user chart selections, debounces click events to eliminate double-triggering, and tracks whether the chart has been scrolled into history so that auto-follow can be restored at the live edge.
  • Visual Update Throttling ( g_lastProcessedBarTime ): Tracks the arrival of new bars so that session marker updates are not repeated on every incoming tick.


Preparing Session Time Boundaries

The analyzer receives session times as strings such as "00:00" and "08:00". Before they can be used to locate price data, these values must be converted into separate hours and minutes components.

The ParseTimeString() helper performs this conversion and provides the resulting values to the session configuration.

//+------------------------------------------------------------------+
//| Parse Time String Helper (HH:MM)                                 |
//+------------------------------------------------------------------+
bool ParseTimeString(string time_str, int &hour, int &min)
  {
   string parts[];
   if(StringSplit(time_str, ':', parts) >= 2)
     {
      hour = (int)StringToInteger(parts[0]);
      min  = (int)StringToInteger(parts[1]);
      if(hour < 0 || hour > 23 || min < 0 || min > 59)
         return(false);
      return(true);
     }
   hour = 0;
   min  = 0;
   return(false);
  }

During initialization, the three configured sessions and the common session end time are parsed and stored in the global configuration arrays.

//+------------------------------------------------------------------+
//| Custom Indicator Initialization Function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Initialize Session Configurations
   g_configs[0].name = InpSession1Name;
   g_configs[0].clr  = InpSession1Color;
   if(!ParseTimeString(InpSession1Start, g_configs[0].start_hour, g_configs[0].start_min))
      return(INIT_PARAMETERS_INCORRECT);
   g_configs[1].name = InpSession2Name;
   g_configs[1].clr  = InpSession2Color;
   if(!ParseTimeString(InpSession2Start, g_configs[1].start_hour, g_configs[1].start_min))
      return(INIT_PARAMETERS_INCORRECT);

   g_configs[2].name = InpSession3Name;
   g_configs[2].clr  = InpSession3Color;
   if(!ParseTimeString(InpSession3Start, g_configs[2].start_hour, g_configs[2].start_min))
      return(INIT_PARAMETERS_INCORRECT);

   if(!ParseTimeString(InpSessionEnd, g_end_hour, g_end_min))
      return(INIT_PARAMETERS_INCORRECT);

   int t1 = g_configs[0].start_hour * 60 + g_configs[0].start_min;
   int t2 = g_configs[1].start_hour * 60 + g_configs[1].start_min;
   int t3 = g_configs[2].start_hour * 60 + g_configs[2].start_min;
   int te = g_end_hour * 60 + g_end_min;
   if(!(t1 < t2 && t2 < t3 && t3 < te))
     {

      Print("Invalid session configuration. Part 1 requires ordered same-day session times.");
      return(INIT_PARAMETERS_INCORRECT);
     }

   g_selectedSessionIdx   = -1;
   g_selectedSessionTime  = 0;
   g_isInspectingHistory  = false;
   g_lastProcessedBarTime = 0;

//--- Enable chart event handling
   ChartSetInteger(0, CHART_EVENT_OBJECT_CREATE, true);
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false);

   return(INIT_SUCCEEDED);
  }

Once initialized, the analyzer can construct concrete datetime boundaries for each trading day. This gives the data engine fixed intervals against which the available price bars can be evaluated.


Building the Session Dataset

With the session boundaries defined, the next step is to locate the corresponding price bars and turn them into session records.

BuildSessionData() rebuilds the dataset for the configured history window: it clears the previous array, iterates days, builds boundaries, collects bars, and creates SessionData records. For each session, the collected bar indices are stored temporarily. The oldest bar within the interval provides the opening price, while the newest available bar provides the current or final close value. These values are therefore bar-based measurements from the selected chart timeframe rather than reconstructed tick-level prices at the exact session boundaries. Bars are included by their opening timestamps, so on higher timeframes a boundary bar may contain price movement extending beyond the configured session interval. A new SessionData record is then created and populated with the session's basic configuration and state.

The completed field is determined by comparing the current trade server time (TimeCurrent()) with the session's end time. This allows the same data structure to represent both completed historical sessions and sessions that are still developing.

//+------------------------------------------------------------------+
//| Session Builder and Quantitative Range Development Engine        |
//+------------------------------------------------------------------+
void BuildSessionData(const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const int total_rates)
  {
   ArrayFree(g_sessions);

   datetime current_time = TimeCurrent();
   datetime oldest_time  = current_time - (InpHistoryDays * 86400);

   SessionData temp_sessions[];
   int count = 0;

//--- Scan days within history window
   for(int d = InpHistoryDays; d >= 0; d--)
     {
      datetime day_base = current_time - (d * 86400);
      MqlDateTime dt;
      TimeToStruct(day_base, dt);
      dt.hour = 0;
      dt.min  = 0;
      dt.sec  = 0;
      datetime day_start = StructToTime(dt);

      //--- Build day's defined boundary times
      datetime s1_start = day_start + (g_configs[0].start_hour * 3600) + (g_configs[0].start_min * 60);
      datetime s2_start = day_start + (g_configs[1].start_hour * 3600) + (g_configs[1].start_min * 60);
      datetime s3_start = day_start + (g_configs[2].start_hour * 3600) + (g_configs[2].start_min * 60);
      datetime s_end    = day_start + (g_end_hour * 3600) + (g_end_min * 60);

      datetime starts[3] = {s1_start, s2_start, s3_start};
      datetime ends[3]   = {s2_start, s3_start, s_end};

      for(int s = 0; s < 3; s++)
        {
         datetime s_st = starts[s];
         datetime s_et = ends[s];

         if(s_st < oldest_time || s_st > current_time)
            continue;

         //--- Collect all bar indices within this specific session
         int bar_indices[];
         int bar_count = 0;

         for(int b = total_rates - 1; b >= 0; b--)
           {
            if(time[b] >= s_st && time[b] < s_et)
              {
               ArrayResize(bar_indices, bar_count + 1);
               bar_indices[bar_count] = b;
               bar_count++;
              }
           }

         if(bar_count == 0)
            continue;

         //--- Oldest bar inside session represents the opening bar
         int open_bar  = bar_indices[0];
         //--- Newest bar inside session represents current/final close bar
         int close_bar = bar_indices[bar_count - 1];

         ArrayResize(temp_sessions, count + 1);
         temp_sessions[count].name       = g_configs[s].name;
         temp_sessions[count].start_time = s_st;
         temp_sessions[count].end_time   = s_et;
         temp_sessions[count].clr        = g_configs[s].clr;
         temp_sessions[count].open       = open[open_bar];
         temp_sessions[count].close      = close[close_bar];
         temp_sessions[count].completed  = (current_time >= s_et);

At this stage, each detected session has been converted from a time interval into a structured record. The remaining calculations can now operate on these records to describe the price behavior that occurred within each session.


Calculating Session Price Characteristics

Once the bars belonging to a session have been identified, the analyzer can derive its main price characteristics.

The highest and lowest prices are found by scanning the collected bars. Their bar timestamps are stored as well, allowing the tool to determine which extreme appeared first when they occur in different bars.
//--- High / Low and High/Low time derivation
double s_high = -DBL_MAX;
double s_low  = DBL_MAX;
datetime s_high_t = 0;
datetime s_low_t  = 0;

for(int k = 0; k < bar_count; k++)
  {
   int b = bar_indices[k];

   if(high[b] > s_high)
     {
      s_high   = high[b];
      s_high_t = time[b];
     }

   if(low[b] < s_low)
     {
      s_low   = low[b];
      s_low_t = time[b];
     }
  }

temp_sessions[count].high      = s_high;
temp_sessions[count].low       = s_low;
temp_sessions[count].high_time = s_high_t;
temp_sessions[count].low_time  = s_low_t;
temp_sessions[count].range     = (s_high - s_low) / GetPipSize();
temp_sessions[count].net_move  =
   (temp_sessions[count].close - temp_sessions[count].open) / GetPipSize();

if(s_high_t < s_low_t)
   temp_sessions[count].high_first = 1;
else
   if(s_low_t < s_high_t)
      temp_sessions[count].high_first = 0;
   else
      temp_sessions[count].high_first = -1;

The session range is calculated as the distance between the high and low, while net movement measures the difference between the closing and opening prices. Both values are converted to pips through the GetPipSize() helper using the conventional Forex pricing convention for 2/4-digit and 3/5-digit symbols.

//+------------------------------------------------------------------+
//| Pip Size Helper for Conventional FX Pricing                      |
//+------------------------------------------------------------------+
double GetPipSize()
  {
//--- 3-digit (JPY pairs) and 5-digit standard Forex pairs
   if(_Digits == 3 || _Digits == 5)
      return(_Point * 10.0);

//--- 2-digit or 4-digit pricing
   return(_Point);
  }

The resulting fields give each session a compact description of its overall price development:

  • Range — total distance between the session high and low.
  • Net Move — directional change from session open to close.
  • High Time / Low Time — the bar timestamps containing the session extremes.
  • Extreme Order — whether the high or low appeared first or remains unknown when both occur within the same bar.

These measurements form the basic quantitative description of each session.


Measuring Range Development

The total session range describes where the session ended, but it does not show how that range developed along the way. To capture this progression, the analyzer measures the cumulative range at four points in the session: 25%, 50%, 75%, and 100% of its duration.

The configured session duration defines the temporal checkpoints. At each reached checkpoint, the analyzer uses the available session bars up to that time to calculate the high-to-low distance reached so far. For a developing session, only checkpoints that have already been reached display their accumulated ranges. Unreached checkpoints remain unavailable, while Current Range shows the range measured from the session start through the latest available bar. Once the session is completed, this final value is displayed as 100% Duration.
//--- Range Development (25%, 50%, 75%, 100% of duration)
datetime checkpoints[4];
checkpoints[0] = s_st + (datetime)((s_et - s_st) * 0.25);
checkpoints[1] = s_st + (datetime)((s_et - s_st) * 0.50);
checkpoints[2] = s_st + (datetime)((s_et - s_st) * 0.75);
checkpoints[3] = s_et;

int q1 = -1;
int q2 = -1;
int q3 = -1;
int q4 = bar_count - 1;

for(int q = 0; q < bar_count; q++)
  {
   datetime bar_time = time[bar_indices[q]];

   if(bar_time < checkpoints[0])
      q1 = q;
   if(bar_time < checkpoints[1])
      q2 = q;
   if(bar_time < checkpoints[2])
      q3 = q;
  }

temp_sessions[count].range_25 =
   (current_time >= checkpoints[0] && q1 >= 0) ?
   CalculateSegmentRange(q1, bar_count, bar_indices, high, low) : 0.0;

temp_sessions[count].range_50 =
   (current_time >= checkpoints[1] && q2 >= 0) ?
   CalculateSegmentRange(q2, bar_count, bar_indices, high, low) : 0.0;

temp_sessions[count].range_75 =
   (current_time >= checkpoints[2] && q3 >= 0) ?
   CalculateSegmentRange(q3, bar_count, bar_indices, high, low) : 0.0;

temp_sessions[count].range_100 =
   CalculateSegmentRange(q4, bar_count, bar_indices, high, low);

The calculation itself is handled by CalculateSegmentRange() , which receives the resolved bar index for a checkpoint and determines the highest high and lowest low reached up to that point.
//+------------------------------------------------------------------+
//| Calculates cumulative range up to a specific bar index in session|
//+------------------------------------------------------------------+
double CalculateSegmentRange(const int limit_idx, const int bar_count, const int &bar_indices[], const double &high[], const double &low[])
  {
   double q_h = -DBL_MAX;
   double q_l = DBL_MAX;
   for(int q = 0; q <= limit_idx && q < bar_count; q++)
     {
      int b = bar_indices[q];
      if(high[b] > q_h)
         q_h = high[b];
      if(low[b] < q_l)
         q_l = low[b];
     }
   return (q_h > q_l) ? (q_h - q_l) / GetPipSize() : 0.0;
  }

The result is stored in range_25, range_50, range_75, and range_100. This gives us more than the final session range. We can now observe how the session's range expanded as the session progressed.

Architectural Note: Session Boundaries

This version assumes sequential, same-day session boundaries. Each session starts when the previous one ends, and the last session ends at the common input-defined boundary. Overnight sessions, overlapping intervals, and timezone or DST-aware session conversion are intentionally reserved for future development. The current structure keeps session identification and comparison straightforward while establishing the foundation for those extensions.


Connecting the Data Engine to the Indicator

The session dataset must be refreshed as new market data arrives. The indicator's OnCalculate() function provides the main execution point for this process.

On each calculation, the price arrays are treated as series so that index 0 represents the current bar. The analyzer then rebuilds the session dataset using the latest available data.

//+------------------------------------------------------------------+
//| Custom Indicator Iteration Function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   if(rates_total < 10)
      return(rates_total);

//--- Enforce series indexing: 0 = current live bar, moving backward into history
   ArraySetAsSeries(time, true);
   ArraySetAsSeries(open, true);
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low, true);
   ArraySetAsSeries(close, true);

   bool is_new_bar = (time[0] != g_lastProcessedBarTime);

//--- LAYER 1 & 2: ENGINE PROCESSING — PARSE & CALCULATE SESSIONS (TICK-DRIVEN)
   BuildSessionData(time, open, high, low, close, rates_total);

//--- Revalidate selected session after dataset rebuild
   if(g_selectedSessionTime > 0)
     {
      g_selectedSessionIdx = -1;

      for(int i = 0; i < ArraySize(g_sessions); i++)
        {
         if(g_sessions[i].start_time == g_selectedSessionTime)
           {
            g_selectedSessionIdx = i;
            break;
           }
        }

      if(g_selectedSessionIdx == -1)
        {
         g_selectedSessionTime = 0;
         DeletePanelObjects(Prefix + "SI_");
         SetSessionMarkersVisible(true);
        }
     }

//--- LAYER 3: USER INTERFACE — RECONCILE SESSION MARKERS ON NEW BAR OR INITIAL LOAD
   if(is_new_bar || prev_calculated == 0)
     {
      if(InpShowSessionMarkers || InpShowSessionLabels)
         RenderSessionMarkers();

      g_lastProcessedBarTime = time[0];
     }

//--- Maintain correct marker occlusion according to Inspector open/close state
   SetSessionMarkersVisible(g_selectedSessionIdx == -1);

   if(InpShowDashboard)
      RenderMasterComparisonPanel();

   if(InpEnableInspector && g_selectedSessionIdx >= 0 && g_selectedSessionIdx < ArraySize(g_sessions))
      RenderSessionInspectorPanel(g_sessions[g_selectedSessionIdx]);
   else
      DeletePanelObjects(Prefix + "SI_");

   ChartRedraw(0);
   return(rates_total);
  }

Session markers are only reconciled when a new bar appears or when the indicator is loaded for the first time. The session dataset itself, however, is rebuilt on each calculation using the latest available bar data.

This separates the continuous data processing from the more expensive chart-object updates and keeps the visual layer from being unnecessarily recreated on every tick.


Visualizing the Session Data

The calculated session records now need a visual representation on the chart. Rather than drawing the entire session range, the tool places an interactive marker and label at each session's opening point.

RenderSessionMarkers() creates these objects from the session records already stored in g_sessions.

//+------------------------------------------------------------------+
//| Rendering and Reconciling Session Markers and Interactive Arrows |
//+------------------------------------------------------------------+
void RenderSessionMarkers()
  {
   int total = ArraySize(g_sessions);

   for(int o = ObjectsTotal(0, 0, -1) - 1; o >= 0; o--)
     {

      string object_name = ObjectName(0, o, 0, -1);
      if(StringFind(object_name, Prefix + "PT_") != 0 && StringFind(object_name, Prefix + "TXT_") != 0)
         continue;

      bool keep_object = false;
      for(int j = 0; j < total; j++)
        {

         string identity = g_sessions[j].name + "_" + IntegerToString((long)g_sessions[j].start_time);
         if(object_name == Prefix + "PT_" + identity || object_name == Prefix + "TXT_" + identity)
           {

            keep_object = true;
            break;
           }
        }

      if(!keep_object)
         ObjectDelete(0, object_name);
     }

   for(int i = 0; i < total; i++)
     {
      datetime t = g_sessions[i].start_time;
      double   p = g_sessions[i].open;
      string   time_id = g_sessions[i].name + "_" + IntegerToString((long)t);

      //--- Interactive arrow at session open
      if(InpShowSessionMarkers)
        {
         string arrow_name = Prefix + "PT_" + time_id;
         if(ObjectFind(0, arrow_name) < 0)
           {
            ObjectCreate(0, arrow_name, OBJ_ARROW, 0, t, p);
            ObjectSetInteger(0, arrow_name, OBJPROP_ARROWCODE, 234); // Downward pointing arrow
            ObjectSetInteger(0, arrow_name, OBJPROP_COLOR, g_sessions[i].clr);
            ObjectSetInteger(0, arrow_name, OBJPROP_WIDTH, 2);
            ObjectSetInteger(0, arrow_name, OBJPROP_SELECTABLE, false);
            ObjectSetInteger(0, arrow_name, OBJPROP_BACK, false);
            ObjectSetInteger(0, arrow_name, OBJPROP_ZORDER, 0);
           }
         else
           {
            ObjectMove(0, arrow_name, 0, t, p);
            ObjectSetInteger(0, arrow_name, OBJPROP_COLOR, g_sessions[i].clr);
           }
        }

      //--- Session text label
      if(InpShowSessionLabels)
        {
         string label_name = Prefix + "TXT_" + time_id;
         if(ObjectFind(0, label_name) < 0)
           {
            ObjectCreate(0, label_name, OBJ_TEXT, 0, t, p);
            ObjectSetString(0, label_name, OBJPROP_TEXT, " " + ToUpperSafe(g_sessions[i].name));
            ObjectSetInteger(0, label_name, OBJPROP_COLOR, g_sessions[i].clr);
            ObjectSetInteger(0, label_name, OBJPROP_FONTSIZE, InpFontSize);
            ObjectSetString(0, label_name, OBJPROP_FONT, "Consolas Bold");
            ObjectSetInteger(0, label_name, OBJPROP_ANCHOR, ANCHOR_LOWER);
            ObjectSetInteger(0, label_name, OBJPROP_SELECTABLE, false);
            ObjectSetInteger(0, label_name, OBJPROP_ZORDER, 0);
           }
         else
           {
            ObjectMove(0, label_name, 0, t, p);
            ObjectSetString(0, label_name, OBJPROP_TEXT, " " + ToUpperSafe(g_sessions[i].name));
            ObjectSetInteger(0, label_name, OBJPROP_COLOR, g_sessions[i].clr);
           }
        }
     }
  }

The objects use the session's start time and opening price, so the visual layer remains directly tied to the underlying data. The session name and configured color are also taken from the corresponding record. The marker objects are given unique names using the common Prefix (e.g., "PT_" / "TXT_"), session name, and start time. This makes them easy to identify and manage separately from other chart objects.


Building the Session Comparison Panel

While the markers provide a way to locate sessions, a comparison view is more useful for quickly reviewing their measurements.

The master panel compares the available configured sessions from the latest common trading day together with their range and net movement.

//+------------------------------------------------------------------+
//| Master Comparison Panel                                          |
//+------------------------------------------------------------------+
void RenderMasterComparisonPanel()
  {
   string p = Prefix + "MC_";
   int x = 15;
   int y = 25;
   int width = 230;
   int height = 115;
   int row_h = 18;

   CreateOrUpdateRect(p + "BG", x, y, width, height, InpPanelBackground, InpPanelBorder);
   CreateOrUpdateLabel(p + "Title", x + 10, y + 8, "SESSION COMPARISON", InpPanelTitle, 9, true);

//--- Sub-headers
   CreateOrUpdateLabel(p + "HDR_SESS",  x + 10,  y + 28, "SESSION",  InpLabelColor, 8, true);
   CreateOrUpdateLabel(p + "HDR_RANGE", x + 110, y + 28, "RANGE",    InpLabelColor, 8, true);
   CreateOrUpdateLabel(p + "HDR_NET",   x + 175, y + 28, "NET",      InpLabelColor, 8, true);

   string session_names[3] = {InpSession1Name, InpSession2Name, InpSession3Name};
   int total = ArraySize(g_sessions);
   int curr_y = y + 48;

   datetime comparison_day = 0;
   if(total > 0)
     {

      MqlDateTime comparison_dt;
      TimeToStruct(g_sessions[total - 1].start_time, comparison_dt);
      comparison_dt.hour = 0;
      comparison_dt.min  = 0;
      comparison_dt.sec  = 0;
      comparison_day = StructToTime(comparison_dt);
     }

   for(int s = 0; s < 3; s++)
     {
      //--- Find occurrence of this session from the common comparison day
      int found_idx = -1;
      for(int i = total - 1; i >= 0; i--)
        {
         if(g_sessions[i].name == session_names[s] && g_sessions[i].start_time >= comparison_day && g_sessions[i].start_time < comparison_day + 86400)
           {
            found_idx = i;
            break;
           }
        }

      string r_str = "-";
      string n_str = "-";
      color net_clr = InpValueColor;

      if(found_idx >= 0)
        {
         r_str = StringFormat("%.1f", g_sessions[found_idx].range);
         double net = g_sessions[found_idx].net_move;
         n_str = StringFormat("%+.1f", net);
         net_clr = (net > 0) ? InpPositiveColor : (net < 0) ? InpNegativeColor : InpNeutralColor;
        }

      CreateOrUpdateLabel(p + "ROW_N_" + IntegerToString(s), x + 10, curr_y, session_names[s], InpValueColor, 8, false);
      CreateOrUpdateLabel(p + "ROW_R_" + IntegerToString(s), x + 110, curr_y, r_str, InpValueColor, 8, false);
      CreateOrUpdateLabel(p + "ROW_M_" + IntegerToString(s), x + 175, curr_y, n_str, net_clr, 8, true);

      curr_y += row_h;
     }
  }

The panel does not perform new calculations. It reads the values already produced by the session engine and presents them in a compact format. Net movement is also given a directional color, making positive and negative sessions immediately distinguishable.

This establishes the first level of interaction: compare the latest sessions without leaving the chart.


Adding an Interactive Session Inspector

A comparison is useful for an overview, but individual sessions contain more information than the summary panel can display. The session inspector provides access to the complete SessionData record for a selected session.

When a session marker or label is clicked, OnChartEvent() identifies the corresponding session using its name and start time and assigns its index to g_selectedSessionIdx. The same event handler also manages closing the inspector when the user clicks an empty area of the chart.

//+------------------------------------------------------------------+
//| Chart Event Handling Object Selection and Navigation             |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- 1. Handle Viewport / Scroll Changes for Auto Navigation
   if(id == CHARTEVENT_CHART_CHANGE)
     {
      CheckLiveEdgeState();
      return;
     }

//--- 2. Handle Object Click (User selects a session arrow or label)
   if(id == CHARTEVENT_OBJECT_CLICK)
     {
      if(!InpEnableInspector)
         return;

      if(StringFind(sparam, Prefix + "PT_") == 0 || StringFind(sparam, Prefix + "TXT_") == 0)
        {
         //--- Record time of object click to prevent immediate trigger of CHARTEVENT_CLICK
         g_lastObjectClickTime = GetTickCount();

         string object_prefix = (StringFind(sparam, Prefix + "PT_") == 0) ? Prefix + "PT_" : Prefix + "TXT_";
         string identity = StringSubstr(sparam, StringLen(object_prefix));

         int separator = -1;
         for(int p = StringLen(identity) - 1; p >= 0; p--)
           {
            if(StringSubstr(identity, p, 1) == "_")
              {
               separator = p;
               break;
              }
           }

         if(separator > 0)
           {
            datetime target_time = (datetime)StringToInteger(StringSubstr(identity, separator + 1));
            string target_name = StringSubstr(identity, 0, separator);
            int total_sessions = ArraySize(g_sessions);

            for(int i = 0; i < total_sessions; i++)
              {
               if(g_sessions[i].start_time == target_time && g_sessions[i].name == target_name)
                 {
                  g_selectedSessionIdx = i;
                  g_selectedSessionTime = g_sessions[i].start_time;
                  SetSessionMarkersVisible(false);
                  RenderSessionInspectorPanel(g_sessions[i]);
                  ChartRedraw(0);
                  return;
                 }
              }
           }
        }
     }

//--- 3. Handle Empty Chart Click (User clicks on open space to close panel)
   if(id == CHARTEVENT_CLICK)
     {
      //--- Ignore click if it was triggered as part of an object click sequence (< 200ms)
      if(GetTickCount() - g_lastObjectClickTime < 200)
         return;

      if(g_selectedSessionIdx != -1)
        {
         g_selectedSessionIdx = -1;
         g_selectedSessionTime = 0;
         DeletePanelObjects(Prefix + "SI_");
         SetSessionMarkersVisible(true);
         ChartRedraw(0);
        }
     }
  }

Once a session has been selected, RenderSessionInspectorPanel() displays its price characteristics, high/low order, and range development measurements.

//+------------------------------------------------------------------+
//| Session Inspector Panel                                          |
//+------------------------------------------------------------------+
void RenderSessionInspectorPanel(const SessionData &sess)
  {
   string p = Prefix + "SI_";
   int x = 255;
   int y = 25;
   int width = 230;
   int height = 285;
   int row_h = 17;

   CreateOrUpdateRect(p + "BG", x, y, width, height, InpPanelBackground, InpPanelBorder);
   CreateOrUpdateLabel(p + "Title", x + 10, y + 8, ToUpperSafe(sess.name) + " INSPECTOR", sess.clr, 9, true);

   color net_clr = (sess.net_move > 0) ? InpPositiveColor : (sess.net_move < 0) ? InpNegativeColor : InpNeutralColor;
   string order_str = "UNKNOWN";
   if(sess.high_first == 1)
      order_str = "HIGH -> LOW";
   else
      if(sess.high_first == 0)
         order_str = "LOW -> HIGH";

   int curr_y = y + 28;
   DrawPanelRow(p, "Open",        DoubleToString(sess.open, _Digits), x + 10, curr_y, InpValueColor);
   curr_y += row_h;
   DrawPanelRow(p, "Close",       DoubleToString(sess.close, _Digits), x + 10, curr_y, InpValueColor);
   curr_y += row_h;
   DrawPanelRow(p, "Range",       StringFormat("%.1f pips", sess.range), x + 10, curr_y, InpValueColor);
   curr_y += row_h;
   DrawPanelRow(p, "Net Move",    StringFormat("%+.1f pips", sess.net_move), x + 10, curr_y, net_clr);
   curr_y += row_h;
   DrawPanelRow(p, "High",        DoubleToString(sess.high, _Digits), x + 10, curr_y, InpHighColor);
   curr_y += row_h;
   DrawPanelRow(p, "High Time",   TimeToString(sess.high_time, TIME_MINUTES), x + 10, curr_y, InpLabelColor);
   curr_y += row_h;
   DrawPanelRow(p, "Low",         DoubleToString(sess.low, _Digits), x + 10, curr_y, InpLowColor);
   curr_y += row_h;
   DrawPanelRow(p, "Low Time",    TimeToString(sess.low_time, TIME_MINUTES), x + 10, curr_y, InpLabelColor);
   curr_y += row_h;
   DrawPanelRow(p, "H/L Order",   order_str, x + 10, curr_y, InpNeutralColor);

   curr_y += (row_h + 4);
   CreateOrUpdateLabel(p + "DEV_TITLE", x + 10, curr_y, "RANGE DEVELOPMENT", InpPanelTitle, 8, true);
   curr_y += row_h;

   datetime inspect_time = TimeCurrent();
   datetime duration = sess.end_time - sess.start_time;
   bool reached_25 = inspect_time >= sess.start_time + (datetime)(duration * 0.25);
   bool reached_50 = inspect_time >= sess.start_time + (datetime)(duration * 0.50);
   bool reached_75 = inspect_time >= sess.start_time + (datetime)(duration * 0.75);

   DrawPanelRow(p, "25% Duration",  reached_25 ? StringFormat("%.1f pips", sess.range_25) : "-",  x + 10, curr_y, InpValueColor);
   curr_y += row_h;
   DrawPanelRow(p, "50% Duration",  reached_50 ? StringFormat("%.1f pips", sess.range_50) : "-",  x + 10, curr_y, InpValueColor);
   curr_y += row_h;
   DrawPanelRow(p, "75% Duration",  reached_75 ? StringFormat("%.1f pips", sess.range_75) : "-",  x + 10, curr_y, InpValueColor);
   curr_y += row_h;
   DrawPanelRow(p, sess.completed ? "100% Duration" : "Current Range", StringFormat("%.1f pips", sess.range_100), x + 10, curr_y, InpValueColor);
  }

This allows the analyzer to follow live data during normal use while still allowing historical sessions to be explored without losing the selected view.


Operating the Session Analysis Tool

With the main components in place, the indicator can now be attached to a chart to inspect the configured market sessions. The session definitions and display options can be adjusted through the input parameters.

Fig. 1. Insertion

Fig. 1. Inserting the Indicator on the chart

Once attached, the analyzer identifies the configured sessions within the selected history and displays their markers on the chart. The comparison panel provides a quick view of the available sessions from the latest common trading day, including their range and net movement.

Clicking a session marker opens the inspector, where the selected session's price characteristics, high/low order, and range development can be examined. Clicking an empty area of the chart closes the inspector.

Fig. 2. Usage

Fig. 2. Using the Indicator on the chart

The chart can also be moved into historical data for inspection. When automatic following is enabled, the analyzer resumes following the live edge when the chart is returned to the current market area.


Conclusion

In this first part, we established the foundation of a quantitative session analysis tool. Instead of treating a trading session as a visual time range alone, the analyzer converts each session into structured data containing its price characteristics and development measurements.

The tool can now identify sessions, measure their behavior, compare sessions from a common trading day, and provide an interactive view of individual occurrences directly on the chart. In future parts, we will build on this foundation by expanding the measurements collected from each session and introducing more ways to analyze the resulting data. We will examine session behavior across larger historical samples, add statistical comparisons, and develop additional metrics that can help reveal recurring characteristics.
The goal is to evolve the tool from a measurement utility into a research framework for market-session analysis.



Attached files |
SessionAnalyzer.mq5 (35.21 KB)
How MQL5 Lite MCP AI Assistant Changed My Debugging Approach on Generated MQL5 Codes How MQL5 Lite MCP AI Assistant Changed My Debugging Approach on Generated MQL5 Codes
This article presents a practical debugging workflow with MetaEditor's integrated AI Assistant and a comparison to the previous external approach. We fix a controlled set of syntax and API errors in a D1 PriceMarker EA, inspect modifications, and recompile. The result is validated in the Strategy Tester, with clear boundaries between compilation success and required runtime checks.
Creating and Testing a Council of 15 Models in MetaTrader 5 Creating and Testing a Council of 15 Models in MetaTrader 5
The article describes the transition from four-voice debates to the Council of Fifteen: ten analysts, four independent risk managers, and a Chair, with strict voting rules. It examines the roles of the participants, the three-phase architecture, and the parallel execution of a full cycle in 10–15 seconds. It shows the operational log, the risk-gate rules, and backward compatibility so you can quickly connect the system to an Expert Advisor.
Neural Networks in Trading: Robust Trading Signals in Any Market Regime (Conclusion) Neural Networks in Trading: Robust Trading Signals in Any Market Regime (Conclusion)
The article provides a detailed examination of the integration of the ST-Expert framework's approaches into the Extralonger architecture, which enables the simultaneous analysis of temporal and spatial representations of data. The results of testing on real historical data are presented, demonstrating the model's effectiveness and its robustness to market anomalies. The article describes the framework's modular structure, which ensures reproducibility, flexibility for research, and the ability to optimize components in stages.
Overcoming Accessibility Problems in MQL5 Trading Tools (Part VII): MetaTrader 5 Model Context Protocol (MCP) the Grand Solution Overcoming Accessibility Problems in MQL5 Trading Tools (Part VII): MetaTrader 5 Model Context Protocol (MCP) the Grand Solution
Many traders know what they want to analyze or automate but cannot navigate MetaTrader 5, use MetaEditor, or translate an idea into working MQL5 code. This article shows how the AI Assistant and MCP turn one natural-language prompt into a complete workflow: strategy development, editing, debugging, chart interaction, testing, and permission-controlled trading. We apply the process by building MCP_Accessibility_Assistant.mq5, leaving you with a working, accessible EA and a reusable prompt-driven development method.