preview
Broker Time vs Real Market Time — Building a DST-Aware Session Clock in MQL5

Broker Time vs Real Market Time — Building a DST-Aware Session Clock in MQL5

MetaTrader 5 — Trading systems |
411 0
Ushana Kevin Iorkumbul
Ushana Kevin Iorkumbul

Introduction

Many session-filtered Expert Advisors have a hidden vulnerability. It raises no error in the Experts log, and in a backtest it is easy to miss. The EA compiles, deploys, and behaves as expected for months. Then, after a clock change, the session filter that should wait for the London open fires an hour early, or trades that should close before the New York close stay open an extra hour. The cause is rarely the trading logic. It is the relationship between broker server time and real market time, which shifts twice a year with daylight saving time (DST).

Most brokers move their server clock for DST, so the value of TimeCurrent() shifts by one hour. A session hour hardcoded in broker time stays correct only while the market it represents moves its clock together with the broker. The US and the EU do not switch on the same dates. The US moves to DST on the second Sunday of March and back on the first Sunday of November. The EU moves on the last Sunday of March and the last Sunday of October. This creates two mismatch windows: two to three weeks in spring, and exactly one week from the last Sunday of October to the first Sunday of November. In these windows London and New York are 4 hours apart instead of 5, and a filter that is right for most of the year can be one hour off.

This article presents a practical solution from the ground up. It explains why TimeCurrent() alone is not enough for session filtering and walks through the arithmetic of converting broker time to market session time. By the end, you will have a reusable MQL5 include file with the function GetMarketSessionHour(), which computes the session hour at runtime for London, New York, and Tokyo, so you no longer adjust session hours by hand after each clock change. You state the DST schedule and the winter offset of your broker once. The file supports the EU schedule, the US schedule, and a fixed offset.


Section 1: Understanding the Time Problem

What TimeCurrent() Actually Returns

There is a common technical misunderstanding many MQL5 developers have about TimeCurrent(). Many MQL5 developers treat TimeCurrent() as though it represents the actual present time. But in reality, TimeCurrent() denotes the last known server time, which is updated only when a new price tick arrives. TimeCurrent() is broker server time. The value TimeCurrent() returns reflects the timezone the broker's server is configured to operate on.

Relying on the TimeCurrent() function alone to know the real global time (UTC) is not enough. Most brokers manually shift their clock twice a year to stay in line with DST. However, TimeCurrent() is blind to DST changes and will simply return the broker server time when those changes occur. So if a trader hardcodes an EA to trade at 8am UTC without properly accounting for broker time offset, a shift to DST will cause trades to open an hour earlier, causing the trader to miss the intended execution time.

To mitigate this issue, MQL5 provides a TimeGMT() function. TimeGMT() returns the current UTC time. By comparing TimeGMT() (UTC) with TimeCurrent() (server time), the program can determine the broker server's UTC offset.

This solution works well in live trading, but it fails in the Strategy Tester because TimeGMT() and TimeCurrent() both return the same broker server time in the Strategy Tester. If a trader tries to run a backtest using these two functions to calculate the time offset, they will get the wrong answer because both functions are set to the same value; the difference between them is always zero in a backtest. 

The DST Calendar Mismatch—Where It Gets Worse

Most traders are aware that clocks change twice a year, but many traders forget that it happens at different times around the world. This gap creates a timing mismatch that affects session filters built around cross-regional pairs.

The following table shows the two DST schedules and the fixed offset that the include file in this article supports:

Schedule DST start DST end Typical broker offset
EU Last Sunday of March Last Sunday of October GMT+2 / GMT+3
US Second Sunday of March First Sunday of November GMT+2 / GMT+3
Fixed offset No shift No shift Constant, for example GMT+0

Table 1: DST switch dates of the supported schedules. The EU and US schedules differ for 2 to 3 weeks in March and for 1 week from the last Sunday of October to the first Sunday of November. Southern hemisphere schedules, such as the Australian one, are not supported. Figure 1 shows these periods for 2026.

When the US and EU Schedules Disagree

US and EU mismatch windows

Figure 1: DST periods of the US and EU schedules in 2026. The shaded windows, 8 to 29 March and 25 October to 1 November, are the weeks in which the two schedules differ.

The most significant timing risk occurs in March. The US shifts to DST on the second Sunday of March, while the EU shifts to DST on the last Sunday of March, creating a discrepancy that lasts between two and three weeks.

When a trader uses a broker that operates on the US DST schedule, the broker's clock shifts forward by one hour in early March. During this period, a broker on the US schedule jumps from GMT+2 to GMT+3, while the London market remains at 08:00 UTC. Because of this, if a trader has a hardcoded command to trade at '10:00 AM', it executes at 07:00 UTC instead of the correct 08:00 UTC, which is an hour before the London market actually opens.

In late October and early November the mismatch returns. The EU goes back to standard time on the last Sunday of October, while the US stays on DST until the first Sunday of November. This window is exactly one week long. For a broker on the US schedule, the broker is still on DST in that week, but London is already back on standard time, so the London open moves from broker hour 10 to broker hour 11. A hardcoded 10 is then one hour early. 

Why the Problem Is Easy to Miss in a Backtest

