Фильтрация событий по множеству условий

Как мы знаем из предыдущих разделов этой главы, MQL5 API позволяет запрашивать события календаря по нескольким условиям:

  • странам (CalendarValueHistory, CalendarValueLast);
  • валютам (CalendarValueHistory, CalendarValueLast);
  • идентификаторам видов событий (CalendarValueHistoryByEvent, CalendarValueLastByEvent);
  • временному диапазону (CalendarValueHistory, CalendarValueHistoryByEvent);
  • изменениям с момента предыдущего опроса календаря (CalendarValueLast, CalendarValueLastByEvent);
  • конкретную новость по идентификатору (CalendarValueById).

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

Условия

Временной диапазон

Последние изменения

Страны

CalendarValueHistory

CalendarValueLast

Валюты

CalendarValueHistory

CalendarValueLast

События

CalendarValueHistoryByEvent

CalendarValueLastByEvent

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

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

Для решения данных задач создан класс CalendarFilter (CalendarFilter.mqh).

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

Это вызвано тем, что класс фильтра будет впоследствии расширяться возможностями кэширования новостей для их чтения из тестера, и начальные условия конструктора фактически определяют контекст кэширования, в пределах которого и возможна последующая фильтрации. Например, если мы при создании объекта укажем код страны "EU", то очевидно, бессмысленно запрашивать через него новости по США или Бразилии. Аналогично с диапазоном дат: его указание в конструкторе сделает невозможным получение новостей вне диапазона.

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

Кроме того, поскольку страны и валюты имеют сейчас почти однозначное отображение (за исключением Европейского союза и EUR), их передача в конструктор осуществляется через единственный параметр context: если в нем указать строку длиной 2 символа — подразумевается код страны (или объединения стран), а если длина равна 3-м символам — подразумевается код валюты. Для кодов "EU" и "EUR", еврозона является подмножеством "EU" (в рамках стран с официальными договорами). В особых случаях, когда интерес представляют страны Евросоюза вне зоны евро, их также можно описать контекстом "EU". При необходимости, более узкие условия на новости по валютам этих стран (BGN, HUF, DKK, ISK, PLN, RON, HRK, CZK, SEK) можно добавить в фильтр динамически, с помощью методов, которые мы представим позднее. Однако ввиду экзотичности, нет гарантий, что такие новости попадут в календарь.

Приступим к изучению класса.

class CalendarFilter
{
protected:
   // начальные (необязательные) условия, задаваемые в конструкторе, инварианты
   string context;    // страна или валюта
   datetime fromto// диапазон дат
   bool fixedDates;   // если 'from'/'to' переданы в конструкторе - их нельзя менять
   
   // выделенные селекторы (страны/валюты/идентификаторы видов событий)
   string country[], currency[];
   ulong ids[];
   
   MqlCalendarValue values[]; // отфильтрованные результаты
   
   virtual void init()
   {
      fixedDates = from != 0 || to != 0;
      if(StringLen(context) == 3)
      {
         PUSH(currencycontext);
      }
      else
      {
         // даже если context равен NULL, берем его для опроса всей базы календаря
         PUSH(countrycontext);
      }
   }
   ...
public:
   CalendarFilter(const string _context = NULL,
      const datetime _from = 0const datetime _to = 0):
      context(_context), from(_from), to(_to)
   {
      init();
   }
   ...

Под страны и валюты выделены два массива country и currency. Если они не заполнены из context-а во время создания объекта, то затем MQL-программа получит возможность добавить условия на несколько стран или валют, чтобы осуществить по ним комбинированный запрос новостей.

Для хранения условий на все прочие атрибуты новостей в объекте CalendarFilter описан массив selectors, с размером 3 по второму измерению. Можно сказать, что это некая таблица, в которой каждая строка имеет 3 колонки.

   long selectors[][3];   // [0] - свойство, [1] - значение, [2] - условие

По 0-му индексу будут располагаться идентификаторы свойств новостей. Поскольку атрибуты разнесены по трем таблицам базы (MqlCalendarCountry, MqlCalendarEvent, MqlCalendarValue) они описаны с помощью элементов обобщенного перечисления ENUM_CALENDAR_PROPERTY (CalendarDefines.mqh).

enum ENUM_CALENDAR_PROPERTY
{                                      // +/- означает поддержку фильтрации по полю
   CALENDAR_PROPERTY_COUNTRY_ID,       // -ulong
   CALENDAR_PROPERTY_COUNTRY_NAME,     // -string
   CALENDAR_PROPERTY_COUNTRY_CODE,     // +string (2 символа)
   CALENDAR_PROPERTY_COUNTRY_CURRENCY// +string (3 символа)
   CALENDAR_PROPERTY_COUNTRY_GLYPH,    // -string (1 символ)
   CALENDAR_PROPERTY_COUNTRY_URL,      // -string
   
