preview
Developing a Quantitative Session Analysis Tool (Part 2): Building Universal Sessions and Analysing Session Relationships

Developing a Quantitative Session Analysis Tool (Part 2): Building Universal Sessions and Analysing Session Relationships

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

Introduction

In Part 1 of this series, we introduced quantitative session analysis by dividing the trading day into fixed broker-server windows and measuring the price activity within them. However, this approach introduces a practical limitation: the same server-time interval may not represent the same local market session throughout the year. As a result, session opening and closing levels, ranges, and overlap measurements can become inconsistent across seasonal clock changes.

The underlying issue is Daylight Saving Time (DST). London and New York change their clocks on different dates, while Tokyo does not observe DST. Although the configured local session hours remain unchanged, their relationship to UTC and the broker's server clock can shift. For MT5 indicators, historical analysis, and backtesting, this means that fixed server-time definitions may include different price bars than the intended local market windows.

To address this limitation, we must shift our approach from asking: "What server-time interval represents this session?"

To asking: "What is the local session definition, and where does that definition fall on the broker's clock for this specific date?"

This article introduces the Universal Session Engine, which defines each session through its local opening and closing hours and a timezone profile. The engine translates these definitions into broker-server timestamps for each occurrence using modeled regional DST rules and the configured broker-offset scheme. These calculated windows then provide the basis for collecting session measurements.

Building on this foundation, Session Relationship Analysis compares configured session pairs using measurable properties of their time windows and price ranges. The resulting measurements are available through the indicator's interactive panel and remain subject to the supported timezone rules, broker-offset assumptions, and bar-level price data:

  • Time Overlap: Measuring the temporal overlap between the two calculated session windows.
  • Price Overlap: Quantifying shared vertical price territory in pips and percentage coverage.
  • High/Low Interactions: Tracking whether the Comparison session moves beyond the Reference session's high or low.
  • Range Ratio: Comparing the range size of the Comparison session with the Reference session.

This implementation provides MT5 developers and quantitative researchers with a reusable session-processing structure and an interactive panel for inspecting the calculated relationships. We will explain the timezone conversion, relationship measurements, and operational verification, together with the limitations that govern these interpretations.


Defining the Session Model

Before implementing timezone translation, we first define the model that the engine and relationship analysis will operate on. These definitions establish the available session pairings, the data collected for each relationship, and the user controls exposed by the indicator.

Session Pair Selection

Session relationships are not hardcoded for individual market sessions. Instead, the engine derives every unique pair from the configured sessions programmatically, storing each pair with an explicit Reference and Comparison session. For each pair, the session appearing first in the configuration becomes the Reference, while the second becomes the Comparison. This assignment follows configuration order rather than chronological session opening time. The panel's SWAP control can then reverse those roles for analysis. With the three default sessions (Asia, London, New York), this produces exactly three pairings: Session 1 vs. Session 2, Session 1 vs. Session 3, and Session 2 vs. Session 3.

Each pair is represented by a SessionPairDef record containing the configuration indices of its reference and comparison sessions. BuildPairDefinitions() fills the global g_pair_defs[] array by looping over the session configurations once at startup, so the pair list is always derived from the configured sessions at startup. The session count itself is fixed at three in this version, so the derived pair count is also three. The pair builder also checks for duplicate session names before creating the definitions. If a duplicate is detected, it reports the configuration error and leaves the pair list empty, preventing ambiguous pair definitions from being constructed.

//+------------------------------------------------------------------+
//| NEW for Part 2: Session Pair Definition                          |
//+------------------------------------------------------------------+
struct SessionPairDef
  {
   int               ref_idx;                    // Reference session config index
   int               cmp_idx;                    // Comparison session config index
  };
//+------------------------------------------------------------------+
//| Derives the session pair combinations by configuration order     |
//+------------------------------------------------------------------+
void BuildPairDefinitions()
  {
   ArrayFree(g_pair_defs);
   int total = ArraySize(g_configs);

//--- Guard: Check for duplicate session names across inputs
   for(int i = 0; i < total; i++)
     {
      for(int j = i + 1; j < total; j++)
        {
         if(g_configs[i].name == g_configs[j].name)
           {
            PrintFormat("[Session Engine Error] Duplicate session name detected: '%s'. All session names must be unique.", g_configs[i].name);
            return; // Abort building pairs to prevent index collision/corrupt metrics
           }
        }
     }

   for(int i = 0; i < total; i++)
     {
      for(int j = i + 1; j < total; j++)
        {
         int n = ArraySize(g_pair_defs);
         ArrayResize(g_pair_defs, n + 1);

         //--- Configuration order: first session becomes Reference
         g_pair_defs[n].ref_idx = i;
         g_pair_defs[n].cmp_idx = j;
        }
     }
  }

Relationship Data

Each session comparison is represented by a dedicated SessionRelationship structure. It provides a single container for every measurement produced by the relationship engine, grouped by concern:

  • Identity — which sessions are being compared (ref_name, cmp_name, ref_config_idx, cmp_config_idx) and the occurrence day that anchors the record (day_key, the session-local date of the reference start).
  • Temporal overlap/state — whether the windows were active together (temporal_overlap), for how long (overlap_seconds), and whether both occurrences are finalized (completed).
  • Price overlap — whether the traded ranges shared territory (price_overlap), expressed in pips (price_overlap_pips) and as a percentage of the reference range (price_overlap_percent).
  • High/Low interaction — whether the comparison traded beyond the reference boundaries (cmp_exceeds_ref_high, cmp_exceeds_ref_low).
  • Relative range — the comparison range divided by the reference range (range_ratio).
//+------------------------------------------------------------------+
//| NEW for Part 2: Session Relationship Data Model                  |
//+------------------------------------------------------------------+
struct SessionRelationship
  {
   //--- Identity: the two independently configured sessions being related
   string            ref_name;                                    // Reference (first) session display name
   string            cmp_name;                                    // Comparison (second) session display name
   int               ref_config_idx;                              // Reference session config index
   int               cmp_config_idx;                              // Comparison session config index
   datetime          day_key;                                     // NEW for Part 2: occurrence day = session-LOCAL date of the reference start (grouping identity)

   //--- Temporal overlap
   bool              temporal_overlap;                            // True when the two windows share any wall-clock time
   long              overlap_seconds;                             // Shared wall-clock duration; 0 when there is no overlap
   bool              completed;                                   // True when both occurrences are finalized

   //--- Price-range overlap
   bool              price_overlap;                               // True when the two traded ranges share a common price region
   double            price_overlap_pips;                          // Extent of the common price region in pips
   double            price_overlap_percent;                       // Common region as % of the reference session range; -1 = unusable

   //--- High/Low interaction (comparison session vs reference session)
   bool              cmp_exceeds_ref_high;                        // Comparison traded above the reference session high
   bool              cmp_exceeds_ref_low;                         // Comparison traded below the reference session low

   //--- Relative range
   double            range_ratio;                                 // comparison range / reference range; -1 = reference range unusable
  };


User Controls

A common question is where the new session and relationship controls are configured. The answer is deliberately split in two:

  1. Session configuration is extended in Part 2 so each session defines its own local start time, end time, and timezone profile. This replaces the shared InpSessionEnd and sequential-session assumption from Part 1, allowing sessions to overlap and remain anchored to their actual local market times.

  1. Interaction is handled on the chart, not through additional inputs. A SHOW RELATIONSHIPS/HIDE RELATIONSHIPS button expands and collapses the relationship section below the session matrix, while a pair selector cycles through the configured pairings and a SWAP control flips the roles for the selected pair, rebuilding the relationship records. Both are processed in OnChartEvent() when the corresponding panel objects are clicked, updating g_showRelationships and g_selectedPairIdx.