The historical data of the Strategy Tester carries the time shifts of the broker. A filter that uses a fixed broker hour therefore behaves in the tester as it does in live trading: it trades the same broker hours, and in the weeks of a mismatch it trades a different real session hour. The tester does not hide the shift. It simply does not know which real session you intended.

The effect becomes visible only when you convert the trade times to UTC or to the local time of the market and compare them with the intended session, which most test reports do not do. Without that comparison, the affected weeks look like ordinary variance. News, spreads, rollover, and liquidity can produce the same picture, so check the trade times in UTC before you blame the clock change. 


Section 2: Detecting the Broker's DST State at Runtime

The Calculation Strategy

MQL5 has no function that returns the time zone of a broker or its DST state. Two approaches are practical. The first compares TimeCurrent() with TimeGMT(). It shows the current offset in live trading, but it does not work in the Strategy Tester, and it cannot tell the EU schedule from the US schedule for most of the year, because both give the same value. The second calculates the offset from the date, using the calendar rules of a schedule that the trader states. It does not call TimeGMT(), so it works in the same way in live trading and in the Strategy Tester.

This article uses the second approach. The include file does not detect the schedule. It takes two inputs, the DST schedule that the broker follows and the broker's winter offset from GMT, and calculates the offset for any date from them. If either input is wrong, every result is wrong. The script SessionDetect.mq5, described in Section 4 (Determining Your Broker's DST Schedule), estimates the schedule from the history. It also prints the current offset from TimeTradeServer() and TimeGMT() for comparison. 

Finding the Nth Sunday of Any Month

The US and EU DST rules are defined in terms of a particular Sunday of a particular month. In the US, the switch happens on the second Sunday of March and the first Sunday of November. In Europe, the switch happens on the last Sunday of March and the last Sunday of October. To calculate these dates, define a function that returns the Nth Sunday for a given month and year.

The logic works as follows: it finds the first day of the target month, computes the day of the week that first day falls on, calculates the gap from the first day to first Sunday, then it adds 7 days increments until it reaches the Nth occurrence. For last Sunday, it finds the first Sunday and keeps adding weeks until the next one would fall outside the month.

//+------------------------------------------------------------------+
//| Returns the datetime of the Nth Sunday in a given month and year |
//+------------------------------------------------------------------+
datetime GetNthSunday(int year,int month,int n)
  {
   const int SECONDS_IN_DAY = 86400;
   MqlDateTime dt;
   dt.year = year;
   dt.mon  = month;
   dt.day  = 1;
   dt.hour = 2;   // 02:00, an approximation of the switch time
   dt.min  = 0;
   dt.sec  = 0;

   datetime first_day = StructToTime(dt);
   TimeToStruct(first_day,dt);

//--- day_of_week: 0=Sunday, 1=Monday ... 6=Saturday
   int days_to_sunday = (7-dt.day_of_week)%7;
   datetime first_sunday = first_day+days_to_sunday*SECONDS_IN_DAY;

   if(n > 0)
     {
      return(first_sunday+(n-1)*7*SECONDS_IN_DAY); // Nth Sunday
     }

//--- n == 0: find the LAST Sunday
   datetime last_sunday = first_sunday;
   while(true)
     {
      datetime next = last_sunday+7*SECONDS_IN_DAY;
      TimeToStruct(next,dt);

      if(dt.mon != month)
        {
         break;
        }
      last_sunday = next;
     }

   return(last_sunday);
  }

The parameter n=0 is used as a convention to request the last Sunday of the month, while n=1 through n=4 return the first through fourth Sunday respectively. This covers the four switch dates of the EU and US schedules.

The IsBrokerOnSummerTime() Function

With a reliable way to get a specific Sunday (using the GetNthSunday() function), the DST test becomes easy. Once the relevant Sunday is determined, the code defines the "DST window" by calculating the start and end dates of DST for that specific year. The switch is placed at 02:00 on the Sunday, which is an approximation (see Limitations of the Include File in Section 5). It then looks at the current trade or timestamp and checks if it falls within that window. It uses the defined window to tell if a date falls within the DST period or not. 

enum DST_SCHEDULE_ENUM
  {
   DST_SCHEDULE_NONE = 0, // Broker uses fixed UTC offset year-round
   DST_SCHEDULE_EU   = 1, // Broker follows EU schedule (last Sun Mar / last Sun Oct)
   DST_SCHEDULE_US   = 2  // Broker follows US schedule (2nd Sun Mar / 1st Sun Nov)
  };

input DST_SCHEDULE_ENUM InpBrokerDST    = DST_SCHEDULE_EU; // Broker DST schedule
input int               InpBrokerWinter = 2;               // Broker winter GMT offset

//+------------------------------------------------------------------+
//| IsBrokerOnSummerTime: returns true if broker is in DST           |
//+------------------------------------------------------------------+
bool IsBrokerOnSummerTime(datetime t=0)
  {
   if(t == 0)
     {
      t = TimeCurrent();
     }

   if(InpBrokerDST == DST_SCHEDULE_NONE)
     {
      return(false);
     }

   MqlDateTime dt;
   TimeToStruct(t,dt);
   int y = dt.year;
   datetime s,e;

   if(InpBrokerDST == DST_SCHEDULE_EU)
     {
      s = GetNthSunday(y,3,0);   // Last Sun March
      e = GetNthSunday(y,10,0);  // Last Sun October
     }
   else
     {
      s = GetNthSunday(y,3,2);   // 2nd Sun March
      e = GetNthSunday(y,11,1);  // 1st Sun November
     }

   return(t >= s && t < e);
  }