   CALENDAR_PROPERTY_EVENT_ID,         // +ulong (ID вида события)
   CALENDAR_PROPERTY_EVENT_TYPE,       // +ENUM_CALENDAR_EVENT_TYPE
   CALENDAR_PROPERTY_EVENT_SECTOR,     // +ENUM_CALENDAR_EVENT_SECTOR
   CALENDAR_PROPERTY_EVENT_FREQUENCY,  // +ENUM_CALENDAR_EVENT_FREQUENCY
   CALENDAR_PROPERTY_EVENT_TIMEMODE,   // +ENUM_CALENDAR_EVENT_TIMEMODE
   CALENDAR_PROPERTY_EVENT_UNIT,       // +ENUM_CALENDAR_EVENT_UNIT
   CALENDAR_PROPERTY_EVENT_IMPORTANCE// +ENUM_CALENDAR_EVENT_IMPORTANCE
   CALENDAR_PROPERTY_EVENT_MULTIPLIER// +ENUM_CALENDAR_EVENT_MULTIPLIER
   CALENDAR_PROPERTY_EVENT_DIGITS,     // -uint
   CALENDAR_PROPERTY_EVENT_SOURCE,     // +string ("http[s]://")
   CALENDAR_PROPERTY_EVENT_CODE,       // -string
   CALENDAR_PROPERTY_EVENT_NAME,       // +string (4+ символов или с символом подстановки '*')
   
   CALENDAR_PROPERTY_RECORD_ID,        // -ulong
   CALENDAR_PROPERTY_RECORD_TIME,      // +datetime
   CALENDAR_PROPERTY_RECORD_PERIOD,    // +datetime (как long)
   CALENDAR_PROPERTY_RECORD_REVISION,  // +int
   CALENDAR_PROPERTY_RECORD_ACTUAL,    // +long
   CALENDAR_PROPERTY_RECORD_PREVIOUS,  // +long
   CALENDAR_PROPERTY_RECORD_REVISED,   // +long
   CALENDAR_PROPERTY_RECORD_FORECAST,  // +long
   CALENDAR_PROPERTY_RECORD_IMPACT,    // +ENUM_CALENDAR_EVENT_IMPACT
   
   CALENDAR_PROPERTY_RECORD_PREVISED,  // +нестандарное (previous или revised если есть)
   
   CALENDAR_PROPERTY_CHANGE_ID,        // -ulong (зарезервировано)
};

По индексу 1 будут храниться значения, для сравнения с ними в условиях отбора новостных записей. Например, если потребуется установить фильтр по сектору экономики, то в selectors[i][0] запишем CALENDAR_PROPERTY_EVENT_SECTOR, а в selectors[i][1] — одно из значений стандартного перечисления ENUM_CALENDAR_EVENT_SECTOR.

Наконец, последняя колонка (под 2-м индексом) зарезервирована для операции сравнения значения селектора со значением атрибута в новости: все поддерживаемые операции сведены в перечисление IS. Напомним его.

enum IS
{
   EQUAL,
   NOT_EQUAL,
   GREATER,
   LESS,
   OR_EQUAL,
   ...
};

Похожий подход нам уже встречался в TradeFilter.mqh. Таким образом, мы сможем компоновать условия не только на равенство значений, но и на неравенство или больше/меньше. Например, легко представить фильтр на поле CALENDAR_PROPERTY_EVENT_IMPORTANCE, которое должно быть больше (GREATER), чем CALENDAR_IMPORTANCE_LOW (это — элемент стандартного перечисления ENUM_CALENDAR_EVENT_IMPORTANCE), что означает выборку новостей средней и высокой важности.

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

#define DAY_LONG     (60 * 60 * 24)
#define WEEK_LONG    (DAY_LONG * 7)
#define MONTH_LONG   (DAY_LONG * 30)
#define QUARTER_LONG (MONTH_LONG * 3)
#define YEAR_LONG    (MONTH_LONG * 12)
   
enum ENUM_CALENDAR_SCOPE
{
   SCOPE_DAY = DAY_LONG,         // Day
   SCOPE_WEEK = WEEK_LONG,       // Week
   SCOPE_MONTH = MONTH_LONG,     // Month
   SCOPE_QUARTER = QUARTER_LONG// Quarter
   SCOPE_YEAR = YEAR_LONG,       // Year
};

Все перечисления вынесены в отдельный заголовочный файл CalendarDefines.mqh.

Но вернемся к классу CalendarFilter. Тип массива selectors в нем — long, что подходит для хранения значений почти всех задействованных типов: перечислений, даты и времени, идентификаторов, целых чисел и даже значений экономический показателей, потому что они хранятся в календаре в виде long-чисел (в миллионных долях от вещественных величин). Однако что делать со строковыми свойствами?

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

class CalendarFilter
{
protected:
   ...
   string stringCache[];  // кэш всех строк в 'selectors'
   ...

Тогда вместо значения строки в selectors[i][1] легко сохранить индекс элемента в массиве stringCache. Сейчас мы покажем это в деталях.

Для заполнения массива selectors условиями фильтров предусмотрено несколько let-методов, в частности, для перечислений:

class CalendarFilter
{
...
public:
   // здесь обрабатываются все поля типов перечислений
   template<typename E>
   CalendarFilter *let(const E econst IS c = EQUAL)
   {
      const int n = EXPAND(selectors);
      selectors[n][0] = resolve(e); // по типу E вернуть элемент ENUM_CALENDAR_PROPERTY
      selectors[n][1] = e;
      selectors[n][2] = c;
      return &this;
   }
   ...

Для фактических значений показателей:

   // здесь обрабатываются поля:
   // CALENDAR_PROPERTY_RECORD_ACTUAL, CALENDAR_PROPERTY_RECORD_PREVIOUS,
   // CALENDAR_PROPERTY_RECORD_REVISED, CALENDAR_PROPERTY_RECORD_FORECAST,
   // а также CALENDAR_PROPERTY_RECORD_PERIOD (как long)
   CalendarFilter *let(const long valueconst ENUM_CALENDAR_PROPERTY propertyconst IS c = EQUAL)
   {
      const int n = EXPAND(selectors);
      selectors[n][0] = property;
      selectors[n][1] = value;
      selectors[n][2] = c;
      return &this;
   }
   ...

И для строк:

   // здесь условия на все строковые свойства (с сокращениями)
   CalendarFilter *let(const string findconst IS c = EQUAL)
   {
      const int wildcard = (StringFind(find"*") + 1) * 10;
      switch(StringLen(find) + wildcard)
      {
      case 2:
         // если начальный контекст отличен от страны, мы можем дополнить его страной,
         // иначе фильтр игнорируется 
         if(StringLen(context) != 2)
         {
            if(ArraySize(country) == 1 && StringLen(country[0]) == 0)
            {
               country[0] = find// сужение "всех стран" до одной (можно добавить еще)
            }
            else
            {
               PUSH(countryfind);
            }
         }
         break;
      case 3:
         // фильтр на валюту можем задать, только если в начальном контексте её не было
         if(StringLen(context) != 3)
         {
            PUSH(currencyfind);
         }
         break;
      default:
         {
            const int n = EXPAND(selectors);
            PUSH(stringCachefind);
            if(StringFind(find"http://") == 0 || StringFind(find"https://") == 0)
            {
               selectors[n][0] = CALENDAR_PROPERTY_EVENT_SOURCE;
            }
            else
            {
               selectors[n][0] = CALENDAR_PROPERTY_EVENT_NAME;
            }
            selectors[n][1] = ArraySize(stringCache) - 1;
            selectors[n][2] = c;
            break;
         }
      }
      
      return &this;
   }

В перегрузке метода для строк обратите внимание, что строки длиной 2 и 3 символа (если они без шаблонной звездочки '*', которая является заменой для произвольной последовательности знаков) попадают в массивы стран и символов соответственно, а все остальные строки — трактуются как фрагменты названия или источника новостей, и оба эти поля задействуют stringCache и selectors.

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

protected:
   ulong ids[];           // фильтруемые виды событий
   ...
public:
   CalendarFilter *let(const ulong event)
   {
      PUSH(idsevent);
      return &this;
   }
   ...

Таким образом, в число приоритетных фильтров (которые обрабатываются вне массива selectors), входят не только страны, валюты, диапазон дат, но и идентификаторы видов событий. Такое конструктивное решение вызвано тем, что именно эти параметры могут передаваться в те или иные API-функции календаря как входные. Все остальные атрибуты новостей мы получаем как выходные значения полей в массивах структур (MqlCalendarValue, MqlCalendarEvent, MqlCalendarCountry). Именно по ним мы будет выполнять дополнительную фильтрацию, согласно правилами в массиве selectors.

Все let-методы возвращают указатель на объект, что позволяет нанизывать их вызовы в цепочку. Например, так:

CalendarFilter f;
f.let(CALENDAR_IMPORTANCE_LOWGREATER// важные и умеренно важные новости
  .let(CALENDAR_TIMEMODE_DATETIME// только события с точным временем
  .let("DE").let("FR"// парочка стран, или, на выбор...
  .let("USD").let("GBP"// ...парочка валют (но оба условия сразу не сработают)
  .let(TimeCurrent() - MONTH_LONGTimeCurrent() + WEEK_LONG// диапазон дат "вокруг" текущего времени
  .let(LONG_MINCALENDAR_PROPERTY_RECORD_FORECASTNOT_EQUAL// есть прогноз
  .let("farm"); // полнотекстовой поиск по названиям новостей

Условия по странам и валютам можно, в принципе, комбинировать, но следует иметь в виду, что несколько значений можно задать только либо для стран, либо для валют, но не для того и другого. Один из этих двух аспектов контекста (любой из двух) в текущей реализации поддерживает только одно или ни одного значения (т.е. отсутствие фильтра по нему). Например, при выбранной валюте "EUR" можно сузить контекст поиска новостей только по Германии и Франции (коды стран "DE" и "FR") — в результате будут отброшены новости ЕЦБ и евростата, а также, в частности, по Италии и Испании. Однако указание "EUR" в данном случае является избыточным, так как в Германии и Франции нет других валют.

Поскольку класс использует встроенные функции, в которых параметры country и currency применяются к новостям с помощью операции логического И, проверяйте непротиворечивость условий фильтрации.

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

public:
   bool select(MqlCalendarValue &result[])
   {
      int count = 0;
      ArrayFree(result);
      if(ArraySize(ids)) // идентификаторы видов событий
      {
         for(int i = 0i < ArraySize(ids); ++i)
         {
            MqlCalendarValue temp[];
            if(PRTF(CalendarValueHistoryByEvent(ids[i], tempfromto)))
            {
               ArrayCopy(resulttempArraySize(result));
               ++count;
            }
         }
      }
      else
      {
         // несколько стран или валют, выбираем за основу то, чего из них больше,
         // а из меньшего массива используется только первый элемент
         if(ArraySize(country) > ArraySize(currency))
         {
            const string c = ArraySize(currency) > 0 ? currency[0] : NULL;
            for(int i = 0i < ArraySize(country); ++i)
            {
               MqlCalendarValue temp[];
               if(PRTF(CalendarValueHistory(tempfromtocountry[i], c)))
               {
                  ArrayCopy(resulttempArraySize(result));
                  ++count;
               }
            }
         }
         else
         {
            const string c = ArraySize(country) > 0 ? country[0] : NULL;
            for(int i = 0i < ArraySize(currency); ++i)
            {
               MqlCalendarValue temp[];
               if(PRTF(CalendarValueHistory(tempfromtoccurrency[i])))
               {
                  ArrayCopy(resulttempArraySize(result));
                  ++count;
               }
            }
         }
      }
      
      if(ArraySize(result) > 0)
      {
         filter(result);
      }
      
      if(count > 1 && ArraySize(result) > 1)
      {
         SORT_STRUCT(MqlCalendarValueresulttime);
      }
      
      return ArraySize(result) > 0;
   }

В зависимости от того, какие из массивов с приоритетными атрибутами заполнены, метод вызывает разные функции API для опроса календаря:

  • если заполнен массив ids, в цикле для всех идентификаторов вызывается CalendarValueHistoryByEvent;
  • если заполнен массив country, и он больше, чем массив валют, выполняется цикл по странам с вызовом CalendarValueHistory;
  • если заполнен массив currency, и он больше или равен размеру массива стран, выполняется цикл по валютам с вызовом CalendarValueHistory;

Каждый вызов функции заполняет временный массив структур MqlCalendarValue temp[], который последовательно аккумулируется в массиве-параметре result. После записи в него всех подходящих новостей по основным условиям (даты, страны, валюты, идентификаторы), если они есть, в дело вступает вспомогательный метод filter, который прореживает массив на основе условий в selectors. В завершении метода select производится сортировка новостей в хронологическом порядке, который может быть нарушен из-за объединения результатов множественных запросов "календарных" функций. Для сортировки используется макрос SORT_STRUCT, рассмотренный в разделе Сравнение, сортировка и поиск в массивах.

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

protected:
   void filter(MqlCalendarValue &result[])
   {
      for(int i = ArraySize(result) - 1i >= 0; --i)
      {
         if(!match(result[i]))
         {
            ArrayRemove(resulti1);
         }
      }
   }
   ...

Наконец, метод match анализирует наш массив selectors и сравнивает его с полями переданной структуры MqlCalendarValue. Здесь код приводится с сокращениями.

 bool match(const MqlCalendarValue &v)
   {
      MqlCalendarEvent event;
      if(!CalendarEventById(v.event_idevent)) return false;
      
      // цикл по всем условиям фильтров, кроме стран, валют, дат, ID,
      // которые были уже ранее использованы при вызовах Calendar-функций
      for(int j = 0j < ArrayRange(selectors0); ++j)
      {
         long field = 0;
         string text = NULL;
         
         // получаем значение поля из новости или её описания
         switch((int)selectors[j][0])
         {
         case CALENDAR_PROPERTY_EVENT_TYPE:
            field = event.type;
            break;
         case CALENDAR_PROPERTY_EVENT_SECTOR:
            field = event.sector;
            break;
         case CALENDAR_PROPERTY_EVENT_TIMEMODE:
            field = event.time_mode;
            break;
         case CALENDAR_PROPERTY_EVENT_IMPORTANCE:
            field = event.importance;
            break;
         case CALENDAR_PROPERTY_EVENT_SOURCE:
            text = event.source_url;
            break;
         case CALENDAR_PROPERTY_EVENT_NAME:
            text = event.name;
            break;
         case CALENDAR_PROPERTY_RECORD_IMPACT:
            field = v.impact_type;
            break;
         case CALENDAR_PROPERTY_RECORD_ACTUAL:
            field = v.actual_value;
            break;
         case CALENDAR_PROPERTY_RECORD_PREVIOUS:
            field = v.prev_value;
            break;
         case CALENDAR_PROPERTY_RECORD_REVISED:
            field = v.revised_prev_value;
            break;
         case CALENDAR_PROPERTY_RECORD_PREVISED// previous или revised (если есть)
            field = v.revised_prev_value != LONG_MIN ? v.revised_prev_value : v.prev_value;
            break;
         case CALENDAR_PROPERTY_RECORD_FORECAST:
            field = v.forecast_value;
            break;
         ...
         }
         
         // сравниваем значение с условием фильтра
         if(text == NULL// числовые поля
         {
            switch((IS)selectors[j][2])
            {
            case EQUAL:
               if(!equal(fieldselectors[j][1])) return false;
               break;
            case NOT_EQUAL:
               if(equal(fieldselectors[j][1])) return false;
               break;
            case GREATER:
               if(!greater(fieldselectors[j][1])) return false;
               break;
            case LESS:
               if(greater(fieldselectors[j][1])) return false;
               break;
            }
         }
         else // строковые поля
         {
            const string find = stringCache[(int)selectors[j][1]];
            switch((IS)selectors[j][2])
            {
            case EQUAL:
               if(!equal(textfind)) return false;
               break;
            case NOT_EQUAL:
               if(equal(textfind)) return false;
               break;
            case GREATER:
               if(!greater(textfind)) return false;
               break;
            case LESS:
               if(greater(textfind)) return false;
               break;
            }
         }
      }
      
      return true;
   }

Методы equal и greater перенесены почти без изменений из наших предыдущих наработок с классами-фильтрами.

На этом задача фильтрации, в целом, решена, то есть MQL-программа может использовать объект CalendarFilter следующим образом:

CalendarFilter f;
f.let()... // серия вызовов метода let для настройки условий фильтрации
MqlCalendarValue records[]; 
if(f.select(records))
{
   ArrayPrint(records);
}

Но на самом деле метод select умеет еще кое-что важное, что мы оставили для факультатива.

Во-первых, в получаемом списке новостей желательно тем или иным образом вставить разделитель (delimiter) между прошлым и будущим, чтобы за него мог зацепиться глаз. В принципе, эта возможность является крайне важной для календарей, но по каким-то причинам отсутствует в пользовательском интерфейсе MetaTrader 5 и на сайте mql5.com. Наша реализация умеет вставлять между прошлым и будущим пустую структуру, которую лишь остается наглядно отобразить (чем мы займемся чуть ниже).

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

Таким образом, полный прототип метода выглядит так:

bool select(MqlCalendarValue &result[],
   const bool delimiter = falseconst int limit = -1);

По умолчанию разделитель не вставляется, и массив не усекается.

Парой абзацев выше была вскользь упомянута дополнительная подзадача фильтрации — визуализация полученного массива. В классе CalendarFilter имеется специальный метод format, который превращает передаваемый массив структур MqlCalendarValue &data[] в массив удобочитаемых строк string &result[]. С телом метода можно ознакомиться в прилагаемом файле CalendarFilter.mqh.

bool format(const MqlCalendarValue &data[],
   const ENUM_CALENDAR_PROPERTY &props[], string &result[],
   const bool padding = falseconst bool header = false);

Какие именно поля из MqlCalendarValue мы хотим отобразить, задается в массиве props. Напомним, что в перечислении ENUM_CALENDAR_PROPERTY имеются поля из всех трех зависимых структур календаря, так что MQL-программа может автоматически выводить не только экономические показатели из конкретной записи о событии, но и его название, характеристики, код страны или валюты — это все берет на себя метод format.

Каждая строка в выходном массиве result содержит текстовое представление значения одного из полей (число, описание, элемент перечисления). Размер массива result равен произведению количества структур на входе (в data) на количество отображаемых полей (в props). Опциональный параметр header позволяет добавить в начало выходного массива ряд с названиями полей (колонок). Параметр padding управляет генерацией дополнительных пробелов в тексте, чтобы таблицу было удобно выводить моноширинным шрифтом (например, в журнал).

В завершение описания класса CalendarFilter упомянем еще один важный публичный метод — update.

bool update(MqlCalendarValue &result[]);

Он по своей структуре почти полностью повторяет select, но вместо вызовов функций CalendarValueHistoryByEvent и CalendarValueHistory использует CalendarValueLastByEvent и CalendarValueLast. Назначение метода очевидно: он запрашивает календарь на наличие свежих изменений, соответствующих условиям фильтрации. Но для своей работы он требует идентификатор изменений. И такое поле действительно определено в классе: в первый раз оно заполняется внутри метода select.

class CalendarFilter
{
protected:
   ...
   ulong change;
   ...
public:
   bool select(MqlCalendarValue &result[],
      const bool delimiter = falseconst int limit = -1)
   {
      ...
      change = 0;
      MqlCalendarValue dummy[];
      СalendarValueLast(changedummy);
      ...
   }

Кое-какие нюансы класса CalendarFilter все еще "остались за кадром", но к некоторым из них мы обратимся в следующих разделах.

Давайте проверим фильтр в деле: сначала в простом скрипте CalendarFilterPrint.mq5, а затем в более практичном индикаторе CalendarMonitor.mq5.

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

input string Context// Context (страна - 2 знака, currency - 3 знака, пусто - без фильтра)
input ENUM_CALENDAR_SCOPE Scope = SCOPE_MONTH;
input string Text = "farm";
input int Limit = -1;

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

CalendarFilter f(ContextTimeCurrent() - ScopeTimeCurrent() + Scope);

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

void OnStart()
{
   f.let(CALENDAR_IMPORTANCE_LOWGREATER)
      .let(LONG_MINCALENDAR_PROPERTY_RECORD_FORECASTNOT_EQUAL)
      .let(Text); // с поддержкой символа-заместителя '*'
      // NB: строки длиной 2 или 3 символа без '*' будут трактоваться
      // как код страны или валюты соответственно

Далее вызывается метод select и полученный массив структур MqlCalendarValue форматируется в таблицу с 9-ю колонками методом format.

 MqlCalendarValue records[];
   // применяем условия фильтра и получаем результат
   if(f.select(recordstrueLimit))
   {
      static const ENUM_CALENDAR_PROPERTY props[] =
      {
         CALENDAR_PROPERTY_RECORD_TIME,
         CALENDAR_PROPERTY_COUNTRY_CURRENCY,
         CALENDAR_PROPERTY_EVENT_NAME,
         CALENDAR_PROPERTY_EVENT_IMPORTANCE,
         CALENDAR_PROPERTY_RECORD_ACTUAL,
         CALENDAR_PROPERTY_RECORD_FORECAST,
         CALENDAR_PROPERTY_RECORD_PREVISED,
         CALENDAR_PROPERTY_RECORD_IMPACT,
         CALENDAR_PROPERTY_EVENT_SECTOR,
      };
      static const int p = ArraySize(props);
      
      // выводим отформатированный результат
      string result[];
      if(f.format(recordspropsresulttruetrue))
      {
         for(int i = 0i < ArraySize(result) / p; ++i)
         {
            Print(SubArrayCombine(result" | "i * pp));
         }
      }
   }
}

Ячейки таблицы "склеиваются" в ряды и выводятся в журнал.

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

Selecting calendar records...

country[i]= / ok

calendarValueHistory(temp,from,to,country[i],c)=2372 / ok

Filtering 2372 records

Got 9 records

            TIME | CUR⁞ |                          NAME | IMPORTAN⁞ | ACTU⁞ | FORE⁞ | PREV⁞ |   IMPACT | SECT⁞

2022.06.02 15:15 |  USD | ADP Nonfarm Employment Change |      HIGH |  +128 |  -225 |  +202 | POSITIVE |  JOBS

2022.06.02 15:30 |  USD |      Nonfarm Productivity q/q |  MODERATE |  -7.3 |  -7.5 |  -7.5 | POSITIVE |  JOBS

2022.06.03 15:30 |  USD |              Nonfarm Payrolls |      HIGH |  +390 |   -19 |  +436 | POSITIVE |  JOBS

2022.06.03 15:30 |  USD |      Private Nonfarm Payrolls |  MODERATE |  +333 |    +8 |  +405 | POSITIVE |  JOBS

2022.06.09 08:30 |  EUR |          Nonfarm Payrolls q/q |  MODERATE |  +0.3 |  +0.3 |  +0.3 |       NA |  JOBS

               — |    — |                             — |         — |     — |     — |     — |        — |     —

2022.07.07 15:15 |  USD | ADP Nonfarm Employment Change |      HIGH |  +nan |  -263 |  +128 |       NA |  JOBS

2022.07.08 15:30 |  USD |              Nonfarm Payrolls |      HIGH |  +nan |  -229 |  +390 |       NA |  JOBS

2022.07.08 15:30 |  USD |      Private Nonfarm Payrolls |  MODERATE |  +nan |   +51 |  +333 |       NA |  JOBS

 

Теперь займемся индикатором CalendarMonitor.mq5. Его назначение: отображать пользователю на графике текущую подборку событий в соответствии с заданными фильтрами. Для визуализации таблицы будет использоваться уже знакомый нам класс табло (Tableau.mqh, см. раздел Расчет залога для будущего ордера). Буферов и диаграмм индикатор не имеет.

Входные параметры позволяют задать диапазон временного окна (Scope), а также глобальный контекст для объекта CalendarFilter — либо кодом валюты или страны в строке Context (по умолчанию пусто, т.е. без ограничений), либо с помощью логического флага UseChartCurrencies. Он по умолчанию включен, и именно им рекомендуется пользоваться, чтобы автоматически получать новости тех валют, которые составляют рабочий инструмент графика.

input string Context// Context (country - 2 chars, currency - 3 chars, empty - all)
input ENUM_CALENDAR_SCOPE Scope = SCOPE_WEEK;
input bool UseChartCurrencies = true;

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

input ENUM_CALENDAR_EVENT_TYPE_EXT Type = TYPE_ANY;
input ENUM_CALENDAR_EVENT_SECTOR_EXT Sector = SECTOR_ANY;
input ENUM_CALENDAR_EVENT_IMPORTANCE_EXT Importance = IMPORTANCE_MODERATE// Importance (at least)

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

Внимательные читатель обратит внимание, что здесь использованы неизвестные перечисления: ENUM_CALENDAR_EVENT_TYPE_EXT, ENUM_CALENDAR_EVENT_SECTOR_EXT, ENUM_CALENDAR_EVENT_IMPORTANCE_EXT. Они находятся в уже упоминавшемся файле CalendarDefines.mqh и почти один в один совпадают с аналогичными встроенными перечислениями. Единственное отличие заключается в том, что в них добавлен элемент, означающий "любое" значение. Необходимость в описании подобных перечислений возникла для упрощения ввода условий: сейчас фильтр для каждого поля настраивается с помощью выпадающего списка, где можно выбрать как одно из значений, так и отключить фильтр. Если бы не добавленный элемент перечисления, пришлось бы вводить в интерфейс для каждого поля логический флаг "включено/выключено".

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

input string Text;
input ENUM_CALENDAR_HAS_VALUE HasActual = HAS_ANY;
input ENUM_CALENDAR_HAS_VALUE HasForecast = HAS_ANY;
input ENUM_CALENDAR_HAS_VALUE HasPrevious = HAS_ANY;
input ENUM_CALENDAR_HAS_VALUE HasRevised = HAS_ANY;
input int Limit = 30;

Объекты CalendarFilter и Tableau описаны на глобальном уровне.

CalendarFilter f(Context);
AutoPtr<Tableaut;

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

Настройки фильтра делаются в OnInit последовательными вызовами let-методов согласно входным параметрам.

int OnInit()
{
   if(!f.isLoaded()) return INIT_FAILED;
   
   if(UseChartCurrencies)
   {
      const string base = SymbolInfoString(_SymbolSYMBOL_CURRENCY_BASE);
      const string profit = SymbolInfoString(_SymbolSYMBOL_CURRENCY_PROFIT);
      f.let(base);
      if(base != profit)
      {
         f.let(profit);
      }
   }
   
   if(Type != TYPE_ANY)
   {
      f.let((ENUM_CALENDAR_EVENT_TYPE)Type);
   }
   
   if(Sector != SECTOR_ANY)
   {
      f.let((ENUM_CALENDAR_EVENT_SECTOR)Sector);
   }
   
   if(Importance != IMPORTANCE_ANY)
   {
      f.let((ENUM_CALENDAR_EVENT_IMPORTANCE)(Importance - 1), GREATER);
   }
   
   if(StringLen(Text))
   {
      f.let(Text);
   }
   
   if(HasActual != HAS_ANY)
   {
      f.let(LONG_MINCALENDAR_PROPERTY_RECORD_ACTUAL,
         HasActual == HAS_SET ? NOT_EQUAL : EQUAL);
   }
   ...
   
   EventSetTimer(1);
   
   return INIT_SUCCEEDED;
}

В конце запускается секундный таймер. Вся работа выполняется в OnTimer.

void OnTimer()
{
   static const ENUM_CALENDAR_PROPERTY props[] = // колонки таблицы
   {
      CALENDAR_PROPERTY_RECORD_TIME,
      CALENDAR_PROPERTY_COUNTRY_CURRENCY,
      CALENDAR_PROPERTY_EVENT_NAME,
      CALENDAR_PROPERTY_EVENT_IMPORTANCE,
      CALENDAR_PROPERTY_RECORD_ACTUAL,
      CALENDAR_PROPERTY_RECORD_FORECAST,
      CALENDAR_PROPERTY_RECORD_PREVISED,
      CALENDAR_PROPERTY_RECORD_IMPACT,
      CALENDAR_PROPERTY_EVENT_SECTOR,
   };
   static const int p = ArraySize(props);
   
   MqlCalendarValue records[];
   
   f.let(TimeCurrent() - ScopeTimeCurrent() + Scope); // временнОе окно сдвигаем каждый раз
   
   const ulong trackID = f.getChangeID();
   if(trackID// если состояние уже снимали, проверяем на изменения
   {
      if(f.update(records)) // запрашиваем изменения по фильтрам
      {
         // если изменения есть, уведомляем пользователя
         string result[];
         f.format(recordspropsresult);
         for(int i = 0i < ArraySize(result) / p; ++i)
         {
            Alert(SubArrayCombine(result" | "i * pp));
         }
         // "проваливаемся" далее на обновление таблицы
      }
      else if(trackID == f.getChangeID())
      {
         return// календарь без изменений
      }
   }
   
   // запрашиваем полный набор новостей по фильтрам
   f.select(recordstrueLimit);
 
   // выводим таблицу новостей на график
   string result[];
   f.format(recordspropsresulttruetrue);
   
   if(t[] == NULL || t[].getRows() != ArraySize(records) + 1)
   {
      t = new Tableau("CALT"ArraySize(records) + 1p,
         TBL_CELL_HEIGHT_AUTOTBL_CELL_WIDTH_AUTO,
         CornerMarginsFontSizeFontNameFontName + " Bold",
         TBL_FLAG_ROW_0_HEADER,
         BackgroundColorBackgroundTransparency);
   }
   const string hints[] = {};
   t[].fill(resulthints);
}

Если запустить индикатор на графике EURUSD с настройками по умолчанию можем получить следующую картину.

Отфильтрованный и отформатированный набор новостей на графике

Отфильтрованный и отформатированный набор новостей на графике