English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
Основы программирования на MQL5 - Время

Основы программирования на MQL5 - Время

MetaTrader 5Примеры | 5 февраля 2013, 14:57
31 847 32
Dmitry Fedoseev
Dmitry Fedoseev

Содержание


Введение

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

  • Выполнение каких-то действий в заданный момент времени (рис. 1). Это может быть выполнение действий ежедневно в одно и то же время, еженедельно в заданный день недели и в определенное время дня, просто в указанную дату и время.

    Рис. 1. Момент времени.
    Рис. 1. Момент времени.

  • Разрешение или запрет выполнения каких-либо действий в заданном диапазоне времени (временной сессии). Это может быть временная сессия в течение суток (каждые сутки от одного момента времени до другого), запрет/разрешение на какие-то действия в определенные дни недели, временные сессии от определенного времени одного дня недели до другого времени другого дня недели, и просто в промежутке указанных дат и времени.

    Рис. 2. Промежуток времени.
    Рис. 2. Промежуток времени.

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

  • Пропуски баров на графике из-за отсутствия изменения цен. Особенно заметны пропуски на малых таймфреймах: M1, M5, даже на M15. Не исключается наличие пропусков и на старших таймфреймах.

  • В котировках некоторых Дилинговых Центров (ДЦ) имеются воскресные бары, которые практически должны относиться к понедельнику.

  • Наличие выходных дней. Вчерашним днем для понедельника является пятница, но не воскресенье. Завтрашним днем для пятницы является понедельник, но не суббота.

  • Кроме наличия воскресных баров, встречались ДЦ, выполняющие котирование непрерывно в течение всех выходных дней. При этом активность цен весьма незначительна по сравнению с рабочими днями, но она имеется в течение всех выходных дней.

  • Различие в часовых поясах торгового сервера и локального компьютера (компьютера трейдера с торговым терминалом). При этом у различных ДЦ серверное время может быть разным.

  • Переход на летнее/зимнее время.

Сначала в статье будут немного общей теории, касающейся времени, затем будет рассмотрено использование стандартных функции MQL5 для работы со временем, параллельно будут рассматриваться некоторые приемы программирования, в конце - решение практических задач.

Статья получилось очень объемная: новичкам, недавно приступившим к изучению программирования языка MQL5, вряд ли получится осилить ее за один раз. Будет лучше выделить на ее прочтение как минимум три дня.


Особенности календарного летосчисления

Немного отвлечемся на астрономию. Известно, что Земля вращается вокруг Солнца, при этом она еще вращается вокруг своей оси. Ось вращения вокруг себя немного наклонена относительно оси вращения вокруг Солнца. Время необходимое на один оборот Земли вокруг своей оси в астрономических (небесных, глобальных) координатах называется астрономическими или сидерическими сутками.

Для простого обитателя Земли (в отличие от астрономов) астрономические сутки не представляют интереса, для них большее значение имеет смена дня и ночи. Время, необходимое на один цикл смены дня и ночи, называется солнечными сутками. Если посмотреть на солнечную систему со сторону северного полюса Земли (рис. 3), Земля вращается и вокруг своей оси, и вокруг Солнца против часовой стрелки. Из-за этого, чтобы Земля совершила один оборот вокруг своей оси относительно Солнца, она должна повернуться не на 360 градусов, а чуть больше. Соответственно, солнечные сутки получаются немного длиннее астрономических.

Рис. 3. Направления вращения Земли вокруг своей оси и вокруг Солнца (вид со стороны северного полюса Земли).
Рис. 3. Направления вращения Земли вокруг своей оси и вокруг Солнца (вид со стороны северного полюса Земли).

Для удобства и точности отсчета времени за основу времяисчисления взяты солнечные сутки. Сутки делятся на 24 части - получается один час, который делится на 60 минут и т.д. Длительность астрономических суток составляет 23 часа 56 минут 4 секунды. Из-за того что ось вращения Земли немного наклонена относительно оси перпендикуляра к её орбитальной плоскости, происходит смена времен года, что очень ощутимо для обитателей Земли.

В один год входит не целое количество солнечных суток, а немного больше - 365 суток и 6 часов. Поэтому, периодически, раз в четыре года (в год, кратный числу 4) выполняется коррекция календаря, добавляется еще один день - 29-е февраля (високосный год). Однако и такая коррекция не является точной (добавляется немного лишнего времени), поэтому некоторые годы кратные 4, не являются високосными. В годы, оканчивающиеся на "00" (кратные 100) не выполняется коррекция календаря. Но и это еще не все.

Если год кратен 100 и к тому же кратен 400, то год снова является високосным, и выполняется коррекция календаря. 1900-ый год кратен 4, кратен 100, но не кратен 400, поэтому он не является високосным. 2000-ый год кратен 4, кратен 100 и кратен 400, поэтому он является високосным. Следующий год, кратный 4-ем и 100 - это 2100-ый год, он не кратен 400, поэтому не будет високосным. Получается, что читатели этой статьи живут во времена, когда каждый год, кратный 4, является високосным. Следующий год кратный 100 и являющийся високосным - 2400-ый год.


Часовые пояса

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

За начало отсчета принят меридиан с долготой 0, называемый Гринвичем. Этот меридиан проходит через город Лондон, район Гринвич, поэтому получил такое название. Часовой пояс Гринвича охватывает меридиан с двух сторон на 7.5 градусов. К востоку от часового пояса Гринвича (правее по карте) расположено 12 положительных часовых поясов (от +1 до +12), к западу (левее по карте) - 12 отрицательных (от -1 до -12). Получается, что часовые пояса -12 и +12 по ширине уже остальных часовых поясов, они занимают по 7.5 градусов, а не по 15. Расположены часовые пояса -12 и +12 по обе стороны от меридиана с долготой 180 градусов, называемого линией смены даты.

Допустим на Гринвиче полдень (12:00), на часовом поясе -12 будет время 00:00 - начало суток, а на поясе +12 - 24:00, т.е. 00:00 следующих суток. Так же с любым другим временем, часы будут показывать одинаковое время, а на календарях будут разные даты. На практике часовой пояс -12 не используется, вместо него используется +12. Также не используется часовой пояс -11, вместо него используется часовой пояс +13. Вероятно, это связано с особенностями экономических отношений: например, государство Самоа, находящееся в часовом поясе +13, имеет активные экономические отношения с Японией, поэтому там удобней пользоваться более близким к Японии временем.

Кроме того в мире существуют и необычные часовые пояса: -04:30, +05:45 и т.д., интересующиеся могут посмотреть на список всех часовых поясов в настройках времени Windows.


Переход на летнее и зимнее время

С целью более рационального использования светлого времени суток и экономии электроэнергии многие страны мира на летний период сдвигают время на один час вперед. Около 80-ти стран мира выполняет переход на летнее время, остальные нет. Из стран, выполняющих переход, некоторые выполняют переход не во всех регионах (в их числе США). В наиболее крупных и экономических развитых странах и регионах выполняется переход: почти во всех странах Европы (в том числе в Германии, Англии, Швейцарии), в США (Нью-Йорк, Чикаго), в Австралии (Сидней), в Новой Зеландии (Веллингтон). Япония не выполняет переход на летнее время. Россия 27-го марта 2011 года выполнила переход на летнее время последний раз. Обратный переход на зимнее время в октябре не выполнялся, и с тех пор Россия больше не выполняет перехода на летнее/зимнее время.

Непосредственно переход на летнее и зимнее время в разных странах выполняется по-разному. В США переход на летнее время выполняется во второе воскресенье марта в 02:00 местного времени, обратный переход на зимнее время выполняется в первое воскресенье ноября в 02:00. В Европе переход на летнее время выполняется в последнее воскресенье марта в 2:00, переход на зимнее время в последнее воскресенье октября в 3:00. При этом переход выполняется не по местному времени, а одновременно во всех странах: Лондон в 02:00, Берлин в 03:00 и т.д. в соответствии с часовым поясом. Обратный переход выполняется, когда в Лондоне 03:00, в Берлине 04:00 и т.д.

