Переход на летнее время (локальное)

Для определения факта перевода локальных часов на летнее время в MQL5 имеется функция TimeDaylightSavings. Она берет настройки из вашей операционной системы.

Определить режим летнего времени на сервере так же легко не получится. Для этого потребуется реализовать на MQL5 анализ котировок, событий экономического календаря или времени ролловера/начисления свопов в торговой истории счета. В примере далее мы покажем один из вариантов.

int TimeDaylightSavings()

Функция возвращает поправку в секундах, если осуществлен переход на летнее время. Зимнее время является стандартным для каждого часового пояса, так что поправка в этот период равна нулю. В условном виде формулу получения поправки можно записать следующим образом:

TimeDaylightSavings() = TimeLocalwinter() - TimeLocalsummer()

Например, если стандартный часовой пояс (winter) равен UTC+3 (то есть время пояса на 3 часа больше, чем UTC), то при переходе на летнее время (summer) добавляется 1 час и получается UTC+4. При этом TimeDaylightSavings вернет -3600.

Пример применения функции приведен в скрипте TimeSummer.mq5, где также предлагается один из возможных эмпирических способов выявления соответствующего режима на сервере.

void OnStart()
{
   PRTF(TimeLocal());          // локальное время терминала
   PRTF(TimeCurrent());        // последнее известное время сервера
   PRTF(TimeTradeServer());    // расчетное время сервера
   PRTF(TimeGMT());            // время GMT (расчет от локального через сдвиг таймзоны)
   PRTF(TimeGMTOffset());      // сдвиг таймзоны от GMT в секундах
   PRTF(TimeDaylightSavings());// поправка на летнее время в секундах
   ...

Для начала выведем все типы времени и его коррекции, предоставляемые MQL5 (функции TimeGMT и TimeGMTOffset будут представлены в следующем разделе про Универсальное время, но их суть должна уже быть в целом ясна из предыдущего описания).

Предполагается, что скрипт запускается в торговые дни. Записи в журнале у вас будут соответствовать настройкам вашего компьютера и сервера брокера.

TimeLocal()=2021.09.09 22:06:17 / ok
TimeCurrent()=2021.09.09 22:06:10 / ok
TimeTradeServer()=2021.09.09 22:06:17 / ok
TimeGMT()=2021.09.09 19:06:17 / ok
TimeGMTOffset()=-10800 / ok
TimeDaylightSavings()=0 / ok

В данном случае часовой пояс клиента отстоит от GMT на 3 часа (UTC+3), поправки на летнее время нет.

Теперь займемся сервером. По значению функции TimeCurrent мы можем определить текущее время сервера, но не его стандартный часовой пояс, поскольку в этом времени может участвовать переход на летнее время (а используется ли оно в принципе, и не включено ли в данный момент, — информации в MQL5 нет).

Для определения реальной "таймзоны" сервера и режима летнего времени воспользуемся тем фактом, что перевод времени сервера сказывается на котировках. Как большинство эмпирических способов решения задач, этот может давать не совсем верные результаты в тех или иных обстоятельствах. Если сверка с другими источниками покажет разночтения, следует подобрать иной способ.

Открытие рынка Форекс происходит в воскресенье вечером в 22 часа по всемирному времени (это соответствует началу утренних торгов в азиатско-тихоокеанском регионе), а закрытие — в пятницу в 22 часа (закрытие торгов в Америке). Это значит, что на серверах в зоне UTC+2 (Восточная Европа), первые бары появятся ровно в 0 часов 0 минут в понедельник. По центральноевропейскому времени, которое соответствует UTC+1, начало торговой недели происходит в 23 часа воскресного вечера.

Подсчитав статистику внутрисуточного смещения первого бара H1 после каждого перерыва на выходные дни, мы получим оценку "таймзоны" сервера. Разумеется, для этого лучше использовать наиболее ликвидный инструмент Forex, то есть EURUSD.

Если в данной статистике за годовой период обнаружится два максимума внутрисуточного смещения и они расположены рядом друг с другом, это будет означать, что брокер производит переход на летнее время и обратно.

Обратите внимание, что периоды летнего и зимнего времени не равны. Так, при переходе на летнее время в начале марта и возврате на зимнее в начале ноября получим примерно 8 месяцев летнего времени. Это будет сказываться на соотношении максимумов в статистике.

Имея две "таймзоны", легко определить, какая из них активна в данный момент и, тем самым, выяснить текущее наличие или отсутствие поправки на летнее время.

При переводе часов на летнее время у брокера с UTC+2 таймзона поменяется на UTC+3, из-за чего начало недели сместится с 22:00 на 21:00. Это скажется на структуре баров H1: чисто визуально на графике мы увидим три бара вечером воскресенья вместо двух.

Перевод часов с зимнего (UTC+2) на летнее (UTC+3) время на графике EURUSD H1

Перевод часов с зимнего (UTC+2) на летнее (UTC+3) время на графике EURUSD H1

Для воплощения задуманного оформлена отдельная функция ServerTimeZone. За получение котировок (а точнее, меток времени баров) в ней отвечает вызов встроенной функции CopyTime (мы изучим её в разделе про доступ к таймсериям).

ServerTime ServerTimeZone(const string symbol = NULL)
{
  const int year = 365 * 24 * 60 * 60;
  datetime array[];
  if(PRTF(CopyTime(symbolPERIOD_H1TimeCurrent() - yearTimeCurrent(), array)) > 0)
  {
     // здесь получим примерно 6000 баров в массиве
     const int n = ArraySize(array);
     PrintFormat("Got %d H1 bars, ~%d days"nn / 24);
     // (-V-) цикл по барам H1
     ...
  }
}

В качестве параметров в CopyTime передается рабочий инструмент, таймфрейм H1, и диапазон дат за последний год. Значение NULL вместо инструмента означает символ текущего графика, на который будет помещен скрипт, поэтому рекомендуется выбрать окно с EURUSD. Константа PERIOD_H1 соответствует H1, как нетрудно догадаться. Функция TimeCurrent нам уже знакома: она вернет текущее, самое последнее известное время сервера. А если вычесть из него количество секунд в году, которое положено в переменную year, мы получим дату и время ровно год назад. Результаты поступят в массив array.

Для подсчета статистики, сколько раз неделя открывалась баром на конкретном часе, зарезервируем массив hours[24]. Сам расчет будем выполнять в цикле по полученному массиву array, то есть по барам из прошлого в настоящее. На каждой итерации открывающий час просматриваемой недели будем хранить в переменной current. Когда цикл закончится, в current останется актуальный часовой пояс сервера, поскольку последней обработается текущая неделя.

     // (-v-) цикл по барам H1
     int hours[24] = {};
     int current = 0;
     for(int i = 0i < n; ++i)
     {
        // (-V-) обработка i-го бара H1
        ...
     }
     
     Print("Week opening hours stats:");
     ArrayPrint(hours);

Внутри цикла по дням воспользуемся классом DateTime из заголовочного файла MQL5Book/DateTime.mqh (см. раздел Дата и время).

        // (-v-) обработка i-го бара H1
        // находим день недели бара
        const ENUM_DAY_OF_WEEK weekday = TimeDayOfWeek(array[i]);
        // пропускаем все дни кроме воскресенья и понедельника
        if(weekday > MONDAYcontinue;
        // анализируем первый бар H1 очередной торговой недели
        // находим час первого бара после выходных
        current = _TimeHour();
        // подсчитываем статистику часов открытия
        hours[current]++;
        
        // пропускаем 2 следующих дня
        // (т.к. статистика начала этой недели уже обновлена)
        i += 48;

Предложенный алгоритм не является оптимальным, но зато не требует вникания в технические детали организации таймсерий, которые нам еще не известны.

Некоторые недели бывают неформатными (начинаются после праздников). Если такая ситуация случится на последней неделе, переменная current будет содержать необычное смещение. Это можно проверить по статистике: для полученного часа окажется очень малое количество зафиксированных "открытий" недели. В тестовом скрипте в таком случае просто выводится сообщение в журнал. На практике следует уточнить стандартное открытие по предыдущим одной-двум неделям.

     // (-V-) цикл по барам H1
     ...
     if(hours[current] <= 52 / 4)
     {
        // TODO: уточнить по предыдущим неделям
        Print("Extraordinary week detected");
     }

Если брокер не переходит на летнее время, в статистике получится один максимум, в который попадут все или почти все недели. Если брокер практикует смену поясов, в статистике будет два максимума.

     // находим самый частый временной сдвиг
     int max = ArrayMaximum(hours);
     // затем проверяем, нет ли еще одного регулярного сдвига
     hours[max] = 0;
     int sub = ArrayMaximum(hours);

Нам нужно определить, насколько значителен второй экстремум (т.е. отличен от случайных праздников, которые могли сместить начало недели). Для этого оцениваем статистику относительно четверти года (52 недели / 4). Если этот предел превышен, брокером поддерживается режим летнего времени.

     int DST = 0;
     if(hours[sub] > 52 / 4)
     {
        // DST поддерживается в принципе
        if(current == max || current == sub)
        {
           if(current == MathMin(maxsub))
              DST = fabs(max - sub); // DST включен сейчас
        }
     }

Если смещение открытия текущей недели (в переменной current) совпадает с одним из двух основных экстремумов, значит, текущая неделя открылась штатно, и по ней можно делать вывод о "таймзоне" (это защитное условие необходимо, потому что выше у нас нет корректировки для нестандартной недели, а вместо этого только выдается предупреждение).

Теперь все готово, чтобы сформировать ответ нашей функции: часовой пояс сервера и признак включенного режима летнего времени.

     current += 2 + DST// +2 для получения смещения от UTC
     current %= 24;
     // таймзоны всегда в диапазоне [UTC-12,UTC+12]
     if(current > 12current = current - 24;

Поскольку мы имеем две характеристики для возврата из функции (current и DST), и помимо этого можем сообщить вызываемому коду, использует ли брокер летнее время в принципе (даже если сейчас зима), имеет смысл объявить специальную структуру ServerTime со всеми необходимыми полями.

struct ServerTime
{
   int offsetGMT;      // таймзона в секундах относительно UTC/GMT
   int offsetDST;      // DST коррекция в секундах (включена в offsetGMT)
   bool supportDST;    // DST коррекция обнаружена в принципе в котировках
   string description// описание результата
};

Тогда в функции ServerTimeZone мы можем заполнить и вернуть такую структуру в качестве результата работы.

     ServerTime st = {};
     st.description = StringFormat("Server time offset: UTC%+d, including DST%+d"currentDST);
     st.offsetGMT = -current * 3600;
     st.offsetDST = -DST * 3600;
     return st;

Если по какой-либо причине функция не сможет получить котировки, вернем пустую структуру.

ServerTime ServerTimeZone(const string symbol = NULL)
{
  const int year = 365 * 24 * 60 * 60;
  datetime array[];
  if(PRTF(CopyTime(symbolPERIOD_H1TimeCurrent() - yearTimeCurrent(), array)) > 0)
  {
     ...
     return st;
  }
  ServerTime empty = {-INT_MAX, -INT_MAXfalse};
  return empty;
}

Проверим новую функцию в действии, для чего в OnStart добавим следующие инструкции:

   ...
   ServerTime st = ServerTimeZone();
   Print(st.description);
   Print("ServerGMTOffset: "st.offsetGMT);
   Print("ServerTimeDaylightSavings: "st.offsetDST);
}

Посмотрим на возможные результаты.

CopyTime(symbol,PERIOD_H1,TimeCurrent()-year,TimeCurrent(),array)=6207 / ok
Got 6207 H1 bars, ~258 days
Week opening hours stats:
52  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
Server time offset: UTC+2, including DST+0
ServerGMTOffset: -7200
ServerTimeDaylightSavings: 0

Согласно собранной статистике баров H1, неделя у данного брокера открывается строго в 00:00 понедельника. Таким образом, реальная "таймзона" равна UTC+2, а поправка на летнее время отсутствует, то есть время сервера должно совпадать с EET (UTC+2). Однако на практике, как мы видели в первой части лога, время на сервере отличается от GMT на 3 часа.

Здесь можно предположить, что нам встретился сервер, круглогодично работающий в летнем времени. В таком случае функция ServerTimeZone не сумеет отличить корректировку от дополнительного часа в "таймзоне": в результате, режим DST будет равен нулю, а время GMT, рассчитанное по котировкам сервера, сместится вправо на час от реального. Или наше исходное предположение о том, что котировки начинают поступать в 22:00 воскресенья, не соответствуют режиму работы данного сервера. Такие моменты следует выяснять у службы поддержки брокера.