StopFirst 1.00, the read-only core
Fourteen headers, a header-only MQL5 library: lot size from risk, pips on
forex, gold and indices, broker checks, bars and candle patterns, cached ATR,
sessions in GMT, account risk, symbol suffixes, a calendar filter. Sends no
trade requests. Plus the LotByRisk example script.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:40:34 +03:00 | | | //+------------------------------------------------------------------+
|
| | | //| Sessions.mqh|
|
| | | //| Copyright 2026, bandersoft |
|
| | | //| https://www.mql5.com/en/users/fats |
|
| | | //+------------------------------------------------------------------+
|
| | | //| |
|
2026-10-16 21:48:01 +03:00 | | | //| Broker GMT offset and FX session detection. |
|
StopFirst 1.00, the read-only core
Fourteen headers, a header-only MQL5 library: lot size from risk, pips on
forex, gold and indices, broker checks, bars and candle patterns, cached ATR,
sessions in GMT, account risk, symbol suffixes, a calendar filter. Sends no
trade requests. Plus the LotByRisk example script.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:40:34 +03:00 | | | //| |
|
2026-10-16 21:48:01 +03:00 | | | //| Read-only: sends no trade requests. |
|
StopFirst 1.00, the read-only core
Fourteen headers, a header-only MQL5 library: lot size from risk, pips on
forex, gold and indices, broker checks, bars and candle patterns, cached ATR,
sessions in GMT, account risk, symbol suffixes, a calendar filter. Sends no
trade requests. Plus the LotByRisk example script.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:40:34 +03:00 | | | //| |
|
| | | //| 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
|
2026-10-16 21:48:01 +03:00 | | | //--- change would be an hour off until restarted. A value set by hand
|
| | | //--- is never re-measured.
|
StopFirst 1.00, the read-only core
Fourteen headers, a header-only MQL5 library: lot size from risk, pips on
forex, gold and indices, broker checks, bars and candle patterns, cached ATR,
sessions in GMT, account risk, symbol suffixes, a calendar filter. Sends no
trade requests. Plus the LotByRisk example script.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:40:34 +03:00 | | | 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 |
|
2026-10-16 21:48:01 +03: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. |
|
StopFirst 1.00, the read-only core
Fourteen headers, a header-only MQL5 library: lot size from risk, pips on
forex, gold and indices, broker checks, bars and candle patterns, cached ATR,
sessions in GMT, account risk, symbol suffixes, a calendar filter. Sends no
trade requests. Plus the LotByRisk example script.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:40:34 +03:00 | | | //| 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? |
|
| | | //| |
|
2026-10-16 21:48:01 +03:00 | | | //| 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. |
|
StopFirst 1.00, the read-only core
Fourteen headers, a header-only MQL5 library: lot size from risk, pips on
forex, gold and indices, broker checks, bars and candle patterns, cached ATR,
sessions in GMT, account risk, symbol suffixes, a calendar filter. Sends no
trade requests. Plus the LotByRisk example script.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:40:34 +03:00 | | | //| |
|
| | | //| 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
|
| | | //+------------------------------------------------------------------+
|