This function is the brain of the entire solution. It takes the time to test, which is the current server time when the argument is omitted, and it reads the DST schedule from the input InpBrokerDST. It does not contain any hardcoded offset values. Every year, it automatically calculates the switch dates using the calendar arithmetic in GetNthSunday(). It works exactly the same in the Strategy Tester and in live trading because it never calls TimeGMT() or TimeDaylightSavings().

A note on names: In prose, this article uses “DST” for the period in which a schedule has moved the clock forward by one hour, and “winter” for the period outside it. A few identifiers in the code keep the word “summer”: IsBrokerOnSummerTime(), SummerAt(), and the local variables lon_summer and ny_summer. In each case, “summer” means “inside the DST period of the schedule”, not a calendar season. The names are kept as they are because they are short and used throughout the code. IsBrokerOnSummerTime() returns true when a time falls inside the DST period of the selected schedule, and it always returns false for DST_SCHEDULE_NONE.


Section 3: Building the GetMarketSessionHour() Function

The Session Hour Conversion Logic

With the broker DST test working, the next step is converting a broker-time hour into the correct market session hour. The conversion requires two things:  the broker's base UTC offset (e.g., GMT+2 in winter), and the broker's DST status, that is whether the broker is currently on its DST offset or its winter offset. 

The table below shows the session hours in UTC and in broker time for a broker on the EU schedule with a winter offset of 2. It compares a winter date, a date in the March window in which the US is on DST and the EU is not, and a DST date.

Session 15 Jan 2026 (GMT+2) 16 Mar 2026 (GMT+2) 15 Jul 2026 (GMT+3)
London open 08:00 → 10:00 08:00 → 10:00 07:00 → 10:00
New York open 13:00 → 15:00 12:00 → 14:00 12:00 → 15:00
Tokyo open 00:00 → 02:00 00:00 → 02:00 00:00 → 03:00
New York close 22:00 → 00:00 21:00 → 23:00 21:00 → 00:00

Table 2: Session hours in UTC and in broker time for a broker on the EU schedule with a winter offset of 2. The values were calculated with the logic of GetMarketSessionHour() and were not read from a terminal. The date 28 Oct 2026 gives the same values as 16 Mar 2026.

On this broker the London open stays at broker hour 10 all year, because London and the broker both follow the EU schedule. The New York open is at 15 for most of the year, but at 14 in the March window and in the last week of October, so a hardcoded 15 is one hour late in those weeks. Tokyo has no DST, so its broker hour changes whenever the broker changes its own clock. A fixed broker hour is therefore safe for one session and wrong for another, which is why the function computes each session on its own.

Session Hours in Broker Time Through the Year

Session Hours in Broker Time Through the Year

Figure 2: Session hours in broker time for a broker on the EU schedule with a winter offset of 2. The values were calculated with the logic of GetMarketSessionHour() for 15 January, 16 March, and 15 July 2026. The bars run from the session open to the session close. In the March window, the New York session starts and ends one hour earlier.

Since brokers all use different clocks, UTC (Coordinated Universal Time) acts as the common middle ground for the math. To convert a broker's time to real UTC market hours, the code subtracts the broker's offset to find the global UTC time. Once it has the clean UTC time, it simply applies the fixed geographic offset for the specific market you are interested in. New York is always behind UTC, you subtract 5 hours in winter (EST) or 4 hours during DST (EDT), London is usually the same as UTC in winter (+0), but adds an hour (+1) during DST, and Tokyo is consistently 9 hours ahead because it ignores DST entirely.

The terms in this article are used in one fixed way. UTC time is the common reference. Broker time is the server time that TimeCurrent() returns. Market local time is the clock of the city of a session. A session hour always means an hour of broker time, as GetMarketSessionHour() returns it.

The sessions are conventions chosen for this article: London 08:00 to 17:00 London time, New York 08:00 to 17:00 New York time, and Tokyo 09:00 to 18:00 Tokyo time. Other sources define sessions differently, for example by exchange hours or by liquidity windows. To use other hours, change the values in GetMarketSessionHour().

The following code provides an easy-to-use tool that converts global market hours into your broker's local time. By setting the DST schedule and the winter offset of your broker (the winter offset is usually GMT+2) and choosing a market, the function automatically handles the complex DST math and tells you the broker hour in which that session begins.

The Complete Include File

//+------------------------------------------------------------------+
//|                                                 SessionClock.mqh |
//|             DST-aware market session hour calculator for MQL5    |
//|             Place this file in MQL5/Include/SessionClock/        |
//+------------------------------------------------------------------+
#property strict

//+------------------------------------------------------------------+
//| ENUMS                                                            |
//+------------------------------------------------------------------+
enum DST_SCHEDULE_ENUM
  {
   DST_SCHEDULE_NONE = 0, // Broker uses fixed UTC offset year-round
   DST_SCHEDULE_EU   = 1, // Broker follows EU schedule
   DST_SCHEDULE_US   = 2  // Broker follows US schedule
  };