Австралия и Новая Зеландия расположены в южном полушарии, там лето начинается, когда в северном полушарии начинается зима. Соответственно, в Австралии переход на летнее время выполняется в первое воскресенье октября, возврат на зимнее время в первое воскресенье апреля. Более точно про Австралию сложно сказать, страна является очень децентрализованной, в разных ее регионах действуют разные правила перехода на летнее/зимнее время. В Новой Зеландии переход на летнее время выполняется в последнее воскресенье сентября в 02:00, а назад в 03:00 в первое воскресенье апреля.


Эталон времени

Ранее упоминалось, что за основу времяисчисления принимаются солнечные сутки, соответственно, эталонным временем является время по Гринвичу, на основе которого вычисляется время во всех остальных часовых поясах. Для времени по Гринвичу часто используется аббревиатура GMT (Greenwich Mean Time, среднее время по Гринвичу).

Однако, как выяснилось, Земля вращается немного неравномерно, поэтому с некоторых пор для отсчета времени стали пользоваться атомными часами - теперь эталонным временем является время UTC (Coordinated Universal Time, всемирное координированное время). На данный момент UTC является эталоном времени для всего мира, по которому выполняется расчет времени для всех часовых поясов с поправкой на летнее время. Время UTC не переводится ни летом, ни зимой.

Из-за того что время GMT, определяемое по Солнцу, немного не соответствует времени UTC, определяемому по атомным часам, примерно за 500 дней время UTC расходится со временем GMT на одну секунду. В связи с этим периодически выполняется поправка времени на одну секунду 30 июня или 31 декабря.


Форматы даты и времени

В различных странах используется различный формат записи даты. Например, в России принято сначала записывать число, затем месяц, в конце год. Разделяются числа точкой, например 01.12.2012 - первое декабря 2012-го года. В США сначала записывается месяц, затем число, в конце год, числа разделяются наклонной чертой "/". Кроме точки и наклонной черты "/" в некоторых стандартах для разделения полей даты может использоваться дефис "-". При записи времени, значения часов, минут и секунд отделяются знаком двоеточия ":", например, 12:15:30 - 12 часов, 15 минут, 30 секунд.

Существует простой способ указания формата записи даты и времени. Например, "dd.mm.yyyy" означает, что сначала записывается день (число месяца, которое должно занимать два знака; если число от 1 до 9 - в начало добавляется ноль), месяц (также должен обязательно состоять из двух знаков), а затем год из четырех знаков. "d-m-yy" означает, что сначала указывается день (при этом он может состоять из одной цифры), в качестве разделителя используется дефис "-", затем месяц (допускается одна цифра) и в конце год из двух цифр, например: 1/12/12 - 1-е января 2012-го года.

Время от даты отделяется пробелом, для обозначения формата используются буквы "h" (часы), "m" (минуты), "s" (секунды) и также указывается требуемое количество знаков. Например, "hh:mi:ss" означает, что сначала записываются часы (к значениям от 1 до 9 в начало добавляется ноль), затем минуты (обязательно два знака) и в конце секунды (два знака), часы минуты и секунды отделяются друг от друга знаком двоеточия.

С программистской точки зрения наиболее правильным форматом записи даты и времени является формат "yyyy.mm.dd hh:mi:ss". При такой записи после сортировки строк с датами они выстраиваются в хронологическом порядке. Допустим, вы ведете записи каждый день в текстовых файлах и складываете их в одну папку: если для имени файла использовать даты в таком формате, файлы в папке будут располагаться в правильном и удобном порядке.

На этом с теорией закончено, приступаем к практике.


Время в MQL5

В языке MQL5 время исчисляется количеством секунд, прошедших с начала так называемой эпохи UNIX, началом которой считается 1-е января 1970-го года. Для хранения времени используются переменные типа datetime. Минимальное значение переменной типа datetime - 0 (соответствует дате начала эпохи), максимальное - 32 535 244 799 (соответствует 31-му декабря 3000-года, времени 23:59:59).


Определение текущего серверного времени

Для определения текущего времени используются функция TimeCurrent(). Функция возвращает последнее известное серверное время:

datetime tm=TimeCurrent();
//--- вывод результата
Alert(tm);

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

Если необходимо узнать серверное время по определенному символу (время последнего изменения цены), для этого может использоваться функция SymbolInfoInteger() с идентификатором SYMBOL_TIME:

datetime tm=(datetime)SymbolInfoInteger(_Symbol,SYMBOL_TIME);
//--- вывод результата
Alert(tm);


Определение текущего локального времени

Локальное время (время показываемое часами компьютера пользователя) определяется функцией TimeLocal():

datetime tm=TimeLocal();
//--- вывод результата
Alert(tm);

На практике при программировании советников и индикаторов предстоит в основном пользоваться серверным временем. Локальное время удобно использовать для сообщений в алертах, журнале: пользователю удобней сверить время сообщения с показаниями часов компьютера и определить, как давно было подано сообщение. Однако терминал MetaTrader 5 сам добавляет время к сообщениям, выводимым функциями Alert() и Print(), поэтому потребность в функции TimeLocal() может возникать крайне редко.


Вывод времени

Обратите внимание, что в вышеприведенном примере кода значение переменной tm выводится через функцию Alert() непосредственно, но значение при этом отображается в удобном для прочтения формате, например, "2012.12.05 22:31:57". Это связано с тем, что функция Alert() преобразует передаваемые ей аргументы к типу string (то же самое происходит при использовании функций Print(), Comment(), выводе в текстовые и csv файлы). При формировании текстового сообщения, включающего значения переменной типа datetime необходимо самостоятельно выполнять преобразование типов. Если необходимо получить отформатированное время, выполняется приведение к типу string, если числовое - к типу long, затем к string:

datetime tm=TimeCurrent();
//--- вывод результата
Alert("Отформатированное: "+(string)tm+", в секундах: "+(string)(long)tm);

Поскольку диапазоны переменных long и ulong охватывают диапазон значений переменной типа datetime, их тоже можно использовать для хранения времени, но в этом случае для вывода отформатированного времени необходимо тип long привести к типу datetime, затем к string, а для вывода числового значения к string:

long tm=TimeCurrent();
//--- вывод результата
Alert("Отформатированное: "+(string)(datetime)tm+", в секундах: "+(string)tm);


Форматирование времени

Форматирование времени рассматривалось в статье "Основы программирования на MQL5 - Строки" в разделе "Преобразование различных переменных в строку". Кратко повторим здесь основные моменты. Помимо возможности приведения типов в языке MQL5 имеется функция TimeToString(), которая позволяет указать формат отображения даты и времени при преобразовании их в строку:

datetime tm=TimeCurrent();
string str1="Дата и время с минутами: "+TimeToString(tm);
string str2="Только дата: "+TimeToString(tm,TIME_DATE);
string str3="Только время с минутами: "+TimeToString(tm,TIME_MINUTES);
string str4="Только время с секундами: "+TimeToString(tm,TIME_SECONDS);
string str5="Дата и время с секундами: "+TimeToString(tm,TIME_DATE|TIME_SECONDS);
//--- вывод результатов
Alert(str1);
Alert(str2);
Alert(str3);
Alert(str4);
Alert(str5);

Функция TimeToString() может применяться к переменным типа datetime и переменным типа long, ulong и к некоторым другим типам целочисленных переменных, но их не следует использовать для хранения времени.

При формировании текстового сообщение функцией StringFormat() необходимо самостоятельно выполнять приведение типа.

  • При хранении времени в переменной типа datetime:

    datetime tm=TimeCurrent();
    //--- формирование строки
    string str=StringFormat("Отформатированное: %s, в секундах: %I64i",(string)tm,tm);
    //--- вывод результата
    Alert(str);
  • При хранении времени в переменной типа long:

    long tm=TimeCurrent();
    //--- формирование строки
    string str=StringFormat("Отформатированное: %s, в секундах: %I64i",(string)(datetime)tm,tm);
    //--- вывод результата
    Alert(str);
  • Или же использовать функцию TimeToString():

    datetime tm=TimeCurrent();
    //--- формирование строки
    string str=StringFormat("Дата: %s",TimeToString(tm,TIME_DATE));
    //--- вывод результата
    Alert(str);


