//+------------------------------------------------------------------+ //| 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 //+------------------------------------------------------------------+ //| 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 CHashMapm_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 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_start && tNYC %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