Расписания торговых и котировочных сессий

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

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

Как известно, существует 2 основных типа сессий: котировочная и торговая. Во время котировочной сессии в терминал поступают (могут поступать) свежие котировки. Во время торговой сессии разрешено отправлять торговые приказы и совершать сделки. В течение суток может быть несколько сессий каждого типа, с перерывами (например, утренняя и вечерняя). Очевидно, что длительность котировочных сессий больше или равна торговым.

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

MQL5 API позволяет узнать котировочные и торговые сессии каждого инструмента с помощью функций SymbolInfoSessionQuote и SymbolInfoSessionTrade. Эта важная информация позволяет, в частности, проверять в программе, открыт ли рынок в данный момент, прежде чем отправлять на сервер торговый приказ. Таким образом, мы предотвращаем получение неизбежного ошибочного результата и не нагружаем сервер зря. Следует учитывать, что в случае массированных ошибочных запросов на сервер из-за некорректно реализованной MQL-программы, сервер может начать "игнорировать" ваш терминал, отказываясь некоторое время выполнять последующие команды (даже корректные).

bool SymbolInfoSessionQuote(const string symbol, ENUM_DAY_OF_WEEK dayOfWeek, uint sessionIndex, datetime &from, datetime &to)

bool SymbolInfoSessionTrade(const string symbol, ENUM_DAY_OF_WEEK dayOfWeek, uint sessionIndex, datetime &from, datetime &to)

Функции работают по одинаковому принципу. Для заданного символа symbol и дня недели dayOfWeek они заполняют переданные по ссылке параметры from и to временем открытия и закрытия сессии под номером sessionIndex. Индексация сессий начинается с 0. Структура ENUM_DAY_OF_WEEK была описана в разделе Перечисления.

Для запроса количества сессий отдельных функций не существует: вместо этого следует вызывать SymbolInfoSessionQuote и SymbolInfoSessionTrade с увеличивающимся индексом sessionIndex, пока функция не вернет признак ошибки (false). Когда сессия с указанным номером существует, и выходные аргументы from и to получили корректные значения, функции возвращают признак успеха (true).

Согласно документации MQL5, в полученных значениях from и to типа datetime следует игнорировать дату и учитывать только время. Это связано с тем, что информация представляет собой внутридневное расписание. Однако в этом правиле есть важное исключение.

Поскольку рынок потенциально открыт 24 часа в сутки, как в случае Forex, или биржа на другом конце света, где дневные рабочие часы совпадают со сменой дат в "таймзоне" вашего брокера, окончание сессий может иметь время, равное или превышающее 24 часа. Например, если начало сессий Forex равно 00:00, то окончание — 24:00. Вместе с тем, с точки зрения типа datetime, 24 часа — это 00 часов 00 минут уже на следующий день.

Более запутанной ситуация становится для бирж, расписание которых сдвинуто относительно временной зоны вашего брокера на несколько часов таким образом, что начало сессии приходится на один сутки, а окончание — на другие. Из-за этого в переменную to записывается не только время, но и лишние сутки, которые нельзя игнорировать, поскольку иначе внутридневное время from окажется больше внутридневного времени to (например, сессия может длиться с 21:00 сегодня до 8:00 завтра, то есть 21 > 8). При этом записанная естественным образом проверка на вхождение текущего времени внутрь сессии ("время икс больше начала и меньше окончания") окажется некорректной (например, условие x >= 21 && x < 8 не выполняется при x = 23, хотя сессия на самом деле активна).

Таким образом, мы приходим к выводу, что игнорировать дату в параметрах from/to нельзя, и этот нюанс следует учитывать в алгоритмах (см. пример).

Для демонстрации возможностей функций вернемся к примеру скрипта EnvPermissions.mq5, который был представлен в разделе Разрешения. Один из типов разрешений (или ограничений, если угодно) относится как раз к доступности торговли. Ранее в скрипте были учтены настройки терминала (TERMINAL_TRADE_ALLOWED) и настройки конкретной MQL-программы (MQL_TRADE_ALLOWED). Теперь мы можем добавить к ним проверку сессий, чтобы определить торговые разрешения, действующие в определенный момент для конкретного символа.

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

