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>
382 lines
16 KiB
MQL5
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
|
|
//+------------------------------------------------------------------+
|