Преобразование времени в число, сложение и вычитание времени

Для преобразования отформатированной даты (строки) в число (количество секунд от начала эпохи) применяется функция StringToTime():

datetime tm=StringToTime("2012.12.05 22:31:57");
//--- вывод результата
Alert((string)(long)tm);

В результате работы этого примера будет выведено "1354746717" - время в секундах, соответствующее дате "2012.12.05 22:31:57".

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

Отнимем или прибавим к текущему времени 1 час (3600 сек) и получим время, которое было час назад или будет через час:

datetime tm=TimeCurrent();
datetime ltm=tm-3600;
datetime ftm=tm+3600;
//--- вывод результата
Alert("Сейчас: "+(string)tm+", час назад было: "+(string)ltm+", через час будет: "+(string)ftm);

В функцию StringToTime() можно передавать не полную дату, можно передать дату без времени, или время без даты. Если передать дату без времени, функция возвращает значение на время 00:00:00 указанной даты:

datetime tm=StringToTime("2012.12.05");
//--- вывод результата
Alert(tm);

Если передать только время, функция возвращает значение, соответствующее указанному времени сегодняшней даты:

datetime tm=StringToTime("22:31:57");
//--- вывод результата
Alert((string)tm);

Также можно передавать время без секунд. Если передается дата, то из времени можно передавать только часы. На практике такое вряд ли когда потребуется, если кому-то любопытно, можете поэкспериментировать самостоятельно.


Компоненты даты и времени

Чтобы узнать значения отдельных составляющих даты и времени (год, месяц, дата и т.д.) используется функция TimeToStruct() и структура MqlDateTime. В функцию по ссылке передается структура, после выполнения функции структура будет заполнена значениями компонентов переданной ей даты:

datetime    tm=TimeCurrent();
MqlDateTime stm;
TimeToStruct(tm,stm);
//--- вывод компонентов даты
Alert("Год: "        +(string)stm.year);
Alert("Месяц: "      +(string)stm.mon);
Alert("Число: "      +(string)stm.day);
Alert("Час: "        +(string)stm.hour);
Alert("Минута: "     +(string)stm.min);
Alert("Секунда: "    +(string)stm.sec);
Alert("День недели: "+(string)stm.day_of_week);
Alert("День года: "  +(string)stm.day_of_year);

Обратите внимание, что структура содержит не только поля с компонентами даты, но и еще пару дополнительных полей: день недели (поле day_of_week) и день года (day_of_year). Отсчет дня недели начинается с 0 (0 - воскресенье, 1 - понедельник и т.д.). Отсчет дней года тоже выполняется с нуля. Остальные значения соответствуют общепринятому порядку отсчета (месяц с 1-го, число с 1-го).

Существует еще один способ вызова функции TimeCurrent(). В функцию по ссылке передается структура типа MqlDateTime. После вызова функции структура будет заполнена компонентами текущей даты:

MqlDateTime stm;
datetime tm=TimeCurrent(stm);
//--- вывод компонентов даты
Alert("Год: "        +(string)stm.year);
Alert("Месяц: "      +(string)stm.mon);
Alert("Число: "      +(string)stm.day);
Alert("Час: "        +(string)stm.hour);
Alert("Минута: "     +(string)stm.min);
Alert("Секунда: "    +(string)stm.sec);
Alert("День недели: "+(string)stm.day_of_week);
Alert("День года: "  +(string)stm.day_of_year);

Такой же вызов возможен и для функции TimeLocal().


Формирование даты из компонентов

Вы можете также провести обратное преобразование структуры MqlDateTime в тип datetime. Для этого используется функция StructToTime().

Определим время, которое было ровно месяц тому назад. Длительность месяца не является постоянной величиной, в некоторых месяцах 30 дней, в некоторых 31, а в феврале 28 или 29, поэтому рассмотренный ранее способ сложения или вычитания времени не подходит. Разложим даты на компоненты, уменьшим значение месяца на 1, если же значение месяца 1, то установим ему значение 12 и уменьшим значение года на 1:

datetime tm=TimeCurrent();
MqlDateTime stm;
TimeToStruct(tm,stm);
if(stm.mon==1)
  {
   stm.mon=12;
   stm.year--;
  }
else
  {
   stm.mon--;
  }
datetime ltm=StructToTime(stm);
//--- вывод результата
Alert("Сейчас: "+(string)tm+", месяц назад было: "+(string)ltm);


Определение времени баров

При создании индикатора, редактор MetaEditor автоматически создает один из двух вариантов функции OnCalculate().

Вариант 1:

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   return(rates_total);
  }

Вариант 2:

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {
   return(rates_total);
  }

У первого типа в параметрах функции присутствует массив time[], в элементах которого находится время всех баров.

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

CopyTime - Вариант 1. Указывается индекс бара и количество копируемых элементов:

//--- переменные для указания параметров функции
int start = 0; // индекс бара
int count = 1; // количество баров
datetime tm[]; // массив, в котором возвращается время баров
//--- копирование времени 
CopyTime(_Symbol,PERIOD_D1,start,count,tm);
//--- вывод результата
Alert(tm[0]);

В данном примере выполняется копирование времени последнего бара на таймфрейме D1. Вариант используемой функции определяется типом передаваемых ей параметров. В данном примере используются переменные типа int - это означает, что требуется получить время по номеру бара для указанного количества баров.

При использовании функции CopyTime() отсчет баров выполняется справа налево с нуля. Для получаемых значений (последний параметр) используется динамический массив, функция CopyTime() сама масштабирует его до нужно размера. Можно использовать и статический массив, но в таком случае размер массива должен строго соответствовать количеству запрашиваемых элементов (значению 4-го параметра).

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

//--- переменные для указания параметров функции
int start = 0; // индекс бара
int count = 2; // количество баров
datetime tm[]; // массив, в котором возвращается время баров
//--- копирование времени 
CopyTime(_Symbol,PERIOD_D1,start,count,tm);
//--- вывод результата
Alert("Сегодня: "+(string)tm[1]+", вчера: "+(string)tm[0]);

В результате выполнения этого кода в 0-м элементе массива tm будет время вчерашнего бара, в 1-м элементе - время сегодняшнего бара.

В некоторых случаях было бы удобней, если бы время в массиве располагалось в таком же порядке, как и отсчет баров графика. Для этого можно воспользоваться функцией ArraySetAsSeries():

//--- переменные для указания параметров функции
int start = 0; // индекс бара
int count = 2; // количество баров
datetime tm[]; // массив, в котором возвращается время баров
ArraySetAsSeries(tm,true); // указываем, что массив будет с обратным отсчетом элементов
//--- копирование времени 
CopyTime(_Symbol,PERIOD_D1,start,count,tm);
//--- вывод результата
Alert("Сегодня: "+(string)tm[0]+", вчера: "+(string)tm[1]);

Теперь время сегодняшнего бара находится в элементе с индексом 0, а время вчерашнего бара - в элементе с индексом 1.

CopyTime - Вариант 2. В этом случае при вызове функции CopyTime() указывается время бара, с которого начинается копирование, и количество копируемых баров. Такой вариант подойдет для определения времени старшего таймфрейма, в который входит бар младшего таймфрейма:

//--- получим время последнего бара таймфрейма M5
int m5_start=0; 
int m5_count=1;
datetime m5_tm[];
CopyTime(_Symbol,PERIOD_M5,m5_start,m5_count,m5_tm);
//--- определим время бара на таймфрейме H1, в который входит бар M5
int h1_count=1;
datetime h1_tm[];
CopyTime(_Symbol,PERIOD_H1,m5_tm[0],h1_count,h1_tm);
//--- вывод результата
Alert("Бар М5 со временем "+(string)m5_tm[0]+" входит в бар H1 со временем "+(string)h1_tm[0]);

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

