Отслеживание изменений событий по стране или валюте

Как было сказано в разделе про основные понятия календаря, платформа регистрирует все изменения событий некими внутренними средствами. Каждое состояние характеризуется идентификатором изменений (change_id). Среди функций MQL5 есть две, которые позволяют этот идентификатор выяснить (в произвольный момент времени) и затем запрашивать записи календаря, измененные позднее. Одна из этих функций — CalendarValueLast, о которой речь пойдет в этом разделе, а про вторую — CalendarValueLastByEvent — мы расскажем в следующем разделе.

int CalendarValueLast(ulong &change_id, MqlCalendarValue &values[],
  const string country = NULL, const string currency = NULL)

Функция CalendarValueLast предназначена для двух задач: получения последнего известного идентификатора изменений календаря change_id и заполнения массива values измененными записями с момента предыдущего изменения, заданного переданным идентификатором в том же change_id. Иными словами, параметр change_id работает и как входной, и как выходной. Именно поэтому он является ссылкой и требует указания переменной.

Если подать на вход функции change_id, равный 0, то функция заполнит переменную актуальным идентификатором, но не станет заполнять массив.

Дополнительно с помощью параметров country и currency можно установить фильтрацию записей по стране и валюте.

Функция возвращает количество скопированных элементов календаря. Поскольку в первом режиме работы (change_id = 0), массив не заполняется, возврат 0 не является ошибкой. Мы также можем получить 0, если с момента указанного изменения календарь более не модифицировался. Поэтому для проверки на ошибку следует анализировать _LastError.

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

ulong change = 0;
MqlCalendarValue values[];
while(!IsStopped())
{
   // передаем последний известный нам идентификатор и получаем новый, если он появился
   if(CalendarValueLast(changevalues))
   {
      // анализ добавленных и измененных записей
      ArrayPrint(values);
      ... 
   }
   Sleep(1000);
}

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

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

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

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

Предусмотрим входные переменные для задания имени файла и периода между опросами (в миллисекундах).

input string Filename = "calendar.chn";
input int PeriodMsc = 1000;

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

void OnStart()
{
   ulong change = 0last = 0;
   int count = 0;
   int handle = FileOpen(Filename,
      FILE_WRITE | FILE_READ | FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_BIN);
   if(handle == INVALID_HANDLE)
   {
      PrintFormat("Can't open file '%s' for writing"Filename);
      return;
   }
   
   const ulong p = FileSize(handle);
   if(p > 0)
   {
      PrintFormat("Resuming file %lld bytes"p);
      FileSeek(handle0SEEK_END);
   }
   
   Print("Requesting start ID...");
   ...

Здесь следует сделать небольшое отступление.

При каждом изменении календаря в файл должна записываться как минимум пара целых 8-байтных чисел: текущее время (datetime) и идентификатор новости (ulong), однако одновременно измененных записей может быть и больше одной. Поэтому в первое число помимо даты упаковывается количество измененных записей. Для этого принимается во внимание, что даты умещаются в 0x7FFFFFFFF и, следовательно, старшие 3 байта остаются неиспользуемыми. Именно в два старших байта (по смещению влево на 48 битов) и помещается количество идентификаторов, которые сервис запишет после соответствующей временной метки. Макрос PACK_DATETIME_COUNTER создает "расширенную" дату, а два других — DATETIME и COUNTER — востребованы впоследствии при чтении архива изменений (другой программой).

#define PACK_DATETIME_COUNTER(D,C) (D | (((ulong)(C)) << 48))
#define DATETIME(A) ((datetime)((A) & 0x7FFFFFFFF))
#define COUNTER(A)  ((ushort)((A) >> 48)) 

Теперь вернемся к основному коду сервиса. В цикле, который "просыпается" каждые заданные PeriodMsc миллисекунд мы запрашиваем изменения с помощью CalendarValueLast. Если изменения есть, записываем текущее серверное время и массив полученных идентификаторов в файл.

   while(!IsStopped())
   {
      if(!TerminalInfoInteger(TERMINAL_CONNECTED))
      {
         Print("Waiting for connection...");
         Sleep(PeriodMsc);
         continue;
      }
      
      MqlCalendarValue values[];
      const int n = CalendarValueLast(changevalues);
      if(n > 0)
      {
         string records = "[" + Description(values[0]);
         for(int i = 1i < n; ++i)
         {
            records += "," + Description(values[i]);
         }
         records += "]";
         Print("New change ID: "change" ",
            TimeToString(TimeTradeServer(), TIME_DATE | TIME_SECONDS), "\n"records);
         FileWriteLong(handlePACK_DATETIME_COUNTER(TimeTradeServer(), n));
         for(int i = 0i < n; ++i)
         {
            FileWriteLong(handlevalues[i].id);
         }
         FileFlush(handle);
         ++count;
      }
      else if(_LastError == 0)
      {
         if(!last && change)
         {
            Print("Start change ID obtained: "change);
         }
      }
      
      last = change;
      Sleep(PeriodMsc);
   }
   PrintFormat("%d records added"count);
   FileClose(handle);
}

