Broker GMT offset in the Strategy Tester: TimeGMT() is server time there, so estimate it from the weekly close (code)

Any EA that works with fixed session times (Tokyo fixing, London open, New York close) needs the broker's GMT offset, the difference between server time and UTC. Live, TimeTradeServer() - TimeGMT() gives it. In the Strategy Tester it does not: the documentation says that during testing TimeGMT() is always equal to TimeTradeServer(), the simulated server time. So a backtest that uses TimeGMT() silently runs every session at the wrong hour, and summer and winter time make it worse.

What I use instead is the weekly close. FX (and gold and most CFDs) closes on Friday at 17:00 New York time, so the last bar before the weekend gap tells you where 17:00 New York sits in server time. For each weekend of the last 45 days the code finds the whole-hour offset that puts that close at 17:00 New York, with US daylight saving time applied, and takes the most frequent one. That makes it robust against a short Friday or a holiday weekend.

// Live: TimeTradeServer() - TimeGMT(). In the tester TimeGMT() always equals the simulated server time, so use the weekly close.
int DowOf(const datetime t)       { MqlDateTime d; TimeToStruct(t, d); return d.day_of_week; }
int MinuteOfDay(const datetime t) { MqlDateTime d; TimeToStruct(t, d); return d.hour * 60 + d.min; }

datetime MakeDate(const int y, const int m, const int d)
{
   MqlDateTime s;
   ZeroMemory(s);
   s.year = y; s.mon = m; s.day = d;
   return StructToTime(s);
}

// n-th weekday dow (0 = Sunday) of a month, n >= 1
datetime NthWeekday(const int year, const int month, const int dow, const int n)
{
   datetime first = MakeDate(year, month, 1);
   int add = (dow - DowOf(first) + 7) % 7;
   return first + (datetime)((add + 7 * (n - 1)) * 86400);
}

// US daylight saving time (rules since 2007): 2nd Sunday of March 2:00 to 1st Sunday of November 2:00 local time
bool IsUSDST(const datetime utc)
{
   MqlDateTime d;
   TimeToStruct(utc, d);
   datetime start = NthWeekday(d.year, 3, 0, 2) + 7 * 3600;    // 2:00 EST = 7:00 UTC
   datetime end   = NthWeekday(d.year, 11, 0, 1) + 6 * 3600;   // 2:00 EDT = 6:00 UTC
   return utc >= start && utc < end;
}

datetime UTCToNewYork(const datetime utc) { return utc - (IsUSDST(utc) ? 4 : 5) * 3600; }

// The last bar before the weekend gap closes at Friday 17:00 New York time (FX, gold and most CFDs).
// For each weekend of the last 45 days, find the whole-hour offset that puts that close at 17:00 New York,
// then take the most frequent one. Returns false when there is no weekend gap (for example 24/7 crypto symbols).
bool BrokerOffsetFromWeekend(const string sym, int &offset_sec)
{
   MqlRates r[];
   ArraySetAsSeries(r, false);
   datetime now = TimeCurrent();
   int n = CopyRates(sym, PERIOD_M5, now - 45 * 86400, now, r);
   if(n < 200) return false;
   int votes[27];
   ArrayInitialize(votes, 0);
   int found = 0;
   for(int i = 1; i < n; i++)
   {
      if(r[i].time - r[i - 1].time < 30 * 3600) continue;      // only the weekend gap
      datetime close_server = r[i - 1].time + 300;                // end of the last M5 bar
      int best_h = 99;
      long best_err = 1000000;
      for(int h = -12; h <= 14; h++)
      {
         datetime ny = UTCToNewYork(close_server - h * 3600);
         int dow = DowOf(ny), mins = MinuteOfDay(ny);
         if(dow != 5 && !(dow == 6 && mins < 60)) continue;      // Friday, or just past midnight
         long err = MathAbs((long)(dow == 6 ? 24 * 60 + mins : mins) - 17 * 60);
         if(err < best_err) { best_err = err; best_h = h; }
      }
      if(best_h != 99 && best_err <= 45) { votes[best_h + 12]++; found++; }
   }
   if(found == 0) return false;
   int bi = 0;
   for(int k = 1; k < 27; k++) if(votes[k] > votes[bi]) bi = k;
   offset_sec = (bi - 12) * 3600;
   return true;
}

// server time - UTC, in seconds
int BrokerOffsetSec(const string sym)
{
   if(!MQLInfoInteger(MQL_TESTER))
      return (int)(MathRound((double)(TimeTradeServer() - TimeGMT()) / 1800.0) * 1800);   // live, rounded to 30 minutes
   int off;
   if(BrokerOffsetFromWeekend(sym, off)) return off;
   return (IsUSDST(TimeCurrent() - 3 * 3600) ? 3 : 2) * 3600;   // fallback: the common "New York close = 00:00 server" convention
}

Notes:

  • Recalculate it once a day, not once in OnInit. The offset changes twice a year, and in a long backtest the code then follows the summer and winter time of each period.
  • It needs a symbol with a weekend gap. On a 24/7 crypto symbol there is none, so read the offset from an FX symbol, or use the fallback: many brokers set server time so that the New York close is 00:00 (GMT+3 in US summer, GMT+2 in winter).
  • Whole hours only. A broker with a half-hour offset would need the loop in 30-minute steps.
  • Give users a manual input as well. A manual offset should always win over the estimate, because some brokers use unusual server times.

If your broker's weekly close does not land at 17:00 New York, I would be interested to hear which one it is.

Not sure if your code is correct considering there are tons of nuances. 

I do not mean to hijack your thread but user amrali has made a few related libraries:

- Determine Broker's Daylight (DST) schedule 

- Local Timezones and Local Session Hours 

- TimeGMT library for the strategy tester 


It is worth while reading the respective threads. A true rabbit hole.

Determine Broker's Daylight (DST) schedule
Determine Broker's Daylight (DST) schedule
  • 2024.03.14
  • www.mql5.com
Script to determine whether your Broker follows the US, UK or AU daylight (DST) schedule.