Пример оформлен в виде функции, полностью готовой к практическому применению:

bool LowerTFFirstBarTime(string aSymbol,
                         ENUM_TIMEFRAMES aLowerTF,
                         datetime aUpperTFBarTime,
                         datetime& aLowerTFFirstBarTime)
  {
   datetime tm[];
//--- определяем время бара младшего таймфрейма, соответствующее времени бара старшего таймфрейма 
   if(CopyTime(aSymbol,aLowerTF,aUpperTFBarTime,1,tm)==-1)
     {
      return(false);
     }
   if(tm[0]<aUpperTFBarTime)
     {
      //--- получено время предшествующего бара
      datetime tm2[];
      //--- определяем время последнего бара младшего таймфрейма
      if(CopyTime(aSymbol,aLowerTF,0,1,tm2)==-1)
        {
         return(false);
        }
      if(tm[0]<tm2[0])
        {
         //--- есть бар после бара младшего таймфрейма, предшествующего открытию бара старшего таймфрейма
         int start=Bars(aSymbol,aLowerTF,tm[0],tm2[0])-2;
         //--- функция Bars() возвращает количеств баров от бара со временем tm[0] до
         //--- бара со временем tm2[0], а необходимо определить индекс бара следующего 
         //--- за баром со временем tm2[2], поэтому вычитается 2
         if(CopyTime(aSymbol,aLowerTF,start,1,tm)==-1)
           {
            return(false);
           }
        }
      else
        {
         //--- нет бара младшего таймфрейма, входящего в бар старшего таймфрейма
         aLowerTFFirstBarTime=0;
         return(true);
        }
     }
//--- присваиваем переменной полученное значение 
   aLowerTFFirstBarTime=tm[0];
   return(true);
  }

Параметры функции:

  • aSymbol - символ;
  • aLowerTF - младший таймфрейм;
  • aUpperTFBarTime - время бара старшего таймфрейма;
  • aLowerTFFirstBarTime - возвращаемое значение младшего таймфрейма.

В функции везде проверятся успешность вызова функции CopyTime(), в случае ошибки функция возвращает false. Время бара возвращается по ссылке через параметр aLowerTFFirstBarTime.

Пример использования функции:

//--- время бара старшего таймфрейма H1
   datetime utftm=StringToTime("2012.12.10 15:00");
//--- переменная для возвращаемого значения
   datetime val;
//--- вызов функции
   if(LowerTFFirstBarTime(_Symbol,PERIOD_M5,utftm,val))
     {
      //--- вывод результата в случае успешной работы функции
      Alert("val = "+(string)val);
     }
   else
     {
      //--- в случае ошибки прерываем работу функции, из которой выполняется вызов функции LowerTFFirstBarTime()
      Alert("Ошибка копирования времени");
      return;
     }

Если бара младшего таймфрейма, входящего в бар старшего таймфрейма, не существует, это может быть в случае, если функции передано время несуществующего бара старшего таймфрейма. Функция при этом возвращает true, а в переменную aLowerTFFirstBarTime записывается время 0. Если бар старшего таймфрейма существует, для него обязательно будет существовать хотя бы один бар в каждом младшем таймфрейме.

Определение времени последнего бара младшего таймфрейма, входящего в бар старшего таймфрейма, немного проще. Вычисляется время следующего бара старшего таймфрейма, по полученному времени определяется время соответствующего бара младшего таймфрейма. Если полученное время равно вычисленному времени старшего таймфрейма, значит нужно определить время предшествующего бара младшего таймфрейма, а если же меньше - значит уже получено правильное время.

Пример оформлен в виде функции, полностью готовой к практическому применению:

bool LowerTFLastBarTime(string aSymbol,
                        ENUM_TIMEFRAMES aUpperTF,
                        ENUM_TIMEFRAMES aLowerTF,
                        datetime aUpperTFBarTime,
                        datetime& aLowerTFFirstBarTime)
  {
//--- время следующего бара старшего таймфрейма
   datetime NextBarTime=aUpperTFBarTime+PeriodSeconds(aUpperTF);
   datetime tm[];
   if(CopyTime(aSymbol,aLowerTF,NextBarTime,1,tm)==-1)
     {
      return(false);
     }
   if(tm[0]==NextBarTime)
     {
      //--- Существует бар младшего таймфрейма, соответствующий времени следующего бара старшего таймфрейма.
      //--- Определяем время последнего бара младшего таймфрейма
      datetime tm2[];
      if(CopyTime(aSymbol,aLowerTF,0,1,tm2)==-1)
        {
         return(false);
        }
      //--- определяем индекс предшествующего бара младшего таймфрейма
      int start=Bars(aSymbol,aLowerTF,tm[0],tm2[0]);
      //--- определяем время этого бара
      if(CopyTime(aSymbol,aLowerTF,start,1,tm)==-1)
        {
         return(false);
        }
     }
//--- присваиваем переменной полученное значение 
   aLowerTFFirstBarTime=tm[0];
   return(true);
  }

Параметры функции:

  • aSymbol - символ;
  • aUpperTF - старший таймфрейм;
  • aLowerTF - младший таймфрейм;
  • aUpperTFBarTime - время бара старшего таймфрейма;
  • aLowerTFFirstBarTime - возвращаемое значение младшего таймфрейма.

В функции везде проверятся успешность вызова функции CopyTime(), в случае ошибки функция возвращает false. Время бара возвращается по ссылке через параметр aLowerTFFirstBarTime.

Пример использования функции:

//--- время бара старшего таймфрейма H1
datetime utftm=StringToTime("2012.12.10 15:00");
//--- переменная для возвращаемого значения
datetime val;
//--- вызов функции
if(LowerTFLastBarTime(_Symbol,PERIOD_H1,PERIOD_M5,utftm,val))
  {
//--- вывод результата в случае успешной работы функции
   Alert("val = "+(string)val);
  }
else
  {
//--- в случае ошибки прерываем работу функции, из которой выполняется вызов функции LowerTFFirstBarTime()
   Alert("Ошибка копирования времени");
   return;
  }

Внимание! Подразумевается, что в функцию передается точное время старшего таймфрейма. Если точное время бара неизвестно, а известно только время какого-нибудь тика внутри бара или бара младшего таймфрейма, входящего в бар старшего таймфрейма, потребуется нормализация времени. В терминале MetaTrader 5 используются таймфреймы, разделяющие сутки на целое число баров. Определяем номер бара от начала эпохи, затем умножаем на длительность бара в секундах:

datetime BarTimeNormalize(datetime aTime,ENUM_TIMEFRAMES aTimeFrame)
  {
   int BarLength=PeriodSeconds(aTimeFrame);
   return(BarLength*(aTime/BarLength));
  }

Параметры функции:

  • aTime - время;
  • aTimeFrame - таймфрейм.

Пример использования функции:

//--- нормализуемое время
datetime tm=StringToTime("2012.12.10 15:25");
//--- вызов функции
datetime tm1=BarTimeNormalize(tm,PERIOD_H1);
//--- вывод результата
Alert(tm1);

Впрочем, это не только нормализация времени, но и еще один способ определения времени старшего таймфрейма по времени младшего таймфрейма.

CopyTime - Вариант 3. В этом случае при вызове функции CopyTime() указывается диапазон времени баров, с которых необходимо выполнить копирование времени. Таким способом легко получить все бары младшего таймфрейма, входящие в бар старшего таймфрейма:

//--- время начала бара таймфрейма H1
datetime TimeStart=StringToTime("2012.12.10 15:00");
//--- предположительное время последнего бара на
//--- таймфрейме M5
datetime TimeStop=TimeStart+PeriodSeconds(PERIOD_H1)-PeriodSeconds(PERIOD_M5);
//--- копируем время
datetime tm[];
CopyTime(_Symbol,PERIOD_M5,TimeStart,TimeStop,tm);
//--- вывод результата 
Alert("Скопировано баров: "+(string)ArraySize(tm)+", первый бар: "+(string)tm[0]+", последний: "+(string)tm[ArraySize(tm)-1]);


Определение времени начала дня и времени, прошедшего с начала дня

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

datetime tm=TimeCurrent();
tm=(tm/86400)*86400;
//--- вывод результата
Alert("Время начала дня: "+(string)tm);

Внимание! Такой трюк пройдет только с целочисленными переменными, если в каких-то вычислениях используются переменные типа double, float, необходимо отбрасывать дробную часть, используя функцию MathFloor():

MathFloor(tm/86400)*86400

Кроме того, после умножения необходимо выполнить нормализацию значения функцией NormalizeDouble(), поскольку должно получиться целочисленное значение, можно использовать функцию округления MathRound():

MathRound(MathFloor(tm/86400)*86400)

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

Для определения количество секунд, прошедших с начала дня, достаточно взять остаток от деления времени на 86400:

datetime tm=TimeCurrent();
long seconds=tm%86400;
//--- вывод результата
Alert("От начала дня прошло: "+(string)seconds+" сек.");

Подобными приёмами можно преобразовать полученное в секундах время в часы, минуты, секунды. Оформлено как функция:

int TimeFromDayStart(datetime aTime,int &aH,int &aM,int &aS)
  {
//--- Количество секунд, прошедших с начала дня (aTime%86400),
//--- делим на количество секунд часа, получаем количество часов
   aH=(int)((aTime%86400)/3600);
//--- Количество секунд, прошедших с начала последнего часа (aTime%3600),
//--- делим на количество секунд в минуте, получаем количество минут 
   aM=(int)((aTime%3600)/60);
//--- Количество секунд, прошедших с начала последней минуты 
   aS=(int)(aTime%60);
//--- Количество секунд с начала дня
   return(int(aTime%86400));
  }

Первым параметром передается время, остальные параметры используются для возвращаемых значений: aH - часы, aM - минуты, aS - секунды, сама функция возвращает общее количество секунд с начала дня. Проверим функцию:

datetime tm=TimeCurrent();
int t,h,m,s;
t=TimeFromDayStart(tm,h,m,s);
//--- вывод результатов
Alert("От начала дня прошло ",t," с, что составляет ",h," ч, ",m," м, ",s," с ");

Вычисление номера дня может использоваться в индикаторах для определения бара начала дня:

bool NewDay=(time[i]/86400)!=(time[i-1]/86400);

Подразумевается, что используется нумерация баров слева направо, time[i] - время текущего бара, time[i-1] - время предыдущего бара.


Определение времени начала недели и времени, прошедшего с начала недели

Определение времени начала недели выполняется несколько сложнее, чем определение начала дня. Хотя в неделе фиксированное количество дней и можно вычислить ее длительность в секундах (получается 604800 секунд), недостаточно вычислить целое количество недель прошедших с начала эпохи и умножить на длительность недели.

Дело в том, что неделя в большинстве стран мира начинается с понедельника, а в некоторых (США, Канада, Израиль и др.) - с воскресенья. Но эпоха отсчета времени, как мы помним, начинается с четверга. Если бы неделя начиналась с четверга, было бы достаточно выполнить такие простые вычисления.

Особенности вычисления начала недели будет удобно рассматривать на примере первого дня эпохи, соответствующего значению 0. Необходимо ко времени добавить такое значение, чтобы первый день эпохи (1970.01.01 00:00), при отсчете от нуля стал четвертым днем, т.е. надо прибавить длительность четырех дней. Если неделя начинается с понедельника, то четверг должен быть четверым днем, значит надо прибавить длительность трех дней. Если же неделя начинается с воскресенья, то четверг должен быть пятым днем, значит надо прибавить длительность четырех дней.

Напишем функцию для вычисления порядкового номера недели:

long WeekNum(datetime aTime,bool aStartsOnMonday=false)
  {
//--- если неделя начинается с воскресенья, прибавляем длительность 4 дней (среда+вторник+понедельник+воскресенье),
//    если с понедельника - 3 дней (среда+вторник+понедельник)
   if(aStartsOnMonday)
     {
      aTime+=259200; // длительность трех дней (86400*3)
     }
   else
     {
      aTime+=345600; // длительность четырех дней (86400*4)  
     }
   return(aTime/604800);
  }

Эта функция может быть полезной в индикаторах для определения первого бара новой недели:

bool NewWeek=WeekNum(time[i])!=WeekNum(time[i-1]);

Подразумевается, что используется нумерация баров слева направо, time[i] - время текущего бара, time[i-1] - время предыдущего бара.

Теперь можно вычислить время начала недели. Поскольку для вычисления номера недели принималось, что эпоха начинается на три (или четыре) дня раньше, значит необходимо выполнить обратную коррекцию времени:

long WeekStartTime(datetime aTime,bool aStartsOnMonday=false)
  {
   long tmp=aTime;
   long Corrector;
   if(aStartsOnMonday)
     {
      Corrector=259200; // длительность трех дней (86400*3)
     }
   else
     {
      Corrector=345600; // длительность четырех дней (86400*4)
     }
   tmp+=Corrector;
   tmp=(tmp/604800)*604800;
   tmp-=Corrector;
   return(tmp);
  }  

Функция возвращает значение типа long, потому что для самой первой недели значение времени может быть отрицательным (на три-четыре дня раньше начала эпохи). Вторым параметром функции можно определять, начинается ли неделя с воскресенья или с понедельника.

Располагая временем начала недели, можно посчитать количество секунд, прошедших с начала недели:

long SecondsFromWeekStart(datetime aTime,bool aStartsOnMonday=false)
  {
   return(aTime-WeekStartTime(aTime,aStartsOnMonday));
  }

Секунды можно перевести в количество дней, часов, минут, секунд. Если при вычислении количества часов, минут и секунд от начала дня было не сложно провести вычисления, в данном случае будет проще использовать функцию TimeToStruct():

long sfws=SecondsFromWeekStart(TimeCurrent());
MqlDateTime stm;
TimeToStruct(sfws,stm);
stm.day--;
Alert("C начала недели прошло "+(string)stm.day+" д, "+(string)stm.hour+" ч, "+(string)stm.min+" м, "+(string)stm.sec+" с");

Обратите внимание, что значение stm.day уменьшается на 1. Число месяца отсчитывается от 1-го, нам же надо определить количество целых дней. Кто-то может посчитать функции этого раздела бесполезными с практической точки зрения, но все же их понимание будет хорошей практикой в работе со временем.


Определение номера недели с заданной даты, с начала года, с начала месяца

Глядя на поля структуры MqlDateTime, в частности - на поле day_of_year, возникает желание создать функции для определения номера недели с начала года, номера недели с начала месяца. Лучше создать универсальную функцию для определения номера недели с заданной даты. Принцип работы функции подобен принципу, использовавшемуся для определения номера недели от начала эпохи:

long WeekNumFromDate(datetime aTime,datetime aStartTime,bool aStartsOnMonday=false)
  {
   long Time,StartTime,Corrector;
   MqlDateTime stm;
   Time=aTime;
   StartTime=aStartTime;
//--- определение начала условной эпохи
   StartTime=(StartTime/86400)*86400;
//--- определение времени, прошедшего с начала условной эпохи
   Time-=StartTime;
//--- определение дня недели начала условной эпохи
   TimeToStruct(StartTime,stm);
//--- если неделя начинается с понедельника, номера дней недели уменьшаются на 1,
//    а день с номером 0 становится днем с номером 6
   if(aStartsOnMonday)
     {
      if(stm.day_of_week==0)
        {
         stm.day_of_week=6;
        }
      else
        {
         stm.day_of_week--;
        }
     }
//--- вычисление величины корректора времени 
   Corrector=86400*stm.day_of_week;
//--- коррекция времени
   Time+=Corrector;
//--- вычисление и возвращение номера недели
   return(Time/604800);
  }

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

  • Функция определения времени начала года:

    datetime YearStartTime(datetime aTime)
          {
           MqlDateTime stm;
           TimeToStruct(aTime,stm);
           stm.day=1;
           stm.mon=1;
           stm.hour=0;
           stm.min=0;
           stm.sec=0;
           return(StructToTime(stm));
          }
  • Функция определения времени начала месяца:

    datetime MonthStartTime(datetime aTime)
          {
           MqlDateTime stm;
           TimeToStruct(aTime,stm);
           stm.day=1;
           stm.hour=0;
           stm.min=0;
           stm.sec=0;
           return(StructToTime(stm));
          }