Для удобного представления информации о каждой новости написана вспомогательная функция Description.

string Description(const MqlCalendarValue &value)
{
   MqlCalendarEvent event;
   MqlCalendarCountry country;
   CalendarEventById(value.event_idevent);
   CalendarCountryById(event.country_idcountry);
   return StringFormat("%lld (%s/%s @ %s)",
      value.idcountry.codeevent.nameTimeToString(value.time));
}

Таким образом, в журнале будет выведен не только идентификатор, но код страны, название и плановое время новости.

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

Посмотрим на сервис в действии. В следующем фрагменте журнала (на периоде 2022.06.28 15:30 — 16:00) обратите внимание, что некоторые новости относятся к отдаленному будущему (в них проставляются значения поля prev_value, которое является по совместительству полем actual_value одноименного текущего события). Однако более важно другое: реальное время выхода новостей может существенно, порой на несколько минут, отличаться от планового.

Requesting start ID...
Start change ID obtained: 86358784
New change ID: 86359040 2022.06.28 15:30:42
[155955 (US/Wholesale Inventories m/m @ 2022.06.28 15:30)]
New change ID: 86359296 2022.06.28 15:30:45
[155956 (US/Wholesale Inventories m/m @ 2022.07.08 17:00)]
New change ID: 86359552 2022.06.28 15:30:48
[156117 (US/Goods Trade Balance @ 2022.06.28 15:30)]
New change ID: 86359808 2022.06.28 15:30:51
[156118 (US/Goods Trade Balance @ 2022.07.27 15:30)]
New change ID: 86360064 2022.06.28 15:30:54
[156231 (US/Retail Inventories m/m @ 2022.06.28 15:30)]
New change ID: 86360320 2022.06.28 15:30:57
[156232 (US/Retail Inventories m/m @ 2022.07.15 17:00)]
New change ID: 86360576 2022.06.28 15:31:00
[156255 (US/Retail Inventories excl. Autos m/m @ 2022.06.28 15:30)]
New change ID: 86360832 2022.06.28 15:31:03
[156256 (US/Retail Inventories excl. Autos m/m @ 2022.07.15 17:00)]
New change ID: 86361088 2022.06.28 15:31:07
[155956 (US/Wholesale Inventories m/m @ 2022.07.08 17:00)]
New change ID: 86361344 2022.06.28 15:31:10
[156118 (US/Goods Trade Balance @ 2022.07.27 15:30)]
New change ID: 86361600 2022.06.28 15:31:13
[156232 (US/Retail Inventories m/m @ 2022.07.15 17:00)]
New change ID: 86362368 2022.06.28 15:36:47
[158534 (US/Challenger Job Cuts y/y @ 2022.07.07 14:30)]
New change ID: 86362624 2022.06.28 15:51:23
...
New change ID: 86364160 2022.06.28 16:01:39
[154531 (US/HPI m/m @ 2022.06.28 16:00)]
New change ID: 86364416 2022.06.28 16:01:42
[154532 (US/HPI m/m @ 2022.07.26 16:00)]
New change ID: 86364672 2022.06.28 16:01:46
[154543 (US/HPI y/y @ 2022.06.28 16:00)]
New change ID: 86364928 2022.06.28 16:01:49
[154544 (US/HPI y/y @ 2022.07.26 16:00)]
New change ID: 86365184 2022.06.28 16:01:54
[154561 (US/HPI @ 2022.06.28 16:00)]
New change ID: 86365440 2022.06.28 16:01:58
[154571 (US/HPI @ 2022.07.26 16:00)]
New change ID: 86365696 2022.06.28 16:02:01
[154532 (US/HPI m/m @ 2022.07.26 16:00)]
New change ID: 86365952 2022.06.28 16:02:05
[154544 (US/HPI y/y @ 2022.07.26 16:00)]
New change ID: 86366208 2022.06.28 16:02:09
[154571 (US/HPI @ 2022.07.26 16:00)]

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