enum SESSION_ENUM
  {
   SESSION_LONDON   = 0,
   SESSION_NEWYORK  = 1,
   SESSION_TOKYO    = 2
  };

//--- Global inputs
input DST_SCHEDULE_ENUM InpBrokerDST    = DST_SCHEDULE_EU; // Broker DST schedule
input int               InpBrokerWinter = 2;               // Broker winter GMT offset

//+------------------------------------------------------------------+
//| GetNthSunday: returns datetime of the Nth Sunday in month/year   |
//+------------------------------------------------------------------+
datetime GetNthSunday(int year,int month,int n)
  {
   const int SECONDS_IN_DAY = 86400;
   MqlDateTime dt = {0};
   dt.year = year;
   dt.mon  = month;
   dt.day  = 1;
   dt.hour = 2;

   datetime first_day = StructToTime(dt);
   TimeToStruct(first_day,dt);

   int days_to_sun = (7-dt.day_of_week)%7;
   datetime first_sun = first_day+days_to_sun*SECONDS_IN_DAY;

   if(n > 0)
     {
      return(first_sun+(n-1)*7*SECONDS_IN_DAY);
     }

   datetime last = first_sun;
   while(true)
     {
      datetime next = last+7*SECONDS_IN_DAY;
      TimeToStruct(next,dt);

      if(dt.mon != month)
        {
         break;
        }
      last = next;
     }

   return(last);
  }

//+------------------------------------------------------------------+
//| IsBrokerOnSummerTime: returns true if broker is in DST           |
//+------------------------------------------------------------------+
bool IsBrokerOnSummerTime(datetime t=0)
  {
   if(t == 0)
     {
      t = TimeCurrent();
     }

   if(InpBrokerDST == DST_SCHEDULE_NONE)
     {
      return(false);
     }

   MqlDateTime dt;
   TimeToStruct(t,dt);
   int y = dt.year;
   datetime s,e;

   if(InpBrokerDST == DST_SCHEDULE_EU)
     {
      s = GetNthSunday(y,3,0);   // Last Sun March
      e = GetNthSunday(y,10,0);  // Last Sun October
     }
   else
     {
      s = GetNthSunday(y,3,2);   // 2nd Sun March
      e = GetNthSunday(y,11,1);  // 1st Sun November
     }

   return(t >= s && t < e);
  }

//+------------------------------------------------------------------+
//| GetBrokerOffset: returns broker's current GMT offset in hours    |
//+------------------------------------------------------------------+
int GetBrokerOffset(datetime t=0)
  {
   if(t == 0)
     {
      t = TimeCurrent();
     }

   int dst_add = 0;
   if(IsBrokerOnSummerTime(t))
     {
      dst_add = 1;
     }

   return(InpBrokerWinter+dst_add);
  }

//+------------------------------------------------------------------+
//| GetMarketSessionHour: returns session open/close in broker time  |
//+------------------------------------------------------------------+
int GetMarketSessionHour(SESSION_ENUM session,bool is_close=false,datetime t=0)
  {
   const int SECONDS_IN_HOUR = 3600;

   if(t == 0)
     {
      t = TimeCurrent();
     }

   int broker_offset = GetBrokerOffset(t);
   datetime utc_now  = t-broker_offset*SECONDS_IN_HOUR;

   MqlDateTime utc_dt;
   TimeToStruct(utc_now,utc_dt);
   int y = utc_dt.year;

//--- EU DST for London
   datetime lon_dst_s  = GetNthSunday(y,3,0);
   datetime lon_dst_e  = GetNthSunday(y,10,0);
   bool     lon_summer = (utc_now >= lon_dst_s && utc_now < lon_dst_e);

//--- US DST for New York
   datetime ny_dst_s = GetNthSunday(y,3,2);
   datetime ny_dst_e = GetNthSunday(y,11,1);
   bool ny_summer = (utc_now >= ny_dst_s && utc_now < ny_dst_e);

   int utc_open,utc_close;

   switch(session)
     {
      case SESSION_LONDON:
         utc_open  = lon_summer ? 7 : 8;
         utc_close = lon_summer ? 16 : 17;
         break;
      case SESSION_NEWYORK:
         utc_open  = ny_summer ? 12 : 13;
         utc_close = ny_summer ? 21 : 22;
         break;
      case SESSION_TOKYO:
      default:
         utc_open  = 0;
         utc_close = 9;
         break;
     }

   int result = (is_close ? utc_close : utc_open)+broker_offset;

//--- Normalise to 0-23
   result = result%24;
   if(result < 0)
     {
      result += 24;
     }

   return(result);
  }

//+------------------------------------------------------------------+
//| IsHourInWindow: true if hour lies in [start, end), also when     |
//| the window crosses midnight (for example start 10, end 0)        |
//+------------------------------------------------------------------+
bool IsHourInWindow(const int hour,const int start,const int end)
  {
   if(start <= end)
     {
      return(hour >= start && hour < end);
     }
   return(hour >= start || hour < end);
  }


Section 4: Using the Include File in a Real EA

Integrating SessionClock.mqh