Напомним, что в скрипте реализован класс Permissions, который предоставляет централизованное описание всех типов разрешений/ограничений, применимых к MQL-программам. Среди прочего в классе имеются методы для проверки доступности торговли: isTradeEnabled и isTradeOnSymbolEnabled. Первый из них относится к глобальным разрешениям и останется практически без изменений:

class Permissions
{
public:
   static bool isTradeEnabled(const string symbol = NULLconst datetime now = 0)
   {
      return TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)
          && MQLInfoInteger(MQL_TRADE_ALLOWED)
          && isTradeOnSymbolEnabled(symbol == NULL ? _Symbol : symbolnow);
   }
   ...

После проверок свойств терминала и MQL-программы идет обращение к isTradeOnSymbolEnabled, где и производится анализ спецификации символа. Раньше этот метод был практически пустым.

Кроме рабочего символа, передаваемого в параметре symbol, функция isTradeOnSymbolEnabled принимает текущее время (now) и требуемый режим торгов (mode). О последнем мы подробнее поговорим в следующих разделах (см. Разрешения на торговлю). Сейчас лишь отметим, что значение по умолчанию SYMBOL_TRADE_MODE_FULL дает максимальную свободу (разрешены все торговые операции).

   static bool isTradeOnSymbolEnabled(string symbolconst datetime now = 0,
      const ENUM_SYMBOL_TRADE_MODE mode = SYMBOL_TRADE_MODE_FULL)
   {
      // проверка сессий
      bool found = now == 0;
      if(!found)
      {
         const static ulong day = 60 * 60 * 24;
         const ulong time = (ulong)now % day;
         datetime fromto;
         int i = 0;
         
         ENUM_DAY_OF_WEEK d = TimeDayOfWeek(now);
         
         while(!found && SymbolInfoSessionTrade(symboldi++, fromto))
         {
            found = time >= (ulong)from && time < (ulong)to;
         }
      }
      // проверка режима торговли для символа
      return found && (SymbolInfoInteger(symbolSYMBOL_TRADE_MODE) == mode);
   }

Если время now не указано (равно 0 по умолчанию), мы считаем, что сессии нас не интересуют. Это выражается в том, что переменная found с признаком того, что была найдена подходящая сессия (то есть, содержащая заданное время), сразу же устанавливается в true. Если же параметр now задан, функция попадает в блок анализа торговых сессий.

Для выделения времени без учета даты из значений типа datetime мы описываем константу day, равную количеству секунд в сутках. Выражение вроде now % day вернет остаток от деления полной даты и времени на длительность одного дня, что даст только время (старшие разряды в datetime будут нулевыми).

Функция TimeDayOfWeek возвращает день недели для переданного значения datetime и находится в заголовочном файле MQL5Book/DateTime.mqh, который мы уже использовали ранее (см. Дата и время).

Далее в цикле while вызываем функцию SymbolInfoSessionTrade, постоянно инкрементируя индекс сессии i, пока не будет найдена подходящая сессия или функция не вернёт false (сессий больше нет). Таким образом программа может получить полный перечень сессий по дням недели, аналогичный тому, что выводится в терминале в окне Спецификации символа.

Очевидно, что подходящей сессией является та, для которой время начала from и окончания to содержат заданную величину time между собой. Именно здесь мы учитываем нюанс, связанный с возможной круглосуточной торговлей: from и to сравниваются с time "как есть", без отбрасывания суток (from % day или to % day).

Как только found станет равен true, выходим из цикла. В противном случае цикл закончится по превышению допустимого количества сессий (функция SymbolInfoSessionTrade вернет false), и подходящая сессия так и не будет найдена.

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