Для целей демонстрации подготовлен скрипт CalendarChangeReader.mq5. На практике приведенный исходный код должен размещаться в эксперте.

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

input string Filename = "calendar2.chn";
input datetime Start;

Для хранения информации об отдельной правке описана структура ChangeState.

struct ChangeState
{
   datetime dt;
   ulong ids[];
   
   ChangeState(): dt(LONG_MAX) {}
   ChangeState(const datetime atulong &_ids[])
   {
      dt = at;
      ArraySwap(ids_ids);
   }
   
   void operator=(const ChangeState &other)
   {
      dt = other.dt;
      ArrayCopy(idsother.ids);
   }
};

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

Дескриптор файла передается как параметр в конструктор, также как и начальное время теста. Чтение файла и заполнение структуры ChangeState для одной правки календаря выполняется в методе readState.

class ChangeFileReader
{
   const int handle;
   ChangeState current;
   const ChangeState zero;
   
public:
   ChangeFileReader(const int hconst datetime start = 0): handle(h)
   {
      if(readState())
      {
         if(start)
         {
            ulong dummy[];
            check(startdummytrue); // находим первую правку после start 
         }
      }
   }
   
   bool readState()
   {
      if(FileIsEnding(handle)) return false;
      ResetLastError();
      const ulong v = FileReadLong(handle);
      current.dt = DATETIME(v);
      ArrayFree(current.ids);
      const int n = COUNTER(v);
      for(int i = 0i < n; ++i)
      {
         PUSH(current.idsFileReadLong(handle));
      }
      return _LastError == 0;
   }
   ...

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

   bool check(datetime nowulong &records[], const bool fastforward = false)
   {
      if(current.dt > nowreturn false;
      
      ArrayFree(records);
      
      if(!fastforward)
      {
         ArrayCopy(recordscurrent.ids);
         current = zero;
      }
      
      while(readState() && current.dt <= now)
      {
         if(!fastforwardArrayInsert(recordscurrent.idsArraySize(records));
      }
      
      return true;
   }
};

Вот как класс используется в OnStart.

void OnStart()
{
   const long day = 60 * 60 * 24;
   datetime now = Start ? Start : (datetime)(TimeCurrent() / day * day);
   
   int handle = FileOpen(Filename,
      FILE_READ | FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_BIN);
   if(handle == INVALID_HANDLE)
   {
      PrintFormat("Can't open file '%s' for reading"Filename);
      return;
   }
   
   ChangeFileReader reader(handlenow);
   
   // читаем шаг за шагом, время now увеличиваем искусственно в этом демо
   while(!FileIsEnding(handle))
   {
      // в реальном приложении вызов reader.check можно делать на каждом тике
      ulong records[];
      if(reader.check(nowrecords))
      {
         Print(now);          // выводим время
         ArrayPrint(records); // массив идентификаторов изменившихся новостей
      }
      now += 60// прибавляем по 1 минуте за раз, можно посекундно
   }
   
   FileClose(handle);
}

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

2022.06.28 15:31:00
155955 155956 156117 156118 156231 156232 156255
2022.06.28 15:32:00
156256 155956 156118 156232
2022.06.28 15:37:00
158534
...
2022.06.28 16:02:00
154531 154532 154543 154544 154561 154571
2022.06.28 16:03:00
154532 154544 154571

Те же самые идентификаторы воспроизводятся в виртуальном времени с тем же запаздыванием, что и онлайн, правда, здесь видно округление до 1 минуты, которое получилось, потому что мы задали такой искусственный шаг в цикле. По идее, из соображений эффективности мы можем откладывать проверки вплоть до времени, хранящемся в структуре ChangeState current. В прилагаемом исходном коде определен метод getState для получения этого времени.