Integrating the include file into an existing EA involves three steps: first, copy the SessionClock.mqh into the MQL5/Include/SessionClock/ folder, then link it by adding the #include directive at the top of the EA file, and lastly, replace the manual time-check logic in OnTick() with the new function (GetMarketSessionHour() function) call. New inputs for DST and winter offsets now handle time calculations, making the previous GMT offset input obsolete.

//--- At the top of your EA .mq5 file:
#include <SessionClock/SessionClock.mqh>

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   datetime now = TimeCurrent();

   int lon_open  = GetMarketSessionHour(SESSION_LONDON,false,now);
   int ny_close  = GetMarketSessionHour(SESSION_NEWYORK,true,now);

   MqlDateTime dt;
   TimeToStruct(now,dt);
   int current_hour = dt.hour;

//--- Allow trading from London open to NY close only
//--- the window can cross midnight in broker time
   bool in_session = IsHourInWindow(current_hour,lon_open,ny_close);

//--- Outside trading window - do nothing
   if(!in_session)
     {
      return;
     }

//--- Your strategy logic continues here...
  }

GetMarketSessionHour() returns only a whole hour, so a window whose end is 0 or 1 crosses midnight. For example, with a winter offset of 2 the New York close is hour 0, so the window runs from 10 to 0. IsHourInWindow() handles that case.

The trading strategy does not change. The source of the timing data changes. The trader no longer needs to use fixed, hardcoded numbers that might be wrong when the clocks change, the program now checks for the correct time every single time the market moves (on tick). The EA automatically adjusts for DST so the trader does not have to update their code manually twice a year.  This holds as long as InpBrokerDST and InpBrokerWinter match the broker.

Determining Your Broker's DST Schedule

If you are unsure which schedule your broker follows, the include file cannot tell you, because it only applies the schedule that you state. The script SessionDetect.mq5 estimates it from the history of the server. It assumes that the FX week reopens on Sunday at 17:00 New York time. A server that follows New York shows the first bar of the week at the same server time all year. A server on the EU schedule shows it one hour earlier in the weeks in which the US is on DST and the EU is not. A server with a fixed offset shows it one hour apart between DST and winter.

The script reads H1 bars and takes the first bar after every gap of at least 36 hours, occurring on a Sunday or Monday. It tests 27 candidates: three schedules (none, EU, US) times winter offsets from -3 to 5. For each candidate it converts the weekly open to GMT, then to New York time, and counts the weeks in which the result is 17:00. It reports a result only when the best candidate leads the second one by at least 3 weeks and matches at least 80 percent of the weeks. Use a forex symbol, because CFDs on indexes and commodities do not reopen at that time.

InpDays = 400 covers more than one full year, so that the history holds both periods in which the EU and US schedules differ, which are March and the time from late October to early November, whatever the date of the run. The script stops when InpDays is below 60, and it prints a note when the history holds no week in which the schedules differ. The depth of the history depends on your broker.

//+------------------------------------------------------------------+
//|                                                SessionDetect.mq5 |
//|                           Estimates the DST schedule of a server |
//+------------------------------------------------------------------+
#property script_show_inputs

//--- GetNthSunday() and DST_SCHEDULE_ENUM come from the include file.
//--- Its two broker inputs are shown in the dialog but are not used here.
#include <SessionClock/SessionClock.mqh>

//--- inputs
input string InpSymbol = "EURUSD"; // forex symbol (empty = chart symbol)
input int    InpDays   = 400;      // days of H1 history to scan

//--- candidate winter offsets, in hours, from -3 to 5
#define WINTER_MIN   -3
#define WINTER_MAX    5
#define WINTER_COUNT  (WINTER_MAX-WINTER_MIN+1)
#define SEC_HOUR      3600

//+------------------------------------------------------------------+
//| SummerAt: DST test for a given schedule                          |
//+------------------------------------------------------------------+
bool SummerAt(const datetime t,const DST_SCHEDULE_ENUM schedule)
  {
   if(schedule == DST_SCHEDULE_NONE)
     {
      return(false);
     }

   MqlDateTime dt;
   TimeToStruct(t,dt);
   datetime s,e;

   if(schedule == DST_SCHEDULE_EU)
     {
      s = GetNthSunday(dt.year,3,0);   // last Sunday of March
      e = GetNthSunday(dt.year,10,0);  // last Sunday of October
     }
   else
     {
      s = GetNthSunday(dt.year,3,2);   // second Sunday of March
      e = GetNthSunday(dt.year,11,1);  // first Sunday of November
     }
   return(t >= s && t < e);
  }

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   string symbol = InpSymbol;
   if(symbol == "")
     {
      symbol = _Symbol;
     }
   if(!SymbolSelect(symbol,true) || InpDays < 60)
     {
      Print("Cannot select the symbol, or InpDays is below 60.");
      return;
     }

//--- read H1 bars
   MqlRates rates[];
   const datetime to   = TimeCurrent();
   const datetime from = to-(datetime)InpDays*86400;
   const int      n    = CopyRates(symbol,PERIOD_H1,from,to,rates);
   if(n < 500)
     {
      PrintFormat("Only %d H1 bars of %s are loaded. Open an H1 chart, scroll back, and run again.",n,symbol);
      return;
     }

