TimeGMT/TimeGMT.mqh
amrali dec41b422f
2025-10-13 16:14:19 +03:00

426 lines
40 KiB
MQL5

//+------------------------------------------------------------------+
//| TimeGMT.mqh |
//| Copyright © 2018, Amr Ali |
//| https://www.mql5.com/en/users/amrali |
//+------------------------------------------------------------------+
#ifndef TIMEGMT_UNIQUE_HEADER_ID_H
#define TIMEGMT_UNIQUE_HEADER_ID_H
#property version "1.64"
// Updates:
// 2024.03.30 - v.1.25 : Fixed broker GMT offset. Currently, the library scans H1 bars only on the GOLD chart as it has the most accurate start times.
// 2024.04.09 - v.1.33 : Fixed potential issue in the calculation of the broker GMT offset during the Christmas holidays on GMT+0 brokers.
// 2024.04.12 - v.1.35 : Fixed issue in the calculation of the broker GMT offset on few brokers which do not provide gold trading.
// 2024.07.07 - v.1.36 : Fixed issue in proper detection of the GOLD symbol on some brokers.
// 2024.10.21 - v.1.40 : Improved detection of the GOLD symbol with a fallback to EURUSD symbol.
// 2024.10.22 - v.1.45 : Faster determination of the server's GMT offset during weekend in the strategy tester.
// 2024.10.26 - v.1.47 : Added better error-handling and debugging code. Renamed the HistoryBrokerOffset() method to HistoryServerGMTOffset().
// 2024.10.28 - v.1.49 : Converted all macros for dealing with time to functions to avoid double evaluation of parameters inside macro body. More code clean-up in other lines.
// 2024.10.30 - v.1.50 : Fixed issue of wrong estimation of GMT offset from XAUEUR quotes on some brokers.
// 2024.11.23 - v.1.55 : Added an option to switch off the default loading of Gold symbol for estimation of the server's TZ/DST. Call CTimeGMT::SetUsingGoldSymbol() with 'false' to use the current chart's symbol, instead.
// 2024.12.12 - v.1.60 : Improved performance of HistoryServerGMTOffset() function and other minor code changes.
// 2024.12.14 - v.1.61 : Improved performance of FindSymbol() function.
// 2024.12.17 - v.1.62 : Further optimization of HistoryServerGMTOffset() function.
// 2024.12.24 - v.1.63 : Fixed potential issue in HistoryServerGMTOffset() function.
// 2025.10.13 - v.1.64 : Minor improvements and bug fixes.
#include <Generic\HashMap.mqh>
//+------------------------------------------------------------------+
//| Macros |
//+------------------------------------------------------------------+
#define Debug(message) PrintFormat(" <%s> %s(){ line %d: %s }", __FILE__, __FUNCTION__, __LINE__, message)
//+------------------------------------------------------------------+
//| Class CTimeGMT. |
//| Purpose: Class to fix the built-in TimeGMT() function in order |
//| to provide the true GMT time during testing in the |
//| strategy tester. |
//| |
//| Usage: Before testing an expert advisor that uses any time |
//| restrictions or News filter in the strategy tester, |
//| just add these three lines at the top, then recompile. |
//| |
//| // Uncomment the following line to see debug messages |
//| // #define PRINT_GMT_DETAILS |
//| #include "TimeGMT.mqh" |
//| |
//| Symbol for Estimation of The Server TZ/DST: |
//| By default, library uses XAUUSD as it provides reliable |
//| results (esp., brokers that follow EU DST schedule). |
//| To use the current chart's symbol instead of XAUUSD, |
//| call CTimeGMT::SetUsingGoldSymbol() with 'false'. |
//| If the XAUUSD symbol is used for estimation of the |
//| server's TZ/DST then dst switches will occur one hour |
//| later in the strategy tester. |
//+------------------------------------------------------------------+
class CTimeGMT
{
public:
static datetime TimeGMT(void);
static datetime TimeGMT(MqlDateTime &dt_struct);
static void SetUsingGoldSymbol(const bool enabled = true); // Sets the option to use Gold symbol for estimation of server TZ/DST
protected:
//--- internal methods
static int HistoryServerGMTOffset(const datetime srvtime); // estimate server TZ offset (UTC+DST) from GMT at a given time
static datetime FirstBarOfWeek(datetime &weekend, int depthLimit = 8); // Returns the time of the first H1 bar after a given weekend (skips abnormal weeks)
static string FindSymbol(string symbol);
static bool CheckLoadHistory(const string symbol, const ENUM_TIMEFRAMES period); // Checks presence of the history for symbol and timeframe
//--- static variables
static CHashMap<datetime,int>m_serveroffset; // hashmap of server tz offset
protected:
//--- methods for working with data of datetime type
static datetime StartOfWeek(const datetime time, const bool StartsOnMonday = false); // Return the start of a week for the given date
static string t2s(const datetime time, const int mode = TIME_DATE | TIME_MINUTES); // Formats time with the weekday name => "Wed, 2023.02.14 01:59"
//--- time unit constants
enum ENUM_TIME_UNITS
{
MINSECS = 60,
HOURSECS = 3600, // = 60 * 60,
DAYSECS = 86400, // = 60 * 60 * 24,
WEEKSECS = 604800, // = 60 * 60 * 24 * 7,
YEARSECS = 31536000 // = 60 * 60 * 24 * 365
};
private:
static string m_symbol;
static bool m_using_xauusd;
};
//--- Initialize static variables
CHashMap<datetime,int> CTimeGMT::m_serveroffset;
string CTimeGMT::m_symbol=NULL;
bool CTimeGMT::m_using_xauusd=true; // search for and load Gold symbol (default)
//+------------------------------------------------------------------+
//| Returns the GMT, which is calculated by subtracting the server |
//| offset (UTC+DST) from the current time of the trading server in |
//| the client terminal. |
//+------------------------------------------------------------------+
datetime CTimeGMT::TimeGMT(void)
{
//--- In the strategy tester, TimeGMT() is always equal to TimeTradeServer() simulated server time.
//--- However, the "true" GMT can be estimated by analysis of H1 quotes history to get the server
//--- timezone offset. The server GMT offset is subtracted from server time to get the true GMT.
//--- (e.g., TimeTradeServer() = 23:00, and offset from H1 bars = GMT+3, then true GMT = 20:00).
datetime tts = ::TimeTradeServer();
return(tts - HistoryServerGMTOffset(tts)); // determine the server GMT offset from H1 quotes history
}
//+------------------------------------------------------------------+
//| TimeGMT with MqlDateTime type parameter. |
//+------------------------------------------------------------------+
static datetime CTimeGMT::TimeGMT(MqlDateTime &dt_struct)
{
datetime t = TimeGMT();
TimeToStruct(t, dt_struct);
return(t);
}
//+------------------------------------------------------------------+
//| Sets the option of using XAUUSD (Gold) symbol to estimate the |
//| server's TZ/DST by analysis of H1 quotes history. |
//| TRUE : search for and load Gold symbol (default behavior). |
//| FALSE : use the symbol of the current chart. |
//| |
//| XAUUSD can provide more reliable results (esp., for brokers |
//| that follow EU DST schedule) on weeks during US DST and EU |
//| DST schedules are out of sync (March and late October). |
//| if your broker follows the US DST schedule, or no schedule |
//| at all, then using the chart symbol is Ok. |
//| |
//| Note: as a side effect that XAUUSD starts an hour after Forex, |
//| dst switches occur one hour later (only in the strategy tester). |
//+------------------------------------------------------------------+
void CTimeGMT::SetUsingGoldSymbol(const bool enabled = true)
{
m_using_xauusd = enabled;
//--- removes all keys and values from the map.
m_serveroffset.Clear();
//--- force a new search for the symbol
m_symbol = enabled ? FindSymbol("XAUUSD") : _Symbol;
//--- Check the H1 history is loaded
CheckLoadHistory(m_symbol, PERIOD_H1);
//---
Print("CTimeGMT >> Using Gold symbol option: ", m_using_xauusd);
Print("CTimeGMT >> Loaded symbol: ", m_symbol, ",H1");
}
//+------------------------------------------------------------------+
//| Estimates server TZ offset (UTC+DST) from GMT at a given time. |
//+------------------------------------------------------------------+
int CTimeGMT::HistoryServerGMTOffset(const datetime srvtime)
{
static datetime last_sunday = -1;
static int last_result = INT_MIN;
#ifdef PRINT_GMT_DETAILS
Debug(StringFormat("** Estimating server GMT offset of '%s' server for %s from %s,H1 quotes", AccountInfoString(ACCOUNT_SERVER), t2s(srvtime), m_symbol));
#endif
if(StartOfWeek(srvtime) == last_sunday && m_serveroffset.Count() > 0)
{
#ifdef PRINT_GMT_DETAILS
Debug(StringFormat("last_sunday: %s -> %d (GMT%+g)", t2s(last_sunday), last_result, last_result/(double)HOURSECS));
#endif
return(last_result);
}
//--- start at the last bar if 'srvtime' falls in the last weekend or holidays (no FstBarWk yet)
const datetime lastbar = (datetime)SeriesInfoInteger(m_symbol, PERIOD_H1, SERIES_LASTBAR_DATE);
datetime curtime = srvtime;
if(curtime == 0 || curtime > lastbar)
{
curtime = lastbar;
}
const datetime sunday = StartOfWeek(curtime); // the weekend "Sunday 00:00" before this time
//--- search this week in the broker gmt offset cache
int serveroffset = INT_MIN;
if(!m_serveroffset.TryGetValue(sunday, serveroffset))
{
const datetime firstbar = (datetime) SeriesInfoInteger(m_symbol, PERIOD_H1, SERIES_FIRSTDATE);
//--- weekend could be modified, if recursion had skipped to previous Sundays
datetime weekend = sunday;
if(weekend >= firstbar)
{
//--- find the time of the first hourly bar after this weekend "Sunday 00:00"
const datetime FstBarWk = FirstBarOfWeek(weekend);
if(FstBarWk == WRONG_VALUE)
{
Debug(StringFormat("Error in FirstBarOfWeek(%s) = WRONG_VALUE, srvtime = %s", t2s(weekend), t2s(srvtime)));
last_result = serveroffset = INT_MIN;
last_sunday = -1;
}
else
{
//--- Forex pairs start Sun 17:00 NY. Gold starts an hour later.
int beginNYC = 17 * HOURSECS;
if(StringFind(m_symbol, "XAU") == 0 || StringFind(m_symbol, "GOLD") == 0)
{
beginNYC = 18 * HOURSECS;
}
//--- US summer starts on the second Sunday of March (GMT-4),
//--- and ends on the first Sunday of November (GMT-5).
MqlDateTime dt;
TimeToStruct(weekend, dt);
MqlDateTime dt1 = {dt.year, 03, 08}; // the second Sunday of March for the US switch
MqlDateTime dt2 = {dt.year, 11, 01}; // the first Sunday of November for the US switch
datetime dst_start = StructToTime(dt1);
datetime dst_end = StructToTime(dt2);
datetime tNYC = weekend + beginNYC; // Sun, 18:00 NYC should co-incide with broker's 'FstBarWk' on GOLD chart.
datetime tGMT = tNYC - ((weekend>=dst_start && weekend<dst_end) ? -4 : -5) * HOURSECS;
serveroffset = (int)(FstBarWk - tGMT);
serveroffset = (int) MathRound(serveroffset/1800.)*1800; // round offset to multiples of 30-minutes
#ifdef PRINT_GMT_DETAILS
Debug(StringFormat("FstBarWk = %s", t2s(FstBarWk)));
Debug(StringFormat("tNYC = %s (GMT%+d)", t2s(tNYC), (tNYC>=dst_start && tNYC<dst_end) ? -4 : -5));
Debug(StringFormat("tGMT = %s", t2s(tGMT)));
Debug(StringFormat("m_serveroffset.Add(%s, %d (GMT%+g))", t2s(sunday), serveroffset, serveroffset/(double)HOURSECS));
#endif
//--- add this week to the server gmt offset cache
m_serveroffset.Add(sunday, serveroffset);
}
}
}
#ifdef PRINT_GMT_DETAILS
else
{
Debug(StringFormat("m_serveroffset.TryGetValue(%s) -> %d (GMT%+g)", t2s(sunday), serveroffset, serveroffset/(double)HOURSECS));
}
#endif
//--- remember the server gmt offset of this week
if(serveroffset != INT_MIN)
{
last_sunday = sunday;
last_result = serveroffset;
}
return(serveroffset);
}
//+------------------------------------------------------------------+
//| Returns the time of the first H1 bar after the given weekend. |
//| The function skips abnormal trading weeks around holidays. |
//+------------------------------------------------------------------+
datetime CTimeGMT::FirstBarOfWeek(datetime &weekend, int depthLimit = 8)
{
if(m_symbol==NULL)
{
//--- Gold has accurate start of trading weeks (esp. EU DST brokers)
m_symbol = m_using_xauusd ? FindSymbol("XAUUSD") : _Symbol;
//--- Check the H1 history is loaded
CheckLoadHistory(m_symbol, PERIOD_H1);
#ifdef PRINT_GMT_DETAILS
Print("CTimeGMT >> Using Gold symbol option: ", m_using_xauusd);
Print("CTimeGMT >> Loaded symbol: ", m_symbol, ",H1");
#endif
}
//--- find the first hourly bar after the specified weekend "Sunday 00:00"
const int barindex = iBarShift(m_symbol, PERIOD_H1, weekend, false);
const datetime bartime = iTime(m_symbol, PERIOD_H1, barindex - 1);
#ifdef PRINT_GMT_DETAILS
Debug(StringFormat("iBarShift(%s, %s)=%d, iTime(%d) = %s", m_symbol, t2s(weekend), barindex, barindex-1, t2s(bartime)));
#endif
if(barindex < 0 || depthLimit < 0)
{
if(MQLInfoInteger(MQL_PROGRAM_TYPE)!=PROGRAM_INDICATOR)
{
PrintFormat(">> iBarShift() FAILED: The requested date %s for %s,H1 is not found in the available history", t2s(weekend), m_symbol);
}
return(WRONG_VALUE);
}
//--- Skip long weekends or holidays (bartime > Monday 12:00 PM)
if(barindex == 0 || bartime > weekend + 36 * HOURSECS)
{
#ifdef PRINT_GMT_DETAILS
Debug(StringFormat("bartime = %s", t2s(bartime)));
Debug(StringFormat("weekend = %s", t2s(weekend)));
Debug("Detected long weekend / Christmas holidays, skip to previous weekend:");
#endif
weekend -= WEEKSECS; // change 'weekend' to the previous Sunday 00:00
return(FirstBarOfWeek(weekend, depthLimit - 1));
}
//---
return(bartime);
}
//+------------------------------------------------------------------+
//| Find and select a symbol for further working with information. |
//+------------------------------------------------------------------+
string CTimeGMT::FindSymbol(string symbol)
{
bool symbol_found = false; // Store if we have found a valid symbol or not yet
//--- if the symbol is found in the list of standard symbols on the server
bool custom = false;
if(SymbolExist(symbol, custom) && !custom)
{
symbol_found = true;
}
//--- First try to find if the passed symbol exists
if(symbol_found == false)
for(int i=0; i < SymbolsTotal(0); i++)
if(StringFind(SymbolName(i,0),symbol)==0)
{
symbol = SymbolName(i,0); // symbol may have suffix
symbol_found = true;
break;
}
//--- Then try to find a symbol equal to "GOLD"
if(symbol_found == false)
for(int i=0; i < SymbolsTotal(0); i++)
if(SymbolName(i,0)=="GOLD")
{
symbol = SymbolName(i,0); // symbol may have suffix
symbol_found = true;
break;
}
//--- Then try to find a symbol that starts with GOLD
if(symbol_found == false)
for(int i=0; i < SymbolsTotal(0); i++)
if(StringFind(SymbolName(i,0),"GOLD")==0)
{
symbol = SymbolName(i,0); // symbol may have suffix
symbol_found = true;
break;
}
//--- And finally use the current chart's symbol for brokers that do not provide gold
if(symbol_found == false)
{
symbol = Symbol();
}
//--- we have found a valid symbol
SymbolSelect(symbol, true);
return(symbol);
}
//+------------------------------------------------------------------+
//| Checks presence of the history for symbol and timeframe. |
//| http://www.mql5.com/en/docs/series/timeseries_access |
//+------------------------------------------------------------------+
bool CTimeGMT::CheckLoadHistory(const string symbol, const ENUM_TIMEFRAMES period)
{
//--- indicator shouldn't load its own symbol and timeframe
if(MQLInfoInteger(MQL_PROGRAM_TYPE)==PROGRAM_INDICATOR && Period()==period && Symbol()==symbol)
return(false);
//---
datetime times[];
datetime first_date=0;
int copied=0;
uint tick=GetTickCount();
//--- wait for timeseries build
while(!SeriesInfoInteger(symbol,period,SERIES_SYNCHRONIZED) && !IsStopped() && (GetTickCount()-tick<=6000))
//Sleep(5);
;
//--- second attempt
return(SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date)
&& first_date>0
&& CopyTime(symbol,period,first_date+PeriodSeconds(period),1,times));
}
//+------------------------------------------------------------------+
//| Return the start of a week for the given date. |
//| By default, the week starts on Sunday, unless superseded. |
//| |
//| StartOfWeek (D'2017.08.03 11:30') => 2017.07.30 00:00:00 |
//+------------------------------------------------------------------+
datetime CTimeGMT::StartOfWeek(const datetime t, const bool StartsOnMonday = false)
{
return (t - (t + (4 - StartsOnMonday) * DAYSECS) % WEEKSECS);
}
//+------------------------------------------------------------------+
//| Formats time with the weekday name => "Wed, 2023.02.14 01:59" |
//+------------------------------------------------------------------+
string CTimeGMT::t2s(const datetime t, const int mode = TIME_DATE | TIME_MINUTES)
{
const string days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
const int i = ((int)((uint)t / DAYSECS + 4) % 7); // i = DayOfWeek(t)
return days[i % 7] + ", " + TimeToString(t, mode);
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//+------------------------------------------------------------------+
//| Global API hook via macro substitution. |
//+------------------------------------------------------------------+
datetime CustomTimeGMT()
{
//--- apply fix only in strategy tester, but use built-in TimeGMT() if running in normal mode.
if((bool)MQLInfoInteger(MQL_TESTER))
{
return CTimeGMT::TimeGMT();
}
else
return ::TimeGMT();
}
//+------------------------------------------------------------------+
//| TimeGMT with MqlDateTime type parameter. |
//+------------------------------------------------------------------+
datetime CustomTimeGMT(MqlDateTime &dt_struct)
{
//--- apply fix only in strategy tester, but use built-in TimeGMT() if running in normal mode.
if((bool)MQLInfoInteger(MQL_TESTER))
{
return CTimeGMT::TimeGMT(dt_struct);
}
else
return ::TimeGMT(dt_struct);
}
//+------------------------------------------------------------------+
#define TimeGMT CustomTimeGMT
#undef Debug
#endif // #ifndef TIMEGMT_UNIQUE_HEADER_ID_H