Теперь функции определения номера недели с начала года и начала месяца.

  • С начала года:

    long WeekNumYear(datetime aTime,bool aStartsOnMonday=false)
          {
           return(WeekNumFromDate(aTime,YearStartTime(aTime),aStartsOnMonday));
          }
  • С начала месяца:

    long WeekNumMonth(datetime aTime,bool aStartsOnMonday=false)
          {
           return(WeekNumFromDate(aTime,MonthStartTime(aTime),aStartsOnMonday));
          }

Наконец подходим к абсолютно практическим задачам.


Создание экспериментального инструментария

Как уже упоминалось, существуют ДЦ с воскресными барами, с котированием в течение всех выходных. Хотелось бы убедиться в правильности работы некоторых функций во всех случаях. Конечно, можно разыскать в интернете подходящие ДЦ и проверять работу функций на котировках их демо-счетов, но кроме поиска ДЦ еще потребуется выискивать на графике подходящие для проверки места.

Создадим собственный тестовый "полигон" для проверки функций. Особый интерес представляет пятница, выходные и понедельник. Создадим массив со временем баров пятницы, понедельника и, по мере необходимости, с барами в выходные. Всего получается четыре варианта:

  1. Без баров в выходные.
  2. Несколько баров в конце воскресенья. Пусть будет 4-ре бара в конце воскресенья.
  3. Непрерывное котирование в выходные.
  4. В субботу есть бары, в воскресенье нет.

Чтобы массив не получился слишком большим, будем использовать таймфрейм H1. Максимальный размер массива при этом будет равен 96 элементам (24 бара в сутках и 4 дня), а сам массив уместится на графике при рисовании графическими объектами. Получится подобие индикаторного буфера со временем, а прохождение по массиву в цикле будет подобно первому выполнению функции OnCalculate() индикатора на запуске. Таким способом получится наглядно увидеть работу функций.

Данный инструмент выполнен в виде скрипта, который прилагается к статье (файл "sTestArea.mq5"). В функции OnStart() скрипта выполняется подготовка, на самом верху кода функции находится переменная Variant, позволяющая выбирать один из четырех вышеперечисленных вариантов. Ниже функции OnStart() расположена функция LikeOnCalculate(), похожая на функцию OnCalculate() индикатора. У этой функции два параметра: rates_total - количество баров и time[] - массив со временем баров.

Дальше работаем в этой функции, как будто пишем индикатор, из нее можно вызывать функцию SetMarker() для установки значка. В функцию SetMarker() передается три параметра: индекс бара, индекс буфера (ряда, в котором отображается маркер) и цвет маркера.

На рис. 4 показан результат работы скрипта при значении переменной Variant = 2 с двумя рядами маркеров под каждым баром (бары отмечаются надписями со временем баров). Всем элементам графика установлен невидимый цвет.

Рис. 4. Работа скрипта sTestArea.mq5.
Рис. 4. Работа скрипта sTestArea.mq5.

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


Индикатор Pivot - Вариант 1

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

Предусмотрим два варианта работы индикатора:

  1. Расчет Pivot выполняется каждый новый день (подразумевается, что в выходные нет баров). Если бары в выходные будут, то эти дни с барами будут расцениваться как отдельные дни.
  2. Субботние бары будут относиться к пятнице, воскресенье к понедельнику (подойдет для случая котирования в течение всех выходных и для случая только воскресных баров). В этом случаем следует иметь в виду, что баров за выходные дни может и не быть.

В первом случае достаточно определить только начало нового дня. В функцию передается текущее время (aTimeCur) и предыдущее (aTimePre), вычисляются номера дней от начала эпохи, если они не совпадают, значит, начался новый день:

bool NewDay1(datetime aTimeCur,datetime aTimePre)
  {
   return((aTimeCur/86400)!=(aTimePre/86400));
  }

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

bool NewDay2(datetime aTimeCur,datetime aTimePre)
  {
   MqlDateTime stm;
//--- новый день
   if(NewDay1(aTimeCur,aTimePre))
     {
      TimeToStruct(aTimeCur,stm);
      switch(stm.day_of_week)
        {
         case 6: // суббота
            return(false);
            break;
         case 0: // воскресенье
            return(true);
            break;
         case 1: // понедельник
            TimeToStruct(aTimePre,stm);
            if(stm.day_of_week!=0)
              { // предыдущий день не воскресенье
               return(true);
              }
            else
              {
               return(false);
              }
            break;
         default: // любой другой день недели
            return(true);
        }
     }
   return(false);
  }

Теперь общая функция в зависимости от варианта:

bool NewDay(datetime aTimeCur,datetime aTimePre,int aVariant=1)
  {
   switch(aVariant)
     {
      case 1:
         return(NewDay1(aTimeCur,aTimePre));
         break;
      case 2:
         return(NewDay2(aTimeCur,aTimePre));
         break;
     }
   return(false);
  }

Проверим работу функции при помощи инструмента "sTestArea" (в приложении файл sTestArea_Pivot1.mq5, начало дня отмечается маркером коричневого цвета). Всего следует провести восемь проверок, два варианта функции для четырех вариантов формирования баров. Убедившись в правильности работы функций, можно смело приступать к написанию индикатора. Программирование индикаторов не является целью данной статьи, поэтому к статье прилагается уже готовый индикатор (файл Pivot1.mq5), самый сложный момент его создания рассмотрен подробно.


Определение временной сессии

Необходимо разрешить эксперту торговать в заданные интервалы времени в течение дня, каждый день в один и тот же интервал. Указывается час и минута начала торговой сессии, час и минута окончания торговой сессии. Раздельное указание часов и минут, а не строковые переменные с указанием времени типа "14:00", позволит проводить оптимизацию в тестере, если функция используется в эксперте.

Для определения временной сессии выполняем следующие действия:

  1. Вычисляем время в секундах от начала дня для начального момента времени и так же для конечного момента времени.
  2. Вычисляем текущее время в секундах от начала дня.
  3. Сравниваем текущее время с начальным и конечным временем.

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

bool TimeSession(int aStartHour,int aStartMinute,int aStopHour,int aStopMinute,datetime aTimeCur)
  {
//--- время начала сессии
   int StartTime=3600*aStartHour+60*aStartMinute;
//--- время окончания сессии
   int StopTime=3600*aStopHour+60*aStopMinute;
//--- текущее время в секундах от начала дня
   aTimeCur=aTimeCur%86400;
   if(StopTime<StartTime)
     {
      //--- переход через полночь
      if(aTimeCur>=StartTime || aTimeCur<StopTime)
        {
         return(true);
        }
     }
   else
     {
      //--- внутри одного дня
      if(aTimeCur>=StartTime && aTimeCur<StopTime)
        {
         return(true);
        }
     }
   return(false);
  }

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

Для проверки работы функции создан индикатор (файл 'Session.mq5' в приложении к статье). Им можно пользоваться не только для проверки, но и практически, как всеми другими индикаторами приложения.


Определение момента времени дня

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

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

Получается следующая функция:

bool TimeCross(int aHour,int aMinute,datetime aTimeCur,datetime aTimePre)
  {
//--- заданное время от начала дня
   datetime PointTime=aHour*3600+aMinute*60;
//--- текущее время от начала дня
   aTimeCur=aTimeCur%86400;
//--- предыдущее время от начала дня
   aTimePre=aTimePre%86400;
   if(aTimeCur<aTimePre)
     {
      //--- переход через полночь
      if(aTimeCur>=PointTime || aTimePre<PointTime)
        {
         return(true);
        }
     }
   else
     {
      if(aTimeCur>=PointTime && aTimePre<PointTime)
        {
         return(true);
        }
     }
   return(false);
  }

На основе этой функции сделан индикатор (файл 'TimePoint.mq5' в приложении к статье).


Индикатор Pivot - Вариант 2

Научившись определять момент времени, усложним индикатор Pivot. Теперь день будет начинаться не как обычно в 00:00, а в любое заданное время. Будем называть его пользовательским днем. Для определения начала пользовательского дня будет использоваться описанная ранее функция TimeCross(). Из-за различных вариантов формирования баров в выходные дни, некоторые дни надо будет пропускать. Сразу сложно представить все правила проверок, поэтому придется все делать постепенно. Главное, чтобы было с чего начать и были варианты как продолжить. У нас имеется проверочный скрипт 'sTestArea.mq5', поэтому возможен и экспериментальный поиск правильного решения.

Случай полного отсутствия баров в выходные дни наиболее прост: в момент пересечения временем заданной временной отметки начинается новый день.

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

Случай непрерывных котировок в выходные: Если начало пользовательского дня приходится на середину календарного дня (рис. 5),

Рис. 5. Начало пользовательского дня приходится на середину календарного дня. Пятница - красный, суббота - фиолетовый, воскресенье - зеленый, понедельник - синий.
Рис. 5. Начало пользовательского дня приходится на середину календарного дня.
Пятница - красный, суббота - фиолетовый, воскресенье - зеленый, понедельник - синий.

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

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

Получается следующая функция:

bool NewCustomDay(int aHour,int aMinute,datetime aTimeCur,datetime aTimePre)
  {
   MqlDateTime stm;
   if(TimeCross(aHour,aMinute,aTimeCur,aTimePre))
     {
      TimeToStruct(aTimeCur,stm);
      if(stm.day_of_week==0 || stm.day_of_week==6)
        {
         return(false);
        }
      else
        {
         return(true);
        }
     }
   return(false);
  }

На основе этой функции сделан индикатор (файл 'Pivot2.mq5' в приложении к статье).


Определение торговых дней недели

Разрешить эксперту торговать только в определенные дни довольно просто. Выполняется разложение времени на компоненты функцией TimeToStruct(), в параметрах эксперта объявляются переменные типа bool для каждого для каждого из дней недели. В зависимости от дня недели функция возвращает значение соответствующей переменной.

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

Переменные:

input bool Sunday   =true; // Воскресенье
input bool Monday   =true; // Понедельник
input bool Tuesday  =true; // Вторник 
input bool Wednesday=true; // Среда
input bool Thursday =true; // Четверг
input bool Friday   =true; // Пятница
input bool Saturday =true; // Суббота

bool WeekDays[7];

Функция инициализации:

void WeekDays_Init()
  {
   WeekDays[0]=Sunday;
   WeekDays[1]=Monday;
   WeekDays[2]=Tuesday;
   WeekDays[3]=Wednesday;
   WeekDays[4]=Thursday;
   WeekDays[5]=Friday;
   WeekDays[6]=Saturday;
  }

Основная функция:

bool WeekDays_Check(datetime aTime)
  {
   MqlDateTime stm;
   TimeToStruct(aTime,stm);
   return(WeekDays[stm.day_of_week]);
  }

На основе этой функции сделан индикатор 'TradeWeekDays.mq5', который прилагается к статье.


Определение торгового времени недели

Необходимо определить торговые сессии от заданного времени одного дня недели до другого времени другого дня недели. Эта функция подобна функции TimeSession(), только вычисления выполняются на основе времени, прошедшего от начала недели. Получается следующая функция:

bool WeekSession(int aStartDay,int aStartHour,int aStartMinute,int aStopDay,int aStopHour,int aStopMinute,datetime aTimeCur)
  {
//--- время начала сессии от начала недели
   int StartTime=aStartDay*86400+3600*aStartHour+60*aStartMinute;
//--- время окончания сессии от начала недели
   int StopTime=aStopDay*86400+3600*aStopHour+60*aStopMinute;
//--- текущее время в секундах от начала недели
   long TimeCur=SecondsFromWeekStart(aTimeCur,false);
   if(StopTime<StartTime)
     {
      //--- переход через смену недели
      if(TimeCur>=StartTime || TimeCur<StopTime)
        {
         return(true);
        }
     }
   else
     {
      //--- внутри одной недели
      if(TimeCur>=StartTime && TimeCur<StopTime)
        {
         return(true);
        }
     }
   return(false);
  }

На основе этой функции сделан индикатор 'SessionWeek.mq5', который прилагается к статье.

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


Дополнительные функции MQL5

Осталось еще несколько функций MQL5 по работе со временем: TimeTradeServer(), TimeGMT(), TimeDaylightSavings(), TimeGMTOffset(). Основная особенность этих функций в том, что они пользуются часами и настройками времени пользовательского компьютера.

Функция TimeTradeServer(). Выше в статье упоминалось, что функция TimeCurrent() на выходные будет показывать неправильное время (время последнего изменения цены в пятницу). Функция TimeTradeServer() вычислит правильное серверное время:

datetime tm=TimeTradeServer();
//--- вывод результата
Alert(tm);

Функция TimeGMT(). Функция вычисляет время GMT исходя из показаний времени компьютера пользователя и настроек: часового пояса, поправки на летнее время:

datetime tm=TimeGMT();
//--- вывод результата
Alert(tm);

Если быть точнее, функция возвращает время UTC.

Функция TimeDaylightSavings(). Функция возвращает величину поправки времени на летнее время из настроек компьютера пользователя.

int val=TimeDaylightSavings();
//--- вывод результата
Alert(val);

Чтобы узнать время без учета поправки на летнее время, к локальному времени нужно прибавить значение поправки.

Функция TimeGMTOffset(). Функция позволяет узнать часовой пояс компьютера пользователя. Значение возвращается в секундах, которые необходимо прибавить к локальному времени, чтобы получить время GMT.

int val=TimeGMTOffset();
//--- вывод результата
Alert(val);

Время на компьютере пользователя, будет равняться TimeGMT()-TimeGMTOffset()-TimeDaylightSavings():

datetime tm1=TimeLocal();
datetime tm2=TimeGMT()-TimeGMTOffset()-TimeDaylightSavings();
//--- вывод результата
Alert(tm1==tm2);


Еще немного полезных функций со временем

Определение високосного года

bool LeapYear(datetime aTime)
  {
   MqlDateTime stm;
   TimeToStruct(aTime,stm);
//--- кратен 4 
   if(stm.year%4==0)
     {
      //--- кратен 100
      if(stm.year%100==0)
        {
         //--- кратен 400
         if(stm.year%400==0)
           {
            return(true);
           }
        }
      //--- не кратен 100 
      else
        {
         return(true);
        }
     }
   return(false);
  }

Принцип определения високосного года описан в разделе "Особенности календарного летоисчисления".

Определение количества дней в месяце

int DaysInMonth(datetime aTime)
  {
   MqlDateTime stm;
   TimeToStruct(aTime,stm);
   if(stm.mon==2)
     {
      //--- февраль
      if(LeapYear(aTime))
        {
         //--- февраль високосного года 
         return(29);
        }
      else
        {
         //--- февраль обычного года 
         return(28);
        }
     }
   else
     {
      //--- остальные месяцы
      return(31-((stm.mon-1)%7)%2);
     }
  }