//--- Input Parameters
input group "=== Session Settings ==="
input string InpSession1Name  = "Asia";
input string InpSession1Start = "09:00";
input string InpSession1End   = "18:00";                           // NEW for Part 2: Session 1 defines its own end boundary
input ENUM_TZ_PROFILE InpSession1TZ  = TZ_TOKYO;                   // NEW for Part 2: timezone profile for this session's Start/End
input color  InpSession1Color = clrDeepSkyBlue;

input string InpSession2Name  = "London";
input string InpSession2Start = "08:00";
input string InpSession2End   = "17:00";                           // NEW for Part 2: Session 2 defines its own end boundary
input ENUM_TZ_PROFILE InpSession2TZ  = TZ_LONDON;                  // NEW for Part 2: timezone profile for this session's Start/End
input color  InpSession2Color = clrLimeGreen;

input string InpSession3Name  = "New York";
input string InpSession3Start = "08:00";

input string InpSession3End   = "17:00";                           // NEW for Part 2: Session 3 defines its own end boundary
input ENUM_TZ_PROFILE InpSession3TZ  = TZ_NEW_YORK;                // NEW for Part 2: timezone profile for this session's Start/End
input color  InpSession3Color = clrTomato;
//--- REMOVED for Part 2 compliance: InpSessionEnd and the sequential assumption
//--- that "the next session's start is this session's end".