//--- first bar of each week: a gap of at least 36 hours, on a Sunday or Monday
   datetime opens[];
   for(int i = 1; i < n; i++)
     {
      if(rates[i].time-rates[i-1].time < 36*SEC_HOUR)
        {
         continue;
        }
      MqlDateTime dt;
      TimeToStruct(rates[i].time,dt);
      if(dt.day_of_week > 1)
        {
         continue;
        }
      const int k = ArraySize(opens);
      ArrayResize(opens,k+1);
      opens[k] = rates[i].time;
     }
   const int weeks = ArraySize(opens);
   if(weeks < 20)
     {
      PrintFormat("Only %d weekly opens found. Use a longer history or a forex symbol.",weeks);
      return;
     }

//--- weeks in which the EU and US schedules disagree
   int gap_weeks = 0;
   for(int w = 0; w < weeks; w++)
     {
      if(SummerAt(opens[w],DST_SCHEDULE_EU) != SummerAt(opens[w],DST_SCHEDULE_US))
        {
         gap_weeks++;
        }
     }

//--- score every candidate: the weekly open must be 17:00 in New York
   const int count = 3*WINTER_COUNT;
   int       hits[];
   ArrayResize(hits,count);
   for(int c = 0; c < count; c++)
     {
      const DST_SCHEDULE_ENUM schedule = (DST_SCHEDULE_ENUM)(c/WINTER_COUNT);
      const int winter = WINTER_MIN+c%WINTER_COUNT;
      hits[c] = 0;
      for(int w = 0; w < weeks; w++)
        {
         const int      broker_offset = winter+(SummerAt(opens[w],schedule) ? 1 : 0);
         const datetime gmt           = opens[w]-(datetime)broker_offset*SEC_HOUR;
         const int      ny            = SummerAt(gmt,DST_SCHEDULE_US) ? 4 : 5;
         MqlDateTime dt;
         TimeToStruct(gmt-(datetime)ny*SEC_HOUR,dt);
         if(dt.hour == 17 && dt.min == 0)
           {
            hits[c]++;
           }
        }
     }

//--- print the best five
   PrintFormat("=== DST SCHEDULE CHECK (heuristic) for %s ===",symbol);
   PrintFormat("Weekly opens: %d, of them in weeks where EU and US differ: %d.",weeks,gap_weeks);
   PrintFormat("Current server minus GMT: %d hours (needs a correct PC clock).",
               (int)MathRound((double)(TimeTradeServer()-TimeGMT())/SEC_HOUR));
   int first = -1, second = -1;
   bool used[];
   ArrayResize(used,count);
   ArrayInitialize(used,false);
   for(int r = 0; r < 5; r++)
     {
      int best = -1;
      for(int c = 0; c < count; c++)
        {
         if(!used[c] && (best < 0 || hits[c] > hits[best]))
           {
            best = c;
           }
        }
      used[best] = true;
      if(r == 0)
        {
         first = best;
        }
      if(r == 1)
        {
         second = best;
        }
      const DST_SCHEDULE_ENUM schedule = (DST_SCHEDULE_ENUM)(best/WINTER_COUNT);
      PrintFormat("#%d  schedule %s, winter offset %d: %d of %d weekly opens at 17:00 New York.",
                  r+1,EnumToString(schedule),WINTER_MIN+best%WINTER_COUNT,hits[best],weeks);
     }

//--- verdict: a clear lead and a high match rate
   const DST_SCHEDULE_ENUM top = (DST_SCHEDULE_ENUM)(first/WINTER_COUNT);
   if(hits[first]-hits[second] >= 3 && hits[first]*10 >= weeks*8)
     {
      PrintFormat("Result: set InpBrokerDST = %s and InpBrokerWinter = %d.",
                  EnumToString(top),WINTER_MIN+first%WINTER_COUNT);
     }
   else
     {
      Print("Result: not clear. Try another forex symbol or a longer history.");
     }
   if(gap_weeks == 0)
     {
      Print("Note: the history holds no week where EU and US differ, so those two cannot be told apart.");
     }
  }

SessionDetect.mq5 Experts tab output:

=== DST SCHEDULE CHECK (heuristic) for EURUSD ===
Weekly opens: 57, of them in weeks where EU and US differ: 4.
Current server minus GMT: 3 hours (needs a correct PC clock).
#1  schedule DST_SCHEDULE_US, winter offset 2: 57 of 57 weekly opens at 17:00 New York.
#2  schedule DST_SCHEDULE_EU, winter offset 2: 53 of 57 weekly opens at 17:00 New York.
#3  schedule DST_SCHEDULE_NONE, winter offset 3: 39 of 57 weekly opens at 17:00 New York.
#4  schedule DST_SCHEDULE_NONE, winter offset 2: 18 of 57 weekly opens at 17:00 New York.
#5  schedule DST_SCHEDULE_EU, winter offset 3: 4 of 57 weekly opens at 17:00 New York.
Result: set InpBrokerDST = DST_SCHEDULE_US and InpBrokerWinter = 2.

The EU candidate misses exactly the 4 weeks in which the two schedules differ, which is the evidence for the US schedule on this server. The result is a heuristic and applies to this server only.