Для февраля проверятся високосный год и возвращается значение 28 или 29, для остальных месяцев выполняется вычисление. Первые 7 месяцев дни месяца чередуются: 31, 30 31, 30 и т.д., также чередуются оставшиеся 5-ть месяцев, поэтому вычисляется остаток от деления на 7. После этого проверятся четность/нечетность, полученная поправка отнимается от 31.


Особенности работы функций времени в тестере

Тестер генерирует свой поток котировок, значения функции TimeCurrent() соответствуют потоку котировок в тестере. Значения функции TimeTradeServer() соответствуют значениям TimeCurrent(). Так же и значения функции TimeLocal() соответствуют значению TimeCurrent(). Функция TimeCurrent() в тестере не учитывает часовой пояс и поправку на летнее время. Работа экспертов основывается на изменениях цены, поэтому, если требуется какая-то работа эксперта по времени, необходимо пользоваться функцией TimeCurrent(), тогда эксперта можно смело тестировать в тестере.

Работа функций TimeGMT(), TimeDaylightSavings(), TimeGMTOffset() основана исключительно на текущих настройках компьютера пользователя (переходы на летнее/зимнее время не моделируются в тестере). Если при тестировании эксперта необходимо смоделировать переходы на летнее/зимнее время (если кому-то это будет действительно необходимо) об этом нужно позаботиться самостоятельно. Потребуется информация о точных датах и времени перевода часов, тщательный анализ.

Решение данной задачи значительно выходит за пределы одной статьи и не рассматривается. Если эксперт работает по времени в Европейскую или Американскую сессию, а ДЦ выполняет переход на летнее время, рассогласования серверного времени со временем событий не будет происходить, в отличие от Азиатской сессии (Япония не переходит на летнее время, Австралия переходит на летнее время в ноябре).


Заключение

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

Все стандартные функции по работе со временем можно разделить на несколько категорий:

  1. TimeCurrent(), TimeLocal() - основные функции, используются для определения текущего времени.
  2. TimeToString(), StringToTime(), TimeToStruct(), StructToTime() - функции обработки времени.
  3. CopyTime() - функция для работы со временем баров.
  4. TimeTradeServer(), TimeGMT(), TimeDaylightSavings(), TimeGMTOffset() - функции, зависящие от настроек компьютера пользователя.


Список файлов приложения

  • sTestArea.mq5 - скрипт для проверки сложных функций времени.
  • sTestArea_Pivot1.mq5 - использование скрипта sTestArea.mq5 для проверки функций времени индикатора Pivot1.mq5.
  • Pivot1.mq5 - индикатор Pivot с использованием стандартных дней (функция NewDay).
  • Session.mql5 - индикатор торговой сессии дня (функция TimeSession).
  • TimePoint.mq5 - индикатор заданного момента времени (функция TimeCross).
  • Pivot2.mq5 - индикатор Pivot с пользовательскими днями (функция NewCustomDay).
  • TradeWeekDays.mq5 - индикатор торговых дней недели (функция WeekDays_Check).
  • SessionWeek.mq5 - индикатор торговой сессии недели (функция WeekSession).
  • TimeFunctions.mqh - все функции времени, приведенные в этой статье, в одном файле.
Прикрепленные файлы |
stestarea.mq5 (4.99 KB)
pivot1.mq5 (4.5 KB)
session.mq5 (3.52 KB)
timepoint.mq5 (3.31 KB)
pivot2.mq5 (4.13 KB)
sessionweek.mq5 (5.08 KB)
timefuncions.mqh (20.26 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (32)
Rashid Umarov
Rashid Umarov | 19 февр. 2016 в 10:31
Dmitry Fedoseev:

Были они. Отвалились куда-то. Наверно при каком-нибудь обновлении сервера. 

Сейчас поищу у себя, если найду, прикреплю сюда. 

Нашли и вернули исходники на место. Причина пропажи пока необъяснима.
Denis Kirichenko
Denis Kirichenko | 8 янв. 2018 в 11:58

Вопрос такой в контексте темы...

Нужен аналог TimeCurrent() с точностью до миллисекунды, что-то такого вида TimeCurrentMsс().

fxsaber
fxsaber | 8 янв. 2018 в 12:41
Dennis Kirichenko:

Нужен аналог TimeCurrent() с точностью до миллисекунды, что-то такого вида TimeCurrentMsс().

Форум по трейдингу, автоматическим торговым системам и тестированию торговых стратегий

Market closed

fxsaber, 2017.09.22 09:45

// Время последнего тика символа
long GetSymbolTime( const string Symb )
{
  MqlTick Tick;
  
  return(SymbolInfoTick(Symb, Tick) ? Tick.time_msc : 0);
}

// Время последнего тика Обзора рынка
long GetMarketWatchTime( void )
{
  long Res = 0;
  
  for (int i = SymbolsTotal(true) - 1; i >= 0; i--)
  {
    const long TmpTime = GetSymbolTime(SymbolName(i, true));
    
    if (TmpTime > Res)
      Res = TmpTime;
  }
  
  return(Res);
}

// Текущее время на торговом сервере без учета пинга
long GetCurrenTime( void )
{
  static ulong StartTime = GetMicrosecondCount();
  static long PrevTime = 0;
  
  const long TmpTime = GetMarketWatchTime();
  
  if (TmpTime > PrevTime)
  {
    PrevTime = TmpTime;
    
    StartTime = GetMicrosecondCount();
  }
  
  return(PrevTime + (long)((GetMicrosecondCount() - StartTime) / 1000));
}

void OnInit()
{
  MarketBookAdd(_Symbol);
}

void OnDeinit( const int )
{
  MarketBookRelease(_Symbol);
}

string TimeToString( const long Value )
{
  return((string)(datetime)(Value / 1000) + "." + (string)IntegerToString(Value % 1000, 3, '0'));
}

void OnBookEvent( const string& )
{
  Comment(TimeToString(GetCurrenTime()));
}

Не идеально, конечно.

Denis Kirichenko
Denis Kirichenko | 8 янв. 2018 в 13:06

fxsaber, спасибо! Вы как всегда по делу. Респект

Aleksey Panfilov
Aleksey Panfilov | 28 февр. 2018 в 08:52
Статья понравилась и пригодилась, спасибо!
Рецепты MQL5 -  Вывод информации на печать в разных режимах Рецепты MQL5 - Вывод информации на печать в разных режимах
Это первая статья из серии "Рецепты MQL5". Я начну с простых примеров, чтобы те, кто только начинает изучать программирование, могли плавно погрузиться в изучение этого языка. Я вспоминаю, как я начинал изучать разработку и программирование торговых систем, и, признаться, мне это было довольно сложно, так как это мой первый язык. Но всё оказалось не так сложно, и уже через несколько месяцев я создал довольно сложную программу.
MQL5 Маркету 1 год MQL5 Маркету 1 год
С момента запуска продаж в MQL5 Маркете прошел ровно год. Это был период напряженной работы, которая привела к появлению на рынке крупнейшего магазина торговых роботов и технических индикаторов для платформы MetaTrader 5.
Рецепты MQL5 - Как получить свойства позиции? Рецепты MQL5 - Как получить свойства позиции?
В этой статье мы создадим скрипт, который получает все свойства позиции и показывает их пользователю в диалоговом окне. При запуске скрипта во внешних параметрах можно будет выбрать из выпадающего списка один из двух режимов: показать свойства позиции только на текущем символе или просмотреть свойства позиций на всех символах.
Расчёт интегральных характеристик излучений индикаторов Расчёт интегральных характеристик излучений индикаторов
Излучения индикаторов - это малоизученное направление исследования рынка. В первую очередь из-за трудности анализа, которая вызвана обработкой очень больших массивов изменяющихся во времени данных. Существующий графический анализ слишком ресурсоёмкий и поэтому был разработан экономный алгоритм с использованием таймсерий излучений. В статье предлагается заменить визуальный (интуитивно-образный) анализ исследованием интегральных характеристик излучения. Статья будет интересна как трейдерам, так и разработчикам механических торговых систем.