Приведенный выше код имеет некоторые упрощения по сравнению с окончательным вариантом в файле SymbolPermissions.mq5. В нем дополнительно реализован механизм для маркировки источника ограничения, вызвавшего отключение торговли. Все такие источники сведены в перечисление TRADE_RESTRICTIONS.

   enum TRADE_RESTRICTIONS
   {
      TERMINAL_RESTRICTION = 1,
      PROGRAM_RESTRICTION = 2,
      SYMBOL_RESTRICTION = 4,
      SESSION_RESTRICTION = 8,
   };

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

Для регистрации факта обнаружения ограничения в классе Permissions заведена переменная lastFailReasonBitMask, позволяющая собирать в себе битовую маску из элементов перечисления с помощью вспомогательного метода pass (бит взводится, когда проверяемое условие value ложно, равно false).

   static uint lastFailReasonBitMask;
   static bool pass(const bool valueconst uint bitflag
   {
      if(!valuelastFailReasonBitMask |= bitflag;
      return value;
   }

Вызов метода pass с конкретным флагом делается на соответствующих этапах проверки. Например, метод isTradeEnabled в полном виде выглядит так:

   static bool isTradeEnabled(const string symbol = NULLconst datetime now = 0)
   {
      lastFailReasonBitMask = 0;
      return pass(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED), TERMINAL_RESTRICTION)
          && pass(MQLInfoInteger(MQL_TRADE_ALLOWED), PROGRAM_RESTRICTION)
          && isTradeOnSymbolEnabled(symbol == NULL ? _Symbol : symbolnow);
   }

За счет этого при отрицательном результате вызова TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) или MQLInfoInteger(MQL_TRADE_ALLOWED) будут взведены флаги TERMINAL_RESTRICTION или PROGRAM_RESTRICTION, соответственно.

Метод isTradeOnSymbolEnabled также устанавливает свои флаги при обнаружении проблем, включая сессионные.

   static bool isTradeOnSymbolEnabled(string symbolconst datetime now = 0,
      const ENUM_SYMBOL_TRADE_MODE mode = SYMBOL_TRADE_MODE_FULL)
   {
      ...
      return pass(foundSESSION_RESTRICTION)
         && pass(SymbolInfoInteger(symbolSYMBOL_TRADE_MODE) == modeSYMBOL_RESTRICTION);
   }

В результате MQL-программа, использующая запрос Permissions::isTradeEnabled, может после получения запрета уточнить его суть с помощью методов getFailReasonBitMask и explainBitMask: первый возвращает маску взведенных флагов запретов "как есть", а второй формирует понятное для пользователя текстовое описание ограничений.

   static uint getFailReasonBitMask()
   {
      return lastFailReasonBitMask;
   }
   
   static string explainBitMask()
   {
      string result = "";
      for(int i = 0i < 4; ++i)
      {
         if(((1 << i) & lastFailReasonBitMask) != 0)
         {
            result += EnumToString((TRADE_RESTRICTIONS)(1 << i));
         }
      }
      return result;
   }

С помощью вышеописанного класса Permissions в обработчике OnStart делается проверка доступности торговли для всех символов Обзора рынка (в текущий момент, TimeCurrent).

void OnStart()
{
   string disabled = "";
   
   const int n = SymbolsTotal(true);
   for(int i = 0i < n; ++i)
   {
      const string s = SymbolName(itrue);
      if(!Permissions::isTradeEnabled(sTimeCurrent()))
      {
         disabled += s + "=" + Permissions::explainBitMask() +"\n";
      }
   }
   if(disabled != "")
   {
      Print("Trade is disabled for following symbols and origins:");
      Print(disabled);
   }
}

Если для какого-то символа торговля запрещена, мы увидим в журнале пояснение.

   Trade is disabled for following symbols and origins:
   USDRUB=SESSION_RESTRICTION
   SP500m=SYMBOL_RESTRICTION

В данном случае рынок закрыт для "USDRUB", а для символа "SP500m" торговля отключена (более строго, не соответствует режиму SYMBOL_TRADE_MODE_FULL).

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