How the Detector Ranks the Candidates

Detector Scores

Figure 3: The five best candidates of SessionDetect.mq5 for EURUSD, taken from the output of the run on October 20, 2026. The US schedule with winter offset 2 matches all 57 weekly opens at 17:00 New York time. The EU schedule with the same offset misses the 4 weeks in which the two schedules differ.


Section 5: Edge Cases and Practical Considerations

The Tokyo Session — No DST, but Still Affected

Japan does not observe DST, which means the Tokyo session always opens at 00:00 UTC and closes at 09:00 UTC. But your broker time offset can affect hardcoded hours targeted at Tokyo sessions. If a broker operates on GMT+2/GMT+3, Tokyo opens at 02:00 broker time in winter and 03:00 broker time during DST. Relying on a function (such as the GetMarketSessionHour() function) that converts UTC to broker time is a reliable way to make sure you trade when the Tokyo banks open.

Session Overlaps and the Two Time Zones Window

Most trading strategies are built around the London-New York Market overlap, which runs from 13:00 to 17:00 UTC (12:00 to 17:00 UTC in the weeks in which only the US is on DST) and represents the highest liquidity period of the trading day. Since both cities have different DST rules, the 'meeting point' of their trading hours can be a moving target. The include file returns the session hours for the date that you pass, so the overlap follows both DST rules, including the weeks in which the US and EU schedules differ. The values are whole hours.

//--- Compute the London-NY overlap window in broker time
   int overlap_start = GetMarketSessionHour(SESSION_NEWYORK,false); // NY opens after London
   int overlap_end   = GetMarketSessionHour(SESSION_LONDON,true);   // London closes before NY

   MqlDateTime dt;
   TimeToStruct(TimeCurrent(),dt);

   bool in_overlap = IsHourInWindow(dt.hour,overlap_start,overlap_end);

With the session definitions of this article, New York always opens after London and London always closes before New York, so the overlap runs from the New York open to the London close.

Brokers on Fixed UTC — When DST_SCHEDULE_NONE Is the Right Setting

There are a few brokers that operate on a fixed UTC offset year-round and do not adjust their server time for DST. These brokers explicitly position themselves as 'GMT brokers' or 'UTC brokers.' For these brokers, InpBrokerDST should be set to DST_SCHEDULE_NONE and InpBrokerWinter should be set to the fixed offset of the server, which is 0 for a GMT broker. IsBrokerOnSummerTime() will return false at all times, and GetBrokerOffset() will always return that fixed offset. 

Even though the broker's server time does not change, the market-open hours for London and New York markets will appear to move on the broker's chart when the market's country changes its time. By setting the DST schedule to DST_SCHEDULE_NONE, you tell the EA that the broker isn't moving, allowing it to correctly calculate when the US or Europe market opens relative to that frozen GMT clock.

Session-filtering is simpler to manage on these brokers because the broker's clock is fixed year-round. The flip side is that hardcoded session hours in an existing EA that was programmed for a GMT+2/GMT+3 broker will need to be adjusted if the trader migrates to a UTC broker. The include file only needs the two inputs to be changed. 

Using the Include File in the Strategy Tester

The entire SessionClock.mqh include file works correctly in the Strategy Tester because it does not call TimeGMT(), which equals the simulated server time in the tester, or TimeDaylightSavings() and TimeGMTOffset(), which depend on the settings of the operating system. All DST calculations are based on TimeCurrent() and calendar arithmetic, both of which are correctly simulated by the tester. The only requirement is that the trader sets InpBrokerDST and InpBrokerWinter to values that reflect the broker's real configuration — the same values they would use in live trading.

When backtesting across a DST switch date, the function switches from the winter offset to the DST offset on the calendar date given by the schedule, with the 02:00 approximation described under Limitations. Table 3 lists the expected values around the 2026 switches for the three supported schedules. Session filters will behave in the backtest as they would in live trading for the same historical period, provided that the two inputs match what the broker used in that period. This is not the case for any approach that relies on TimeGMT() in the tester — those approaches will produce subtly incorrect session timing across every DST boundary in the backtest window.

Date (12:00 broker time) DST_SCHEDULE_EU DST_SCHEDULE_US DST_SCHEDULE_NONE
Fri 06 Mar 2026 +2/10/15 +2/10/15 +2/10/15
Mon 09 Mar 2026 +2/10/14
+3/11/15 +2/10/14
Fri 27 Mar 2026 +2/10/14
+3/11/15
+2/10/14
Mon 30 Mar 2026 +3/10/15
+3/10/15
+2/09/14
Fri 23 Oct 2026 +3/10/15
+3/10/15
+2/09/14
Mon 26 Oct 2026 +2/10/14
+3/11/15
+2/10/14
Fri 30 Oct 2026 +2/10/14
+3/11/15
+2/10/14
Mon 02 Nov 2026
+2/10/15
+2/10/15
+2/10/15

Table 3: Broker GMT offset, London open hour, and New York open hour (written as offset / London / New York) for a broker with winter offset 2, at 12:00 broker time. The values were calculated with the logic of the include file and were not read from a terminal or a tester log. With DST_SCHEDULE_NONE the offset stays at GMT+2, so the broker hours of the sessions move with the real clocks.