input group "=== Analysis Settings ==="
input int    InpHistoryDays     = 5;
input bool   InpDebugPrints     = false;                           // Print per-window debug diagnostics to the journal (default off)
input ENUM_BROKER_DST_SCHEME InpBrokerDstScheme = BROKER_DST_AUTO; // Broker server DST scheme: AUTO/NONE safe for fixed-UTC servers; pin US/EU for seasonal brokers

    //--- 2b. NEW for Part 2: Handle Comparison Panel Interaction
       if(id == CHARTEVENT_OBJECT_CLICK)
         {
          if(!InpShowDashboard)
             return;
    
          bool rel_btn  = (StringFind(sparam, Prefix + "MC_BTN_REL_") == 0);
          bool pair_sel = (sparam == Prefix + "MC_PAIR");
          bool swap_sel = (sparam == Prefix + "MC_SWAP");
    
          if(rel_btn || pair_sel || swap_sel)
            {
             //--- Record time of object click to prevent immediate trigger of CHARTEVENT_CLICK
             g_lastObjectClickTime = GetTickCount();
    
             if(rel_btn)
               {
                //--- Expand the panel downward / collapse back to minimal
                g_showRelationships = !g_showRelationships;
                if(g_showRelationships && g_selectedPairIdx < 0)
                   g_selectedPairIdx = 0;
               }
             else
                if(swap_sel)
                  {
                   //--- Flip Reference and Comparison roles for the selected pair
                   if(g_selectedPairIdx >= 0 && g_selectedPairIdx < ArraySize(g_pair_defs))
                     {
                      int temp = g_pair_defs[g_selectedPairIdx].ref_idx;
                      g_pair_defs[g_selectedPairIdx].ref_idx = g_pair_defs[g_selectedPairIdx].cmp_idx;
                      g_pair_defs[g_selectedPairIdx].cmp_idx = temp;
    
                      BuildRelationships();
                     }
                  }
                else
                  {
                   //--- Cycle to the next configured session pair
                   g_selectedPairIdx++;
                   if(g_selectedPairIdx >= ArraySize(g_pair_defs))
                      g_selectedPairIdx = 0;
                  }
    
             RenderMasterComparisonPanel();
             ChartRedraw(0);
             return;
            }
         }
    

    InpHistoryDays defaults to 5 to keep the initial analysis focused on a short recent period. It can be increased when examining earlier session occurrences, provided sufficient historical price data is available. With each session now carrying its own local timezone definition, the engine can translate those definitions into broker server-time boundaries for each historical date.


    The Universal Session Engine & Timezone Translation Logic

    The session definitions are now in place, but defining a session is only half of the problem. We still need to answer a more important question: "When does that session actually occur on the broker's chart?"

    A session such as London can be described using its local market time, but the historical price series available to an MQL5 indicator is indexed using broker/server time. The engine therefore needs to perform a translation for every date it analyzes.

    This translation cannot rely on a permanent UTC offset. The effective relationship between a financial center and UTC changes when that location enters or leaves Daylight Saving Time (DST), while other locations like Tokyo, Hong Kong, or Singapore do not observe DST at all. 

    To establish this local identity, we first structure our supported regional profiles and DST rules inside a dedicated helper library TimeZone.mqh.

    enum ENUM_TZ_PROFILE
      {
       TZ_SERVER,      // Broker / server time - no timezone conversion
       TZ_UTC,         // Coordinated Universal Time - fixed UTC+00:00, no DST
       TZ_TOKYO,       // Japan Standard Time - fixed UTC+09:00, no DST
       TZ_LONDON,      // United Kingdom - UTC+00:00 standard, EU DST rule
       TZ_NEW_YORK,    // US Eastern - UTC-05:00 standard, US DST rule
       TZ_CHICAGO,     // US Central - UTC-06:00 standard, US DST rule
       TZ_SYDNEY,      // Australian Eastern - UTC+10:00 standard, AU DST rule
       TZ_SINGAPORE,   // Singapore Time - fixed UTC+08:00, no DST
       TZ_HONG_KONG,   // Hong Kong Time - fixed UTC+08:00, no DST
       TZ_FRANKFURT,   // Central European (Germany) - UTC+01:00 standard, EU DST rule
       TZ_ZURICH       // Central European (Switzerland) - UTC+01:00 standard, EU DST rule
      };
    
    enum ENUM_TZ_DST_RULE
      {
       TZ_DST_NONE,   // No daylight saving - the standard offset applies all year
       TZ_DST_EU,     // Starts last Sunday of March, ends last Sunday of October
       TZ_DST_US,     // Starts second Sunday of March, ends first Sunday of November
       TZ_DST_AU      // Starts first Sunday of October, ends first Sunday of April (southern hemisphere - the DST span crosses the year boundary)
      };
    
    The engine uses a curated regional clock profile consisting of a standard UTC offset and a current-era DST rule. This provides the clock behavior required by the supported session profiles without attempting to reproduce the complete historical timezone database represented by IANA.

    From Local Session Time to Server Time

    To resolve modeled historical clock shifts, TimeZone.mqh exposes TZOffsetMin(), which evaluates the selected date against the regional DST rule assigned to that profile. The date-based overload uses a representative midday timestamp, so session-profile DST is applied at calendar-date resolution rather than reconstructing the exact transition instant within that day.
    int TZOffsetMin(int tz_id, datetime time)
      {
       TZProfile p = TZProfileOf(tz_id);
       int off = p.std_offset_min;
       if(TZDstAppliesOnDate(time, (ENUM_TZ_DST_RULE)p.dst_rule))
          off += 60;
       return(off);
      }
    
    //--- Overload to preserve support if passed separate year/mon/day integers
    int TZOffsetMin(int tz_id, int year, int mon, int day)
      {
       MqlDateTime dt;
       dt.year = year;
       dt.mon = mon;
       dt.day = day;
       dt.hour = 12;
       dt.min = 0;
       dt.sec = 0; // Evaluate at midday
       return TZOffsetMin(tz_id, StructToTime(dt));
      }

    For each session occurrence, the translation pipeline follows five steps:

    1. Anchor the day in the session's local calendar.
    2. Resolve the local UTC offset in effect on the session's start date — and separately on its end date, since the end can fall on the next calendar day for overnight sessions — using TZOffsetMin() from TimeZone.mqh. The date-based overload evaluates the DST rule with a midday timestamp, so the post-transition offset applies to the entire day on the two transition dates.
    3. Retrieve the broker's server UTC offset through TZBrokerServerOffsetSec(). The offset is initialized by TZCacheServerOffset(), with initialization deferred until valid terminal clock values are available. During window construction, the broker offset is evaluated near each proposed boundary and checked again at the resolved server timestamp, allowing the calculation to account for modeled broker DST changes near a transition.
    4. Compute the net offset shift independently for the start and end boundaries — net_start_offset_min = tz_start_off_min − server_start_off_min and net_end_offset_min = tz_end_off_min − server_end_off_min — and map each local wall-clock boundary onto an absolute MT5 server timestamp.
    5. Apply the overnight wrap-around rule: if the computed end timestamp is not after the start timestamp s_et <= s_st , the session crosses midnight and s_end is pushed forward by 86,400 seconds.
    This all happens inside BuildSessionWindow(), which converts the local profile into server timestamps for bar selection (exact within the model's stated limits).
    //+------------------------------------------------------------------+
    //| NEW: Builds one independent session window for an occurrence day |
    //+------------------------------------------------------------------+
    void BuildSessionWindow(const SessionConfig &cfg, datetime day_start, datetime &s_start, datetime &s_end)
      {
       int start_sec = (cfg.start_hour * 3600) + (cfg.start_min * 60);
       int end_sec   = (cfg.end_hour   * 3600) + (cfg.end_min   * 60);
    
    //--- Profile "Broker/Server Time": legacy behaviour - Start/End are interpreted
    //--- directly as MT5 server (chart) time, with no timezone conversion.
       if(cfg.tz_profile == (int)TZ_SERVER)
         {
          s_start = day_start + start_sec;
          s_end   = day_start + end_sec;
          if(s_end <= s_start)
             s_end += 86400;
          return;
         }
    
    //--- 1. Approximate the local target timestamp for the session start
       datetime approx_local_start = day_start + start_sec;
       MqlDateTime local_start_dt;
       TimeToStruct(approx_local_start, local_start_dt);
    
    //--- 2. Evaluate DST using the session's opening DATE (midday evaluation; per-date, not per-instant)
       int tz_start_off_min = TZOffsetMin(cfg.tz_profile, local_start_dt.year, local_start_dt.mon, local_start_dt.day);
    
    //--- 3. Retrieve broker server's active UTC offset for the session start
       int server_start_off_sec = TZBrokerServerOffsetSec(approx_local_start);   // seed sample near the session start 
       int server_start_off_min = server_start_off_sec / 60;
    
    //--- 4. Calculate net minute offset shift for the session start
       int net_start_offset_min = tz_start_off_min - server_start_off_min;
    
    //--- 5. Map local start wall-clock time directly to MT5 Server time
       s_start = day_start + start_sec - (net_start_offset_min * 60);
    
    //--- 5b. Re-sample the broker offset at the RESOLVED start instant (one refinement pass).
       int server_start_off_sec_resolved = TZBrokerServerOffsetSec(s_start);
       if(server_start_off_sec_resolved != server_start_off_sec)
         {
          server_start_off_sec = server_start_off_sec_resolved;
          server_start_off_min = server_start_off_sec / 60;
          net_start_offset_min = tz_start_off_min - server_start_off_min;
          s_start              = day_start + start_sec - (net_start_offset_min * 60);
         }
    
    
    //--- 6. Approximate the local target timestamp for the session end
       datetime approx_local_end = day_start + end_sec;
       if(end_sec <= start_sec)
          approx_local_end += 86400;
    
       MqlDateTime local_end_dt;
       TimeToStruct(approx_local_end, local_end_dt);
    
    //--- 7. Evaluate DST using the session's actual closing hour/date
       int tz_end_off_min = TZOffsetMin(cfg.tz_profile, local_end_dt.year, local_end_dt.mon, local_end_dt.day);
    
    //--- 8. Retrieve broker server's active UTC offset for the session end
       int server_end_off_sec = TZBrokerServerOffsetSec(approx_local_end);
       int server_end_off_min = server_end_off_sec / 60;
    
    //--- 9. Calculate net minute offset shift for the session end
       int net_end_offset_min = tz_end_off_min - server_end_off_min;
    
    //--- 10. Map local end wall-clock time directly to MT5 Server time
       s_end = day_start + end_sec - (net_end_offset_min * 60);
    
    //--- 10b. Same single refinement pass as the start boundary. The candidate is the already-wrapped
    //--- instant for midnight-crossing sessions, so a session that closes inside the broker's switch
    //--- window is not shifted by one hour either. Off transition dates this is a no-op.
       datetime end_candidate = (s_end <= s_start) ? (s_end + 86400) : s_end;
       int server_end_off_sec_resolved = TZBrokerServerOffsetSec(end_candidate);
       if(server_end_off_sec_resolved != server_end_off_sec)
         {
          datetime refined_end = day_start + end_sec - ((tz_end_off_min - server_end_off_sec_resolved / 60) * 60);
          if(refined_end <= s_start)
             refined_end += 86400;
          s_end = refined_end;
         }
    
    //--- Overnight session wrap-around rule
       if(s_end <= s_start)
          s_end += 86400;
    
    //--- Optional diagnostic print per computed window (gated by InpDebugPrints, off by default)
       if(InpDebugPrints)
         {
          int std_offset_sec = TZServerOffsetSec();
          bool dst_applied = (tz_start_off_min != TZProfileOf(cfg.tz_profile).std_offset_min);
    
          PrintFormat("Window '%s' [%s] -> Base UTC%+d min | %s | Net Server Offset: UTC%+d min | Final Window: %s to %s",
                      cfg.name,
                      TimeToString(day_start, TIME_DATE),
                      server_start_off_min,
                      (dst_applied ? "Daylight Time (DST Active)" : "Standard Time (No DST)"),
                      net_start_offset_min,
                      TimeToString(s_start, TIME_DATE|TIME_MINUTES),
                      TimeToString(s_end, TIME_DATE|TIME_MINUTES));
          PrintFormat("LOCAL=%s | SERVER=%s | TRADE_SERVER=%s",
                      (string)TimeLocal(),
                      (string)TimeCurrent(),
                      (string)TimeTradeServer());
         }
      }

    The important detail here is that the two sides of the conversion are handled differently. The session's local timezone offset is resolved for each occurrence date, while the broker server offset is normally taken from the cached server-clock measurement and reused across the historical range. If the broker follows a seasonal EU or US server-clock schedule, the user can instead select the corresponding broker DST scheme. This matters for historical analysis. MT5 does not provide a historical table of broker UTC-offset changes, so seasonal broker-clock changes cannot be reconstructed automatically for past dates. Without a suitable broker DST scheme, historical session boundaries may therefore be displaced by one hour during periods when the broker's server offset differed from the cached value. In this implementation, BROKER_DST_AUTO uses the cached broker offset without reconstructing historical seasonal changes. For brokers following a known EU or US server-clock DST schedule, the corresponding scheme must be selected explicitly. These rules provide a modeled historical adjustment rather than a complete record of the broker's actual clock changes.

    Turning Timestamps into Session Data

    Once the server-time boundaries have been resolved, the problem becomes much simpler. We now have two concrete timestamps:

    • s_st — the calculated server-time session start;
    • s_et — the calculated server-time session end.

    BuildSessionData() uses these boundaries to identify the bars belonging to the session. Bars satisfying:

    s_st ≤ bar_time < s_et
    Because membership is decided by each bar's opening timestamp, a bar that spans a boundary is attributed wholly to one side; on higher timeframes this can displace boundary values by up to one bar.

    The bar search is performed against the series-indexed price data without repeatedly resizing the collection for every matching bar. Because the arrays are ordered from the current bar backward, the search skips bars newer than the session and stops at bars older than the session. After the boundaries are identified, it allocates the index array once.

    The resulting indices are restored to oldest-to-newest order so that the existing opening, closing, range, and development calculations retain their original meaning.

    For each valid occurrence, the engine records the session's opening price (oldest in-window bar), closing price (newest in-window bar), high, low, the timestamps at which the high and low formed, the range and net move in pips, the observable high/low ordering when the extremes occur on different bars, and the time-based range-development measurements computed by CalculateSegmentRange().

    The populated records are stored in the global g_sessions[] array, each tagged with its owning configuration index (config_idx) and its local occurrence date (occ_date).

    There is also an important practical case to handle here: not every calendar date contains market data. Weekends and other periods without bars must not be interpreted as empty sessions with zero-valued statistics. The engine skips calendar weekends and discards any occurrence with no in-window bar ( newest_bar < 0 ). The weekend skip is a calendar approximation: holidays, early closes and Sunday market openings are not modeled individually — occurrences without bars are simply discarded, so such periods never create fabricated zero-value sessions.

    At the end of the build, the session records are passed to BuildRelationships() , which derives the relationship measurements from the same session occurrences.
    //+------------------------------------------------------------------+
    //| 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 = time[0];
    
       datetime server_time  = TimeCurrent();
       datetime oldest_time  = current_time - (InpHistoryDays * 86400);
    
       SessionData temp_sessions[];
       int count = 0;
    
    //--- NEW for Part 2: iterate per session over THAT session's LOCAL dates.HistoryDays now counts SESSION-LOCAL calendar dates; each session's "today" is resolved in its own timezone so all occurrences share a
    //--- stable local-date identity (occ_date) regardless of the server clock. Broker/Server Time keeps its original server-date semantics.
       for(int s = 0; s < 3; s++)
         {
    
          // session-local "today" = UTC-midnight epoch of that calendar date
          datetime occ_today = 0;
          if(g_configs[s].tz_profile == (int)TZ_SERVER)
            {
             MqlDateTime tds;
             TimeToStruct(current_time, tds);
             tds.hour = 0;
             tds.min  = 0;
             tds.sec  = 0;
             occ_today = StructToTime(tds);
            }
          else
            {
             int oy = 0, om = 0, oday = 0;
             TZLocalDateOfInstant(g_configs[s].tz_profile, current_time - TZBrokerServerOffsetSec(current_time), oy, om, oday);
             MqlDateTime tds;
             tds.year = oy;
             tds.mon  = om;
             tds.day  = oday;
             tds.hour = 0;
             tds.min  = 0;
             tds.sec  = 0;
             occ_today = StructToTime(tds);
            }
    
          //--- Generate the last (HistoryDays+1) session-local occurrence dates. Server timestamps stay the authority for bar selection; the local date (occ_date) is the stable identity used for relationship grouping.
          for(int d = InpHistoryDays; d >= 0; d--)
    
            {
             datetime occ_date = occ_today - (d * 86400);
    
             MqlDateTime occ_dt;
             TimeToStruct(occ_date, occ_dt);
    
             if(occ_dt.day_of_week == 0 || occ_dt.day_of_week == 6)
                continue;
    
             datetime s_st = 0;
             datetime s_et = 0;
             BuildSessionWindow(g_configs[s], occ_date, s_st, s_et);
    
             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;
             int newest_bar = -1;
             int oldest_bar = -1;
    
             for(int b = 0; b < total_rates; b++)
               {
                if(time[b] >= s_et)
                   continue;
    
                if(time[b] < s_st)
                   break;
    
                if(newest_bar < 0)
                   newest_bar = b;
    
                oldest_bar = b;
               }
    
             if(newest_bar < 0)
                continue;
    
             bar_count = oldest_bar - newest_bar + 1;
             ArrayResize(bar_indices, bar_count);
    
             for(int k = 0; k < bar_count; k++)
                bar_indices[k] = oldest_bar - k;
    
             //--- 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].config_idx = s;               // NEW for Part 2: owning config (used for pairing)
             temp_sessions[count].occ_date    = occ_date;       // NEW for Part 2: session-local occurrence date (stable identity)
             temp_sessions[count].open       = open[open_bar];
             temp_sessions[count].close      = close[close_bar];
    
             temp_sessions[count].completed  = (server_time >= s_et);
    
             //--- 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;
    
             //--- 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  = (server_time >= checkpoints[0] && q1 >= 0) ? CalculateSegmentRange(q1, bar_count, bar_indices, high, low) : 0.0;
             temp_sessions[count].range_50  = (server_time >= checkpoints[1] && q2 >= 0) ? CalculateSegmentRange(q2, bar_count, bar_indices, high, low) : 0.0;
             temp_sessions[count].range_75  = (server_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);
    
             count++;
            }
         }
    
       ArrayResize(g_sessions, count);
       for(int i = 0; i < count; i++)
          g_sessions[i] = temp_sessions[i];
    
    //--- NEW for Part 2: derive relationship measurements over the freshly built session records
       BuildRelationships();
      }
    

    At this point, the timezone problem has been separated from the analysis problem. The engine has converted local market definitions into concrete historical sessions that can be compared using ordinary price and time measurements. That is where the relationship layer begins.


    Measuring How Sessions Interact

    Once individual sessions have been reconstructed correctly, we can stop looking at them independently. Suppose Session 1 establishes a range and Session 2 follows it. We can now ask measurable questions:

    • How long were the two sessions active at the same time?
    • Did their price ranges overlap?
    • Did Session 2 exceed Session 1's high or low?
    • How large was Session 2 relative to Session 1?

    These questions form the basis of the SessionRelationship layer.

    Aligning Comparable Sessions

    BuildRelationships() first collects the distinct occurrence days (day_key values, carried as occ_date) present in the session records. It then walks every pair in g_pair_defs[] and, for each pair and each day, locates the reference and comparison occurrences that share both the pair's configuration indices and that exact day. This matching is essential: the relationship is not between arbitrary elements of two arrays; it is between two specific session occurrences sharing the same session-local occurrence date (occ_date). occ_date is each session's own local calendar date, so sessions that fall on different local dates are not paired even if they belong to the same trading week. Only when both occurrences exist is a relationship record created. This is a same-local-date pairing model, not an absolute-time matching model. Two sessions with different local occurrence dates are not paired, even if their calculated windows overlap in server time.

    //+------------------------------------------------------------------+
    //| NEW for Part 2: Builds relationship records over session data    |
    //+------------------------------------------------------------------+
    void BuildRelationships()
      {
       ArrayFree(g_relationships);
       int total = ArraySize(g_sessions);
       if(total < 2)
          return;
    
    //--- Collect the distinct occurrence days present in the session records
       datetime day_keys[];
       for(int i = 0; i < total; i++)
         {
          datetime k = g_sessions[i].occ_date;
          bool found = false;
          for(int d = 0; d < ArraySize(day_keys); d++)
             if(day_keys[d] == k)
               {
                found = true;
                break;
               }
          if(!found)
            {
             int n = ArraySize(day_keys);
             ArrayResize(day_keys, n + 1);
             day_keys[n] = k;
            }
         }
    
    //--- Pair the two occurrences that belong to the SAME occurrence day
    //--- (sessions from different days are never related even if names match)
       for(int p = 0; p < ArraySize(g_pair_defs); p++)
         {
          int r_cfg = g_pair_defs[p].ref_idx;
          int c_cfg = g_pair_defs[p].cmp_idx;
    
          for(int d = 0; d < ArraySize(day_keys); d++)
            {
             int r_idx = -1, c_idx = -1;
             for(int i = 0; i < total; i++)
               {
                if(g_sessions[i].config_idx == r_cfg && g_sessions[i].occ_date == day_keys[d])
                   r_idx = i;
                if(g_sessions[i].config_idx == c_cfg && g_sessions[i].occ_date == day_keys[d])
                   c_idx = i;
               }
    
             if(r_idx >= 0 && c_idx >= 0)
               {
                int n = ArraySize(g_relationships);
                ArrayResize(g_relationships, n + 1);
                g_relationships[n] = CalculateRelationship(g_sessions[r_idx], g_sessions[c_idx]);
               }
            }
         }
      }
    

    Measuring Temporal Overlap

    Two sessions may partially overlap, completely overlap, or occur at separate times. Rather than representing this relationship visually, we measure it directly.

    For two intervals — Reference [R_start, R_end) and Comparison [C_start, C_end) — the overlapping interval is:

    and its duration is:

    When the intervals do not overlap, the engine assigns a zero duration instead of a negative one. Because the engine stores absolute timestamps (including next-day ends), midnight-crossing sessions are handled implicitly by this interval math. The overlap is measured on the calculated window boundaries and is exact within the model, not necessarily within the real market's trading hours.

    The result is stored as overlap_seconds, allowing the panel to report the actual amount of concurrent session time rather than relying on visual estimation.

    Measuring Shared Price Territory

    Time overlap tells us when two sessions coexist. It does not tell us whether they occupy the same part of the price range.

    For the reference session with high R_H and low R_L, and the comparison session with high C_H and low C_L, their shared price interval is determined by taking the lower of the two highs and the higher of the two lows:

    The raw vertical distance between these boundaries is calculated as:

    If this value is positive, the two ranges share price territory. If it is zero or negative, there is no common vertical range.

    To convert this raw price distance into pips, we divide by the symbol's point multiplier:


    Where the reference session's range (R_range = R_H - R_L) is non-zero and usable, this value is normalized against the reference range to produce a percentage — directly answering "how much of the reference window's range the comparison window also traded"

    This gives the relationship layer two different dimensions of overlap: time tells us whether the sessions were active together; price tells us whether their ranges occupied the same territory.

    Tracking Range Breaks

    Range overlap alone does not tell us whether the comparison session respected the structure established by the reference session. We therefore test the comparison extremes against the reference extremes:

    These boolean values provide a simple structural description of the interaction. A session may therefore remain completely inside the reference range, exceed its high, exceed its low, or extend beyond both boundaries. The comparison is always the pair's second member, so the reading is directional: each test reports whether the comparison traded beyond the reference's boundary, and SWAP reverses which session is the reference — and with it the 'Broken/Held' reading. The underlying measurement itself is order-independent; only the label is directional. 

    Comparing Relative Range Expansion

    The final measurement compares the size of the two session ranges:

    A ratio of 1.0 means both ranges are equal in size. A value greater than 1.0 means the comparison session covered a larger range, while a value below 1.0 means it covered a smaller one. The ratio is therefore a compact way to describe relative expansion or contraction without depending on the absolute price scale of the instrument. Because the ratio is comparison range / reference range , swapping the pair inverts it.

    All of these measurements are computed in a single pure function that takes the two matched SessionData records and returns a fully populated SessionRelationship.

    //+------------------------------------------------------------------+
    //| NEW for Part 2: Session Relationship Measurements                |
    //| Pure observation layer: computes raw measurements only, renders  |
    //| nothing. Separated from presentation for later aggregation.      |
    //+------------------------------------------------------------------+
    SessionRelationship CalculateRelationship(const SessionData &ref_sess, const SessionData &cmp_sess)
      {
       SessionRelationship rel;
    
       rel.ref_name       = ref_sess.name;
       rel.cmp_name       = cmp_sess.name;
       rel.ref_config_idx = ref_sess.config_idx;
       rel.cmp_config_idx = cmp_sess.config_idx;
       rel.day_key        = ref_sess.occ_date;   // NEW for Part 2: group by the session-LOCAL occurrence date
       rel.completed      = (ref_sess.completed && cmp_sess.completed);
    
    //--- Temporal overlap: the shared wall-clock interval of the two windows.
    //--- Absolute timestamps (including next-day ends) handle midnight-crossing sessions implicitly.
       datetime ov_start = (ref_sess.start_time > cmp_sess.start_time) ? ref_sess.start_time : cmp_sess.start_time;
       datetime ov_end   = (ref_sess.end_time   < cmp_sess.end_time)   ? ref_sess.end_time   : cmp_sess.end_time;
       rel.temporal_overlap = (ov_start < ov_end);
       rel.overlap_seconds  = rel.temporal_overlap ? (long)(ov_end - ov_start) : 0;
    
    //--- Price-range overlap: intersection of the two traded ranges.
    //--- Denominator for the percentage = the REFERENCE session range, so the value answers
    //--- "how much of the reference window's range the comparison window also traded".
       double p_lo = (ref_sess.low  > cmp_sess.low)  ? ref_sess.low  : cmp_sess.low;
       double p_hi = (ref_sess.high < cmp_sess.high) ? ref_sess.high : cmp_sess.high;
       rel.price_overlap        = (p_hi > p_lo);
       rel.price_overlap_pips   = rel.price_overlap ? (p_hi - p_lo) / GetPipSize() : 0.0;
       rel.price_overlap_percent = (ref_sess.range > 0.0) ? (rel.price_overlap_pips / ref_sess.range) * 100.0 : -1.0;
    
    //--- High/Low interaction: did the comparison session trade beyond the reference boundaries?
    //--- The boolean tests only compare two values (order-independent); the reported reading is always relative to the selected Reference — SWAP reverses it.
       rel.cmp_exceeds_ref_high = (cmp_sess.high > ref_sess.high);
       rel.cmp_exceeds_ref_low  = (cmp_sess.low  < ref_sess.low);
    
    //--- Relative range: comparison range / reference range.
    //--- -1 (not a real ratio) marks an unusable zero reference range; the UI shows "n/a".
       rel.range_ratio = (ref_sess.range > 0.0) ? (cmp_sess.range / ref_sess.range) : -1.0;
    
       return(rel);
      }

    With these calculations, a session relationship is no longer a visual impression from two boxes on a chart. It becomes a structured record containing measurable temporal, spatial, and range information.


    Presenting the Relationship Model

    The calculations are useful only if the resulting information can be inspected without reading the source code. For that reason, the relationship data is integrated into the existing comparison panel rather than presented as a separate diagnostic window. RenderMasterComparisonPanel() retains the original three-session matrix — SESSION / RANGE / NET columns with the configured sessions aligned to a common occurrence date — and appends a toggle button beneath it.

    When g_showRelationships is true, the panel grows downward and RenderRelationshipSection() draws the relationship block: a pair selector, the five measurement rows (Time Overlap, Price Overlap, High Interaction, Low Interaction, Range Ratio), and a LIVE badge when the displayed relationship belongs to today's active occurrence and both of its participants are open at the same instant.

    The panel is built entirely from chart objects — CreateOrUpdateRect() (OBJ_RECTANGLE_LABEL) and CreateOrUpdateLabel() (OBJ_LABEL) — so no canvas buffer or redraw pipeline is required, and existing objects are updated in place rather than recreated each tick.

    //+------------------------------------------------------------------+
    //| Master Comparison Panel                                          |
    //| The panel is always drawn above ALL chart decorations: markers   |
    //| render behind the chart (OBJPROP_BACK=true), the panel rect has  |
    //| the highest z-order. NEW for Part 2: the panel grows downward    |
    //| to host the relationship layer when toggled on.                  |
    //+------------------------------------------------------------------+
    void RenderMasterComparisonPanel()
      {
       string p = Prefix + "MC_";
       int x = 15;
       int y = 25;
       int width = 230;
       int base_h = 135;                    // collapsed height (Part 1 style)
       int row_h = 18;
    
       int height = base_h;
       if(g_showRelationships)             // NEW for Part 2: expand downward
          height = 262;
    
    //--- Purge relationship-layer artifacts when the panel collapses back to its minimal size
       int view = g_showRelationships ? 1 : 0;
       if(view != g_lastRenderedView)
         {
          if(view == 0)
             DeleteRelationshipViewObjects(p);
          g_lastRenderedView = view;
         }
    
       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;
       for(int i = 0; i < total; i++)
          if(g_sessions[i].occ_date > comparison_day)
             comparison_day = g_sessions[i].occ_date;
    
    //--- Find each configured session within the common comparison day
       for(int s = 0; s < 3; s++)
         {
          int found_idx = -1;
          for(int i = total - 1; i >= 0; i--)
            {
             if(g_sessions[i].config_idx == s && g_sessions[i].occ_date == comparison_day)
               {
                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;
         }
    
    //--- NEW for Part 2: toggle button — expands the panel downward / collapses back
       string btn_text = g_showRelationships ? "HIDE RELATIONSHIPS" : "SHOW RELATIONSHIPS";
       int by = (y + base_h) - 24;
       CreateOrUpdateRect(p + "BTN_REL_BG",  x + 10, by, width - 20, 18, InpPanelBorder, InpValueColor);
       CreateOrUpdateLabel(p + "BTN_REL_TXT", x + 20, by + 3, btn_text, InpPanelTitle, 8, true);
    
       if(g_showRelationships)
          RenderRelationshipSection(p, x, y);
      }
    //+------------------------------------------------------------------+
    //| NEW for Part 2: Relationship layer rendered below the matrix     |
    //| (the panel grows downward to host it). Consumes measurements;    |
    //| contains no calculation logic.                                   |
    //+------------------------------------------------------------------+
    void RenderRelationshipSection(string p, int x, int y)
      {
       int row_h = 17;
       int ry = y + 142;                 // relationship block sits below the collapsed panel area
    
       if(ArraySize(g_pair_defs) == 0)
          return;
    
       if(g_selectedPairIdx < 0 || g_selectedPairIdx >= ArraySize(g_pair_defs))
          g_selectedPairIdx = 0;
    
       int ref_cfg = g_pair_defs[g_selectedPairIdx].ref_idx;
       int cmp_cfg = g_pair_defs[g_selectedPairIdx].cmp_idx;
    
    //--- Pair selector: Explicitly label Reference [Ref] and Comparison [Cmp] sessions
       string pair_text = "[Ref] " + ToUpperSafe(g_configs[ref_cfg].name) + " vs [Cmp] " + ToUpperSafe(g_configs[cmp_cfg].name) + "  ▼";
       CreateOrUpdateLabel(p + "PAIR", x + 10, ry, pair_text, InpPanelTitle, 8, true);
    
       int rel_idx = FindLatestRelationship(g_selectedPairIdx);
       bool live = false;
       int curr_y = ry + 22;
    
       if(rel_idx < 0)
         {
          //--- No occurrence with both sessions available yet
          DeletePanelObjects(p + "R_");
          ObjectDelete(0, p + "LIVE");
          CreateOrUpdateLabel(p + "NO_DATA", x + 10, curr_y, "No pair data yet", InpLabelColor, 8, false);
         }
       else
         {
          SessionRelationship rel = g_relationships[rel_idx];
          //--- LIVE shows ONLY if the displayed relationship belongs to today's occurrence AND both of its participating sessions are open at the same instant.
          live = false;
          datetime now_time = TimeCurrent();
          bool ref_active = false;
          bool cmp_active = false;
    
          for(int li = ArraySize(g_sessions) - 1; li >= 0; li--)
            {
             //--- 1. Must match the exact day of the displayed relationship
             if(g_sessions[li].occ_date != rel.day_key)
                continue;
    
             //--- 2. Must belong to either the reference or comparison session of THIS pair
             if(g_sessions[li].config_idx != rel.ref_config_idx && g_sessions[li].config_idx != rel.cmp_config_idx)
                continue;
    
             //--- 3. Is THIS specific session actively running right now?
             if(now_time >= g_sessions[li].start_time && now_time < g_sessions[li].end_time)
               {
                //--- LIVE = real overlap: both participants must be open at the same instant
                if(g_sessions[li].config_idx == rel.ref_config_idx)
                   ref_active = true;
                if(g_sessions[li].config_idx == rel.cmp_config_idx)
                   cmp_active = true;
                if(ref_active && cmp_active)
                  {
                   live = true;
                   break; // Found an active participant for this specific day/pair, stop scanning!
                  }
               }
            }
          ObjectDelete(0, p + "NO_DATA");
    
          string t_ov = FormatOverlapDuration(rel.overlap_seconds);
    
          string p_ov;
          if(rel.price_overlap_percent < 0.0)
             p_ov = "n/a";
          else
             p_ov = StringFormat("%.1f%% (%.1fp)", rel.price_overlap_percent, rel.price_overlap_pips);
    
          string r_ratio = (rel.range_ratio < 0.0) ? "n/a" : StringFormat("%.2fx", rel.range_ratio);
    
          //--- Render relationship metrics with explicit Cmp/Ref directional context
          DrawPanelRow(p + "R_", "Time Overlap",      t_ov,                                                    x + 10, curr_y, InpValueColor);
          curr_y += row_h;
          DrawPanelRow(p + "R_", "Price Overlap",     p_ov,                                                    x + 10, curr_y, InpValueColor);
          curr_y += row_h;
          DrawPanelRow(p + "R_", "Ref High",    rel.cmp_exceeds_ref_high ? "Broken" : "Held",            x + 10, curr_y, InpValueColor);
          curr_y += row_h;
          DrawPanelRow(p + "R_", "Ref Low",     rel.cmp_exceeds_ref_low  ? "Broken" : "Held",            x + 10, curr_y, InpValueColor);
          curr_y += row_h;
          DrawPanelRow(p + "R_", "Range Ratio (C/R)", r_ratio,                                                x + 10, curr_y, InpValueColor);
         }
    
    //--- Interactive Swap Icon (Positioned at bottom right to avoid header collision)
       CreateOrUpdateLabel(p + "SWAP", x + 180, ry + 85, "SWAP", InpNeutralColor, 9, true);
    
    //--- Live sessions: measurements reflect only currently available data
       if(live)
          CreateOrUpdateLabel(p + "LIVE", x + 180, ry + 22, "LIVE", InpNeutralColor, 8, true);
       else
          ObjectDelete(0, p + "LIVE");
      }
    

    The SHOW RELATIONSHIPS control therefore changes the amount of information displayed without changing the underlying calculations. Historical sessions remain fixed once their data has been established, while the currently developing session can continue to update as new bars arrive. This distinction is important because a completed session represents a final measurement, whereas an active session represents a changing state — which is why LIVE is shown only when the displayed record belongs to today's occurrence and both of its sessions are open at the same instant.

    The same principle is applied to value formatting. Nothing in the panel is a manually assigned state: net movement is colored from the sign of the underlying measurement (InpPositiveColor/InpNegativeColor/InpNeutralColor), high/low interaction is rendered as "Broken"/"Held" directly from cmp_exceeds_ref_high/cmp_exceeds_ref_low, the overlap duration is formatted with FormatOverlapDuration(), and the pair selector resolves the latest available record for the chosen pair through FindLatestRelationship().

    //+------------------------------------------------------------------+
    //| Explicit Session Marker Visibility Control                       |
    //+------------------------------------------------------------------+
    void SetSessionMarkersVisible(bool visible)
      {
       long visibility_flag = visible ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS;
    
       for(int i = ObjectsTotal(0, 0, -1) - 1; i >= 0; i--)
         {
          string name = ObjectName(0, i, 0, -1);
          if(StringFind(name, Prefix + "PT_") == 0 || StringFind(name, Prefix + "TXT_") == 0)
            {
             if(ObjectGetInteger(0, name, OBJPROP_TIMEFRAMES) != visibility_flag)
                ObjectSetInteger(0, name, OBJPROP_TIMEFRAMES, visibility_flag);
            }
         }
      }
    

    The detailed interpretation of these measurements is best demonstrated through actual examples in the walkthrough. There, you will see a concrete session pair, trace the numbers back to the underlying ranges and timestamps, and understand what each displayed value represents.


    Keeping Historical Processing Efficient

    There is one final architectural consideration. Historical session analysis can involve scanning a substantial amount of chart data, while OnCalculate() may be invoked repeatedly as ticks arrive. Rebuilding the entire session and relationship history on every tick would therefore perform the same expensive work unnecessarily.

    The solution is to separate calculation events from display events and gate both the analytical rebuild and the normal panel rendering around meaningful state changes:

    • Engine layer: series indexing is enforced via ArraySetAsSeries() (on every calculation), and the full pipeline is executed — BuildSessionData(), which finalizes BuildRelationships() at its end.
    • Bar gate: subsequent engine rebuilds are gated by new-bar detection, comparing time[0] against the cached g_lastProcessedBarTime. The guard is_new_bar || prev_calculated == 0 ensures the historical session data is rebuilt only on initial calculation or when a new bar appears.
    • Startup synchronization: if the timezone engine has not yet obtained a valid broker/server clock sample, OnCalculate() retries TZCacheServerOffset(). Once initialization succeeds, g_lastProcessedBarTime is reset so the session data receives a full rebuild using the valid offset.
    • Session search: within each session occurrence, the bar search is bounded by the calculated s_st and s_et timestamps and builds the index array once, avoiding repeated full-history scans and repeated dynamic resizing for every matching bar.
    • UI layer: the comparison panel, inspector, marker visibility state, and ChartRedraw() are updated together with the normal calculation cycle rather than being rebuilt on every incoming tick. User-driven panel interactions remain handled separately through OnChartEvent().
    //+------------------------------------------------------------------+
    //| 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);
    
    //--- Cold-start clock-sync retry: never keep the server offset uninitialised after the
    //--- terminal clock syncs; flip to a full rebuild once a valid sample becomes available.
       if(!g_tz_initialized)
         {
          TZCacheServerOffset(InpBrokerDstScheme);
          if(g_tz_initialized)
             g_lastProcessedBarTime = 0;
          else
             return(0);
         }
    
       bool is_new_bar = (time[0] != g_lastProcessedBarTime);
    
    //--- LAYER 1 & 2: ENGINE PROCESSING — REBUILD ONLY ON NEW BAR OR INITIAL LOAD
       if(is_new_bar || prev_calculated == 0)
         {
          BuildSessionData(time, open, high, low, close, rates_total);
          // BuildRelationships() is finalized at the end of BuildSessionData()
          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);
               }
            }
    
          if(InpShowSessionMarkers || InpShowSessionLabels)
             RenderSessionMarkers();
    
          g_lastProcessedBarTime = time[0];
    
          //--- LAYER 3: USER INTERFACE — RENDER PANELS & MAINTAIN VISIBILITY STATES
          //--- 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);
      }

    This keeps the expensive historical processing tied to meaningful changes in the price series while avoiding unnecessary repeated chart-object work during normal tick activity. It also ensures that a terminal startup race does not permanently lock the analyzer to an uninitialized clock state; a wrong-but-plausible measured offset, however, is retained until the indicator is re-initialized (see Weekend limitation).

    The result is a cleaner separation between session reconstruction, relationship analysis, and chart presentation. The engine performs the expensive work when the underlying price series changes, while interaction-specific updates remain event-driven.


    Architectural Considerations

    The Universal Session Engine resolves each session using its local timezone rules and maps the resulting boundaries onto the broker's server time. The final measurements, however, depend on the price data supplied by that broker.

    Broker feed differences: Different brokers can provide slightly different OHLC data for the same market interval. As a result, session ranges, pip movements, and high/low locations may differ slightly between brokers even when the session boundaries are identical. These differences are therefore a property of the underlying price feed rather than the timezone calculation.

    Terminal synchronization: The engine measures the broker's server-to-UTC offset once, at startup, using TimeCurrent() − TimeGMT() and rounding the result to 30-minute steps. This value is cached for the lifetime of the indicator instance and is reused for all historical session conversions. At startup or after a connection interruption, the trade-server clock may not yet be available. Until both clocks provide a plausible sample, the server-time conversion remains uninitialized. The cold-start path then retries the measurement on the next recalculation cycle and rebuilds the session data once a valid sample becomes available.

    Weekend limitation:The automatic cold-start recovery path is tick-driven because its retry and session-data rebuild are performed during recalculation. During a weekend with no incoming market ticks, an unattended terminal restart therefore cannot trigger that recovery path, leaving the indicator in its current synchronization state until recalculation resumes. Re-attaching the indicator is different: it forces a fresh OnInit() measurement and a full OnCalculate() rebuild, so it can clear a previously cached incorrect offset once the terminal clock is valid. However, re-attaching cannot correct a clock source that is still unavailable or itself incorrect. A timer-driven re-measurement and a sanity bound on the cached offset are planned improvements for a future segment.

    Historical server-time limitation: MetaTrader 5 does not provide a historical table of broker server UTC-offset changes. The engine therefore cannot independently reconstruct every historical broker DST transition. Historical analysis involving brokers whose server clock changes seasonally should be interpreted with this limitation in mind.

    Completed-bar updates: Session statistics are recomputed when a new bar opens, from the bars available at that moment, and the panel updates on that same cycle rather than on every tick. The currently developing session includes the latest — still-forming — bar, so its displayed values are frozen between bar openings and always represent the state as of the last processed bar.

    LIVE indicator: The LIVE indicator is shown only when the two sessions of the displayed pair are both open at the same instant, meaning the pair is genuinely overlapping at that moment. A relationship whose reference session has already closed shows no LIVE badge even if the comparison session is still trading.

    These limitations affect different parts of the analysis. Broker-offset assumptions and timezone rules determine the calculated session boundaries, while feed differences, bar availability, and update timing influence the resulting measurements and their presentation.


    Operational WalkThrough

    After compiling the indicator, attach it to a chart and observe the analyzer as the configured sessions are processed.

    In our test, the chart automatically displayed the session boundaries and measured ranges and movements, while development levels are shown inside the inspector panel. The comparison panel summarized the relationships between sessions, while the inspector and navigation controls provided a closer view of individual occurrences.

    Showcase

    Fig. 1. Indicator Usage On Chart

    Separately, to verify the timezone conversion, we tested the indicator in the MetaTrader 5 Strategy Tester using short historical periods around the March 2026 and October–November 2025 DST transitions. The tests used USDJPY on the M15 timeframe, with InpHistoryDays = 5, InpBrokerDstScheme = BROKER_DST_AUTO, and InpDebugPrints enabled. The cached broker-server offset was UTC+0, allowing the session-profile DST adjustments to be examined against a fixed server-time reference.

    London enters DST on the last Sunday in March and returns to standard time on the last Sunday in October. New York enters DST on the second Sunday in March and returns on the first Sunday in November. Since these transitions occur on Sundays, the table compares available trading-day occurrences before and after each change, rather than the transition dates themselves. Both sessions are configured for 08:00–17:00 in their respective local timezones, while the table shows their corresponding windows in broker-server time.
    Session Occurrence date
    DST state
    Session UTC offset
    Broker offset  Calculated server window 
    London 2026.03.25 (Wed)
    Standard
    UTC+0
    UTC+0  08:00–17:00 
    London
    2026.03.30 (Mon)
    DST Active
    UTC+1
    UTC+0  07:00–16:00 
    London
    2025.10.24 (Fri)
    DST Active
    UTC+1
    UTC+0  07:00–16:00 
    London
    2025.10.28 (Tue)
    Standard
    UTC+0
    UTC+0  08:00–17:00 
    New York
    2026.03.06 (Fri)
    Standard
    UTC−5
    UTC+0  13:00–22:00 
    New York
    2026.03.09 (Mon)
    DST Active
    UTC−4
    UTC+0  12:00–21:00
    New York
    2025.10.31 (Fri)
    DST Active
    UTC−4 UTC+0  12:00–21:00 
    New York  2025.11.03 (Mon) Standard UTC−5 UTC+0  13:00–22:00 

    The calculated server-time windows shift by one hour across each market's DST transition, while the configured local session hours remain unchanged. With BROKER_DST_AUTO, the engine reuses the cached broker offset rather than reconstructing historical broker-clock changes. These Strategy Tester runs therefore demonstrate the session-profile DST rules against a fixed UTC+0 server offset, not the behavior of a broker whose own server clock change seasonally. Figures 2 and 3 show the resolved DST states, conversion offsets, and calculated session boundaries.

    Fig. 2. Log proof showing the start of DST for London and New York

    Fig. 3. Log proof showing the end of DST for London and New York

    Reading the Values

    The displayed values describe the measurements available for each session occurrence. Range measures the distance between the high and low, while Net Move measures the change from open to close. For a completed session, these values represent the final measurements; for the currently developing session, they reflect the latest processed bar.

    The comparison panel adds context by measuring how sessions relate to each other as the following table shows.

    Metric
    What it shows
    Example reading
    Possible interpretation
    Time Overlap
    Shared active time between two sessions.
    4 hours
    London and New York were active together for four hours. This could help identify periods where their market activities coincided.
    Price Overlap
    Percentage of shared price territory.
    75%
    Most of the ranges overlapped. This could suggest that both sessions remained within similar price territory.
    High/Low Interaction
    Whether the Comparison session exceeds the Reference session's high or low. Ref High: Broken New York moved above London's high. This could be worth examining as a breakout or liquidity sweep.
    Range Ratio
    Relative size of two session ranges.
    1.5

    The Comparison session's range was 50% larger than the Reference session's range. This could indicate an increase in volatility.

    These interpretations are observations to investigate, not guaranteed outcomes. The timezone determines when the session occurs, while the broker's price feed determines what price movement is measured.

    The implementation is organized into two files, with the analyzer handling session analysis and the timezone module providing the time-conversion layer.

    File Description
    SessionAnalyzer2.mq5
    Main indicator that builds session data, calculates session statistics, displays session boundaries and development levels, and presents the relationship analysis.
    TimeZone.mqh
    Provides the timezone layer used to resolve session-local times, apply date-specific DST rules, and map them directly onto broker server time via the occurrence date's net offset.
    MQL5.zip  Package containing the source files in their required folder structure. Unpack the archive into the terminal's MQL5 data folder so that SessionAnalyzer2.mq5 is placed under MQL5/Indicators/SessionAnalyzer/V2 and TimeZone.mqh under MQL5/Include/TimeZone/V1. 


    Conclusion

    This part extended session analysis from fixed broker-server windows to a timezone-aware framework with a measurable relationship layer. Each configured session defines its own local Start/End hours and timezone profile. For each occurrence date, BuildSessionWindow() calculates the corresponding server-time boundaries, BuildSessionData() collects bar-based session statistics, and BuildRelationships() produces comparison records for sessions sharing the same local occurrence date.

    The resulting measurements are exposed through the indicator panel, allowing you to inspect session interactions, reverse Reference and Comparison roles, and examine the calculated relationships without relying solely on chart objects.

    Important limitations remain. Session membership follows bar-opening timestamps, broker feeds may produce different OHLC-derived measurements, and MetaTrader 5 does not provide a complete historical record of broker UTC offsets. The BROKER_DST_AUTO setting uses a cached offset, while explicit broker DST schemes apply modeled seasonal rules. Server-offset initialization may also be delayed when terminal clocks are unavailable during a tickless weekend. These constraints should be considered when interpreting historical results.

    With this temporal foundation established, subsequent parts can explore aggregation across session occurrences, statistical summaries of session relationships, and the evaluation of session-derived conditions for trading strategies.

    References

    Attached files |
    MQL5.zip (23.25 KB)
    Features of Custom Indicators Creation Features of Custom Indicators Creation
    Creation of Custom Indicators in the MetaTrader trading system has a number of features.
    Building A Candlestick Trend Constraint Model (Part 13): Normalizing and Scoring Lower-Timeframe Trigger Quality Building A Candlestick Trend Constraint Model (Part 13): Normalizing and Scoring Lower-Timeframe Trigger Quality
    The EA now computes an equal-weight score at M1 confirmation from normalized body fraction, breakout relative to C's range, and elapsed time, while excursions are measured only afterward. We refine processing of closed minutes and export a typed CSV that distinguishes completed observations from snapshots. The result is a clearer, reproducible basis for analyzing confirmed-signal behavior.
    Features of Experts Advisors Features of Experts Advisors
    Creation of expert advisors in the MetaTrader trading system has a number of features.
    Neural Networks in Practice: ReLU Function Graph Neural Networks in Practice: ReLU Function Graph
    We will create an indicator written entirely in MQL5 to display the activation function and its derivative on the chart, using ReLU as an example. We will explain how derivatives affect the choice of activation function and what precautions are needed at nondifferentiable points. Readers will be able to interactively visualize the curves and gain a practical foundation for deciding when to use certain activation functions and when not to.