//+------------------------------------------------------------------+ //| Sessions.mqh| //| Copyright 2026, bandersoft | //| https://www.mql5.com/en/users/fats | //+------------------------------------------------------------------+ //| | //| Broker GMT offset and FX session detection. | //| | //| Read-only: sends no trade requests. | //| | //| Every session boundary an EA cares about is defined in GMT or | //| in a local market time, and every timestamp MetaTrader gives it | //| is in broker server time. The offset between them is not a | //| constant: most brokers sit on New York or Cyprus DST and shift | //| by an hour twice a year, on dates that differ from the ones | //| London and New York use. | //| | //| WHAT IS RELIABLE HERE: | //| - live, connected: the offset is measured, and correct | //| - the session windows themselves, given a correct offset | //| | //| WHAT IS NOT: | //| - the Strategy Tester. TimeGMT() there is derived from the | //| modelled server time rather than the machine clock, so the | //| measured offset can be 0 or simply wrong. DetectGmtOffset | //| reports that it is unreliable rather than returning a | //| confident lie - see SGmtOffset.reliable. | //| - the transition weekend. For a day or two each spring and | //| autumn the broker and the market disagree about the hour. | //| Nothing here can fix that; it can only be known about. | //| | //+------------------------------------------------------------------+ #ifndef STOPFIRST_SESSIONS_MQH #define STOPFIRST_SESSIONS_MQH #include "Defs.mqh" //+------------------------------------------------------------------+ //| The major FX sessions, in GMT | //| | //| Conventional hours, not exchange opening times: FX has no | //| exchange. These are the windows in which each centre's banks are | //| the dominant flow. | //+------------------------------------------------------------------+ enum ENUM_SF_SESSION { SF_SESSION_SYDNEY = 0, // 22:00 - 07:00 GMT SF_SESSION_TOKYO = 1, // 00:00 - 09:00 GMT SF_SESSION_LONDON = 2, // 08:00 - 17:00 GMT SF_SESSION_NEWYORK = 3 // 13:00 - 22:00 GMT }; //+------------------------------------------------------------------+ //| A measured broker offset, with an honest confidence flag | //+------------------------------------------------------------------+ struct SGmtOffset { int hours; // server time minus GMT, e.g. +2 or +3 bool reliable; // false in the tester, or while disconnected string note; // why, when reliable is false }; //--- measured, then reused for up to an hour. Not forever: brokers move //--- their clock with DST twice a year, and an EA running across the //--- change would be an hour off until restarted. A value set by hand //--- is never re-measured. int g_gmtOffsetHours = 0; bool g_gmtOffsetKnown = false; bool g_gmtOffsetReliable = false; bool g_gmtOffsetManual = false; datetime g_gmtOffsetMeasured = 0; // TimeLocal() of the measurement #define STOPFIRST_GMT_REMEASURE_SECONDS 3600 //+------------------------------------------------------------------+ //| Measure the broker server clock against GMT | //| | //| TimeTradeServer() is the broker clock; TimeGMT() is the machine | //| clock converted to GMT. The difference, rounded to the nearest | //| hour, is the offset. Rounding matters: the two are sampled a few | //| microseconds apart and brokers are not second-accurate. | //| | //| Returns: the offset with a reliability flag. In the Strategy | //| Tester, and whenever the terminal is not connected, the | //| flag is false and `hours` should not be trusted. | //+------------------------------------------------------------------+ SGmtOffset DetectGmtOffset(const bool forceRemeasure = false) { SGmtOffset result; result.hours = 0; result.reliable = false; result.note = ""; //--- The tester has no real machine clock relationship: TimeGMT() //--- is derived from the modelled server time, so measuring one //--- against the other is circular. Say so rather than returning //--- a number that looks measured. if(MQLInfoInteger(MQL_TESTER)) { result.note = "Strategy Tester: TimeGMT() is derived from modelled server " "time, so the offset cannot be measured. Set it explicitly " "with SetGmtOffset()."; if(g_gmtOffsetKnown) result.hours = g_gmtOffsetHours; return result; } if(!TerminalInfoInteger(TERMINAL_CONNECTED)) { result.note = "terminal is not connected, so the server clock is stale"; if(g_gmtOffsetKnown) result.hours = g_gmtOffsetHours; return result; } bool fresh = g_gmtOffsetManual || TimeLocal() - g_gmtOffsetMeasured < STOPFIRST_GMT_REMEASURE_SECONDS; if(g_gmtOffsetKnown && g_gmtOffsetReliable && fresh && !forceRemeasure) { result.hours = g_gmtOffsetHours; result.reliable = true; return result; } datetime server = TimeTradeServer(); datetime gmt = TimeGMT(); if(server == 0 || gmt == 0) { result.note = "the terminal returned a zero timestamp"; return result; } //--- round to the nearest hour: the samples are microseconds apart //--- and no broker is second-accurate double rawHours = (double)((long)server - (long)gmt) / 3600.0; result.hours = (int)MathRound(rawHours); result.reliable = true; g_gmtOffsetHours = result.hours; g_gmtOffsetKnown = true; g_gmtOffsetReliable = true; g_gmtOffsetManual = false; g_gmtOffsetMeasured = TimeLocal(); return result; } //+------------------------------------------------------------------+ //| State the offset by hand | //| | //| The only way to get session logic right in the Strategy Tester. | //| Take the value from a live run of DetectGmtOffset, or from the | //| broker's specification, and set it in OnInit. | //| | //| Returns: nothing. | //+------------------------------------------------------------------+ void SetGmtOffset(const int hours) { g_gmtOffsetHours = hours; g_gmtOffsetKnown = true; g_gmtOffsetReliable = true; g_gmtOffsetManual = true; } //+------------------------------------------------------------------+ //| Forget the measured offset so the next call re-measures | //| Returns: nothing. | //+------------------------------------------------------------------+ void ClearGmtOffset(void) { g_gmtOffsetHours = 0; g_gmtOffsetKnown = false; g_gmtOffsetReliable = false; g_gmtOffsetManual = false; g_gmtOffsetMeasured = 0; } //+------------------------------------------------------------------+ //| The offset currently in use | //| Returns: hours of server time ahead of GMT. 0 if never measured | //| or set, which is also a legitimate offset - check | //| DetectGmtOffset().reliable to tell them apart. | //+------------------------------------------------------------------+ int GmtOffsetHours(void) { //--- always through Detect: it answers from the cache while the //--- measurement is fresh and re-measures once it is an hour old DetectGmtOffset(); return g_gmtOffsetHours; } //+------------------------------------------------------------------+ //| Convert a broker timestamp to GMT | //| Returns: the same instant expressed in GMT. | //+------------------------------------------------------------------+ datetime ServerToGmt(const datetime serverTime) { return serverTime - (datetime)(GmtOffsetHours() * 3600); } //+------------------------------------------------------------------+ //| Convert a GMT timestamp to broker time | //| Returns: the same instant expressed on the broker clock. | //+------------------------------------------------------------------+ datetime GmtToServer(const datetime gmtTime) { return gmtTime + (datetime)(GmtOffsetHours() * 3600); } //+------------------------------------------------------------------+ //| The current hour in GMT, derived from the broker clock | //| | //| Deliberately NOT TimeGMT(): in the tester that is unrelated to | //| the modelled bar time, so a strategy using it would see one hour | //| while trading in another. | //| | //| Returns: 0..23. | //+------------------------------------------------------------------+ int CurrentGmtHour(void) { MqlDateTime gmt; TimeToStruct(ServerToGmt(TimeTradeServer()), gmt); return gmt.hour; } //+------------------------------------------------------------------+ //| Session boundaries in GMT | //| Returns: true and fills the hours, or false for an unknown value. | //+------------------------------------------------------------------+ bool SessionHours(const ENUM_SF_SESSION session, int &startHour, int &endHour) { switch(session) { case SF_SESSION_SYDNEY: startHour = 22; endHour = 7; return true; case SF_SESSION_TOKYO: startHour = 0; endHour = 9; return true; case SF_SESSION_LONDON: startHour = 8; endHour = 17; return true; case SF_SESSION_NEWYORK: startHour = 13; endHour = 22; return true; } startHour = 0; endHour = 0; return false; } //+------------------------------------------------------------------+ //| Is a session open now? | //| | //| The FX week closes at the New York close and opens at the Sydney | //| open: 21:00 or 22:00 GMT on Friday and Sunday, depending on US | //| DST. Taken here as the wider window, Friday 21:00 to Sunday 22:00 | //| GMT, so it errs toward "closed". Excluding weekends by GMT | //| calendar day instead would call Sydney closed on Sunday evening | //| after the week has opened, and open on Friday night after it has | //| closed. | //| Sydney wraps past midnight and is handled. | //| | //| Returns: true if the current GMT hour falls inside the session | //| and the FX week is open. | //+------------------------------------------------------------------+ bool IsSessionOpen(const ENUM_SF_SESSION session) { int startHour, endHour; if(!SessionHours(session, startHour, endHour)) return false; MqlDateTime gmt; TimeToStruct(ServerToGmt(TimeTradeServer()), gmt); if(gmt.day_of_week == SATURDAY) return false; if(gmt.day_of_week == SUNDAY && gmt.hour < 22) return false; if(gmt.day_of_week == FRIDAY && gmt.hour >= 21) return false; if(startHour <= endHour) return gmt.hour >= startHour && gmt.hour < endHour; //--- wraps past midnight, e.g. Sydney 22:00 to 07:00 return gmt.hour >= startHour || gmt.hour < endHour; } //+------------------------------------------------------------------+ //| Short name for a session | //| Returns: "Sydney", "Tokyo", "London", "New York" or "Unknown". | //+------------------------------------------------------------------+ string SessionToString(const ENUM_SF_SESSION session) { switch(session) { case SF_SESSION_SYDNEY: return "Sydney"; case SF_SESSION_TOKYO: return "Tokyo"; case SF_SESSION_LONDON: return "London"; case SF_SESSION_NEWYORK: return "New York"; } return "Unknown"; } //+------------------------------------------------------------------+ //| Are London and New York both open? | //| | //| 13:00-17:00 GMT, the deepest liquidity of the day and where most | //| session-based strategies want to be. | //| | //| Returns: true during the overlap on a weekday. | //+------------------------------------------------------------------+ bool IsLondonNewYorkOverlap(void) { return IsSessionOpen(SF_SESSION_LONDON) && IsSessionOpen(SF_SESSION_NEWYORK); } //+------------------------------------------------------------------+ //| Which sessions are open, as text | //| Returns: e.g. "London, New York", or "none". | //+------------------------------------------------------------------+ string OpenSessions(void) { string open = ""; for(int i = 0; i <= 3; i++) { ENUM_SF_SESSION session = (ENUM_SF_SESSION)i; if(!IsSessionOpen(session)) continue; if(open != "") open += ", "; open += SessionToString(session); } return (open == "") ? "none" : open; } //+------------------------------------------------------------------+ //| Is the current GMT hour inside a window? | //| | //| IsWithinTradingHours in Time.mqh also evaluates in GMT, adding | //| its own gmtOffset on top. This one takes no offset: use it where | //| a window is specified in GMT and nothing else. | //| | //| Parameters: | //| startHour, endHour - 0..23, end exclusive, wrapping allowed | //| | //| Returns: true if inside. false for out-of-range arguments. | //+------------------------------------------------------------------+ bool IsWithinGmtHours(const int startHour, const int endHour) { if(startHour < 0 || startHour > 23 || endHour < 0 || endHour > 23) return false; int hour = CurrentGmtHour(); if(startHour <= endHour) return hour >= startHour && hour < endHour; return hour >= startHour || hour < endHour; } //+------------------------------------------------------------------+ //| Report the offset, its confidence, and the open sessions | //| Returns: a multi-line summary. Never empty. | //+------------------------------------------------------------------+ string SessionReport(void) { SGmtOffset offset = DetectGmtOffset(); string report = "=== Sessions ===\n"; report += "Server time: " + TimeToString(TimeTradeServer(), TIME_DATE | TIME_SECONDS) + "\n"; report += "GMT (derived):" + TimeToString(ServerToGmt(TimeTradeServer()), TIME_DATE | TIME_SECONDS) + "\n"; report += "GMT offset: " + (offset.hours >= 0 ? "+" : "") + IntegerToString(offset.hours) + " hours"; if(offset.reliable) report += " (measured)\n"; else report += " (UNRELIABLE: " + offset.note + ")\n"; report += "Open now: " + OpenSessions() + "\n"; report += "LDN/NY overlap: " + (IsLondonNewYorkOverlap() ? "yes" : "no") + "\n"; return report; } #endif // STOPFIRST_SESSIONS_MQH //+------------------------------------------------------------------+