Limitations of the Include File

The include file has these limits, and you need to know them before you rely on it:

  • Whole hours only: GetMarketSessionHour() returns an hour from 0 to 23, without minutes and without a date. It cannot tell 00:00 today from 00:00 tomorrow, which is why IsHourInWindow() is needed for windows that cross midnight.
  • Whole-hour offsets only: InpBrokerWinter is an integer, so offsets such as UTC+5:30, UTC+9:30, and UTC+10:30 are not supported.
  • Three models only: The code covers the EU schedule, the US schedule, and a fixed offset. Southern hemisphere schedules, where the DST period crosses the year boundary, are not supported.
  • Approximate switch times: GetNthSunday() places every switch at 02:00 on the Sunday. The real switch is at 01:00 UTC for the EU, and at 07:00 UTC in March and 06:00 UTC in November for the US. The result can be wrong for a few hours on that Sunday, when the FX market is normally closed.
  • Session definitions are conventions: The hours of London, New York, and Tokyo are the ones given in Section 3 (The Session Hour Conversion Logic subsection). CFD and exchange sessions can differ.
  • The inputs are your responsibility: The code cannot check InpBrokerDST and InpBrokerWinter. Check them again after the account moves to another server and after any change in the DST rules or in the policy of the broker.


Conclusion

DST is not a dramatic problem. It does not crash EAs. It does not produce error messages. It sits quietly inside session-filtered strategies. Twice a year, for a few weeks at a time, a session filter can run an hour away from the session it was meant to follow. That is long enough for the affected period to look like ordinary variance, and short enough that most traders never diagnose the cause.

The solution presented in this article addresses the problem at its source, as long as its two inputs are correct. GetMarketSessionHour() computes accurate London, New York, and Tokyo session hours at runtime, accounting for the broker's current DST state and the market's own DST offset independently. It requires the trader to set two inputs, the DST schedule of the broker and the winter offset of the broker from GMT. These need to be checked again if the account moves to another server, or if the broker or the DST rules change.

The broader point is this: session hours are not constants. They shift with the broker's clock, and the broker's clock shifts with the real world twice a year. Any EA that treats session hours as fixed values is making an assumption that breaks on a predictable schedule. The include file in this article makes that assumption unnecessary, as long as its two inputs match the broker.


Programs used in the article:

# Name Type Description
1 SessionClock.mqh Include File DST-aware include file with GetNthSunday(), IsBrokerOnSummerTime(), GetBrokerOffset(), GetMarketSessionHour(), and IsHourInWindow().
2 SessionDetect.mq5
Script Heuristic script that estimates the DST schedule and the winter offset of a server from the weekly opens in H1 history.
3 SessionClock_Source.zip
Zip Archive Zip archive containing all the attached files and their paths relative to the terminal's root folder.


Attached files |
SessionClock.mqh (5.36 KB)
SessionDetect.mq5 (5.97 KB)
Measuring the Probability of Backtest Overfitting of Your MetaTrader 5 Optimization (Part 1): The CSCV Engine in MQL5 Measuring the Probability of Backtest Overfitting of Your MetaTrader 5 Optimization (Part 1): The CSCV Engine in MQL5
This article implements the Probability of Backtest Overfitting in MQL5 with Combinatorially Symmetric Cross-Validation. It shows how to build the bar-by-bar returns matrix of a parameter grid, aggregate it by blocks and rank the in-sample winner out-of-sample across every split of the history. You get a single probability that this winner ends up in the lower half of the grid out-of-sample, along with the MQL5 code to compute it on your own optimizations.
Neural Networks in Trading: Iterative Forecast Refinement Models (Key Components) Neural Networks in Trading: Iterative Forecast Refinement Models (Key Components)
In this article, we explore the inner workings of the RAFT framework — one of the most precise and elegant approaches to analyzing dynamic processes. Step by step, we adapt its idea of iterative refinement to financial time series, laying a solid foundation for a future model. Readers can look forward to an engaging exploration of the architecture, where every component has its own purpose and function.
Fundamentals of Bayesian Inference in Discrete and Continuous Settings: From Theory to Practical Model Implementation Fundamentals of Bayesian Inference in Discrete and Continuous Settings: From Theory to Practical Model Implementation
The article discusses the fundamentals of Bayesian statistics in both discrete and continuous settings. We will progress from classical Bayes’ theorem and simple examples involving coin flips to conjugate distributions and dynamic Bayesian updating, which enables real-time analysis of market quotes. Using the beta-binomial model as an example, a simple change-point detection indicator has been implemented to help identify shifts in market regimes.
From Signal to Trade via a Chain of Agents: LangChain Architecture on Top of MQL5 From Signal to Trade via a Chain of Agents: LangChain Architecture on Top of MQL5
The article describes an architecture in which the MQL5 Expert Advisor (EA) handles only data collection and execution, while the logic is moved to a Python server with three LangChain agents: a signal agent, a news agent, and a risk manager. Agents process the WebSocket request sequentially; if any agent rejects it, "hold" is returned. Decisions and actual PnL are stored in SQLite, forming memory and statistics. The reader will receive an interaction diagram, a command protocol, and a feedback approach.