StopFirst-Core/Include/StopFirst/Sessions.mqh
RUSLAN SLOBODIANIUK 6ba79702fd Make the header comments self-contained
They referred to project documents that are not part of this repository.
Comments only; PriceToPips is now documented as not rounding, which is what
it does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:48:01 +03:00

382 lines
16 KiB
MQL5

//+------------------------------------------------------------------+
//| 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
//+------------------------------------------------------------------+