- Основные понятия календаря
- Получение списка и описаний доступных стран
- Запрос видов событий по странам и валютам
- Получение описания вида события по идентификатору
- Получение записей о событиях по странам или валютам
- Получение записей о событиях конкретного вида
- Чтение записи о событии по идентификатору
- Отслеживание изменений событий по стране или валюте
- Отслеживание изменений событий по типу
- Фильтрация событий по множеству условий
- Перенос базы календаря в тестер
- Торговля по календарю
Перенос базы календаря в тестер
Календарь доступен для MQL-программ только в режиме онлайн, в связи с чем тестирование новостных торговых стратегий представляет некоторую сложность. Одно из решений — самостоятельное создание некоего образа календаря, то есть кэша, и последующее его использование внутри тестера. Технологии хранения кэша могут быть разными, например, файлы или встроенная база данных SQLite. В данном разделе мы покажем реализацию с применением файла.
В любом случае, при использовании кэша календаря следует помнить, что он соответствует конкретному моменту времени X. Во всех "старых" событиях (финансовых отчетах) более ранних, чем X, уже проставлены актуальные значения, а в более поздних ("будущих" относительно X) — актуальных значений нет и не будет, пока не появится новая, более свежая копия кэша. Иными словами, тестировать индикаторы и эксперты правее X не имеет смысла. А левее X следует избегать заглядывания вперед, то есть не читать актуальные показатели до времени публикации каждой конкретной новости.
Внимание! При запросе данных календаря в терминале время всех событий сообщается с учетом текущей временной зоны сервера, включая и возможную поправку на "летнее" время (как правило, это означает увеличение меток времени на 1 час). Это синхронизирует выпуск новостей со временем котировок в режиме онлайн. Однако прошлые переводы часов (полгода, год назад и более) отображаются только в котировках, но не в событиях календаря. Вся база календаря считывается через MQL5 по текущему часовому поясу сервера. Из-за этого любой создаваемый архив календаря будет содержать корректные временные метки для тех событий, которые происходили при том же режиме DST (включенном или выключенном), что был активен в момент сохранения. Для событий в "противоположных" "полугодиях" требуется самостоятельно делать поправку на час после чтения архива. В рассматриваемых далее примерах этот нюанс опущен.
Назовем класс кэша CalendarCache и поместим его в файл CalendarCache.mqh. Нам потребуется сохранять в файле все 3 таблицы базы календаря (MqlCalendarCountry, MqlCalendarEvent, MqlCalendarValue). Напомним, что MQL5 предоставляет функции FileWriteArray и FileReadArray (см. Запись и чтение массивов), способные напрямую писать и считывать массивы простых структур в файлы. Однако 2 из 3 структур в нашем случае не являются простыми, поскольку в них имеются строковые поля. Поэтому нам потребуется механизм отдельного сохранения строк, похожий на тот, что мы уже использовали в классе CalendarFilter (там был массив строк stringCache, а в фильтрах указывался индекс нужной строки из этого массива).
Чтобы не перемешивать строки из разных "календарных" структур в одном "словаре" мы подготовим шаблонный класс StringRef: параметром-типом T будет выступать любая из MqlCalendar-структур. За счет этого мы получим отдельный кэш строк для стран, и отдельный кэш строк для видов событий.
template<typename T>
|
Сами строки "складируются" в массиве cache с помощью operator=, и извлекаются из него с помощью operator[] (с фиктивным индексом, который всегда опущен). В каждом объекте хранится лишь индекс строки в массиве. Массив cache объявлен статическим, так что будет аккумулировать все строковые поля одной структуры T. Желающие могут поменять принцип кэширования таким образом, чтобы каждое поле структуры имело свой массив, но нам это не принципиально.
Запись массива в файл и чтение из файла выполняются парой статических методов save и load: оба принимают в качестве параметра дескриптор файла.
С учетом класса StringRef опишем структуры, дублирующие стандартные структуры календаря, но в которых вместо строковых полей используются объекты StringRef. Например, для MqlCalendarCountry получим MqlCalendarCountryRef. Стандартная и модифицированная структуры копируются друг в друга также перегруженными операторами '=' и '[]'.
struct MqlCalendarCountryRef
|
Обратите внимание, что в операторах присваивания первого метода работает перегрузка '=' из StringRef, за счет чего все строки попадают в массив StringRef<MqlCalendarCountry>::cache. Во втором методе вызовы оператора '[]' невидимым образом получают адрес строки и возвращают из StringRef непосредственно строку, хранящуюся по этому адресу в массиве cache.
Структура MqlCalendarEventRef определена похожим образом, но в ней всего 3 поля (source_url, event_code, name) требуют замены типа string на StringRef<MqlCalendarEvent>. Структура MqlCalendarValue подобных преобразований не требует, так как в ней нет строковых полей.
На этом подготовительные этапы завершены, и можно приступать к основному рабочему классу кэша CalendarCache.
Исходя из общих соображений, а также для совместимости с уже разработанным классом CalendarFilter опишем в кэше поля, задающие контекст (страну или валюту), диапазон дат хранимых событий, и момент генерации кэша (время X, переменная t).
class CalendarCache
|
В принципе, прописывать ограничения при создании кэша из календаря не имеет особого смысла: более практичным, вероятно, является полный кэш, так как его размер не является критичным — пара десятков мегабайт на середину 2022 года (это история с 2007 года и планируемые события до 2024). Впрочем, ограничения могут пригодиться для демонстрационных программ с искусственно урезанным функционалом.
Очевидно, что в кэше следует предусмотреть массивы календарных структур для хранения всех данных.
MqlCalendarValue values[];
|
Изначально они заполняются из базы календаря методом update.
bool update()
|
Признаком работоспособности кэша выступает поле t со временем заполнения массивов.
Объект заполненного кэша можно записать в файл с помощью метода save. В начале файла идет заголовок CALENDAR_CACHE_HEADER — это строка "MQL5 Calendar Cache\r\nv.1.0\r\n", позволяющая убедиться при чтении в правильности формата. Далее сохраняются переменные context, from, to и t, а также массив values "как есть". Перед самим массивом мы записываем его размер, чтобы восстановить его при чтении.
bool save(string filename = NULL)
|
С массивами events и countries в дело вступают наши структуры-"обертки" с суффиксом "Ref". Вспомогательный метод store конвертирует массив events в массив простых структур erefs, в которых строки заменены на номера в "словаре" строк StringRef<MqlCalendarEvent>. Такие простые структуры уже можно обычным способом записать в файл, но для их последующего чтения требуется также сохранить все строки "словаря" (вызов StringRef<MqlCalendarEvent>::save(handle)). Структуры стран преобразуются и сохраняются в файл аналогичным образом.
MqlCalendarEventRef erefs[];
|
Упомянутый метод store довольно прост: в нем в цикле по элементам выполняется перегруженный оператор присваивания в структурах MqlCalendarEventRef или MqlCalendarCountryRef.
template<typename T1,typename T2>
|
Для загрузки полученного файла в объект кэша написан "зеркальный" метод load. Он в том же порядке читает данные из файла в переменные и массивы, попутно выполняя обратные преобразования строковых полей для видов событий и стран.
bool load(const string filename)
|
Вспомогательный метод restore использует в цикле по элементам перегрузку оператора '[]' в структурах MqlCalendarEventRef или MqlCalendarCountryRef, чтобы по номеру строки получить саму строку и присвоить её в стандартную структуру MqlCalendarEvent или MqlCalendarCountry.
template<typename T1,typename T2>
|
Уже на этом этапе мы могли бы написать на основе класса CalendarCache простой тестовый индикатор, запустить его на онлайн-чарте и сохранить в файл с кэшем календаря. Затем файл можно было бы загрузить из копии индикатора в тестере и получить полный набор событий. Однако для практических разработок этого недостаточно.
Дело в том, что для быстрого доступа к данным требуется обеспечить индексирование — широко известный в программировании принцип, с которым мы еще встретимся в рамках главы про базы данных. По идее, мы могли бы использовать встроенный движок SQLite для хранения кэша, и тогда получили бы индексы "бесплатно", но об этом чуть позже.
Суть индексирования легко понять, если представить, как в нашем кэше эффективно реализовать аналоги стандартных функций календаря. Например, в функцию CalendarValueById передается идентификатор события. Прямой перебор записей в массиве values был бы очень затратным по времени. Поэтому требуется дополнить массив некоторой "структурой данных", которая позволила бы оптимизировать поиск. "Структура данных" взята в кавычки, потому что речь не о сущности языка программирования (struct), а в целом об архитектуре построения данных — она может состоять из разных частей и основываться на разных организационных принципах. Разумеется, дополнительные данные потребуют памяти, но обмен памяти на скорость — обычное явление в программировании.
Наиболее простым решением для индексирования выступает отдельный двумерный массив, отсортированный по возрастанию, так что к нему можно применить быстрый поиск с помощью функции ArrayBsearch. По второму измерению достаточно двух элементов: значения с индексами [i][0], по которым и выполняется сортировка, содержат идентификаторы, а значения [i][1] — порядковые позиции в массиве структур.
Также часто применяется хэширование — преобразование исходных значений в некие ключи (хэши, целые числа) таким образом, что это обеспечивает минимальное количество коллизий (совпадений ключей для разных исходных данных). Фундаментальное свойство ключей — близкое к равномерному случайное распределение их значений, за счет чего они могут использоваться как индексы в заранее распределенных массивах ("корзинах"). Вычисление хэш-функции для одного элемента исходных данных — это быстрый процесс, который фактически выдает адрес самого элемента. По такому принципу, в частности, работают известные "структуры данных" хэш-карты (hashmap).
Если два исходных значения все же получают один и тот же хэш (хотя это бывает редко), они выстраиваются в список для своего ключа, и внутри списка уже будет выполняться последовательный поиск перебором. Но поскольку хэш-функции выбираются так, чтобы количество совпадений было мало, то обычно поиск достигает цели сразу после вычисления хэша.
Для демонстрации мы используем в классе CalendarCache оба подхода: хэширование и бинарный поиск.
В поставку MetaTrader 5 входит набор классов для создания хэш-карт (MQL5/Include/Generic/HashMap.mqh), но мы обойдемся собственной более простой реализацией, в которой останется только принцип использования хэш-функции.

Схема индексации данных путем хэширования
Хэшировать в нашем случае достаточно только идентификаторы объектов календаря. Функция хэширования, которую мы выберем, должна будет преобразовать идентификатор в индекс внутри специального массива: в ячейку с этим индексом и будет сохранена позиция индентификатора в массиве "календарных" структур. Для стран, видов событий и конкретных новостей выделено по собственному массиву.
int id4country[];
|
В их элементах будет храниться порядковый номер записи в соответствующем массиве (countries, events, values).
Под каждый из массивов "переадресации" следует выделить как минимум в 2 раза больше элементов, чем количество соответствующих структур в базе (и в кэше) календаря. За счет этой избыточности мы минимизируем количество коллизий при хэшировании. Считается, что наибольшая эффективность достигается при выборе размера, равного простому числу. Поэтому в классе имеется статический метод size2prime, возвращающий рекомендованный размер массива хэш-"корзинок" (одного из id4-массивов) по количеству элементов в исходных данных.
static int size2prime(const int size)
|
Весь процесс хэширования календаря описан в методе hash. Рассмотрим его начало на примере массива структур countries, а два остальных массива обрабатываются аналогично.
Итак, мы получаем рекомендованный "простой" размер индекса id4country из размера массива countries, вызвав size2prime. Изначально индексный массив заполняется значением -1, то есть всего его элементы свободны. Далее в цикле по странам необходимо вычислить хэш для каждого очередного идентификатора страны и найти по нему свободный индекс в массиве id4country. Этим занимается вспомогательный метод place.
bool hash()
|
В качестве хэш-функции внутри place используется выражение: (MathSwap(id) ^ 0xEFCDAB8967452301) % n, где id — это наш идентификатор, а n — размер индексного массива. Таким образом, результат вычислений всегда приводится к валидному индексу внутри array[]. Принцип выбора хэш-функции — это отдельная тема, выходящая за рамки книги.
int place(const ulong id, const int index, int &array[])
|
Если ячейка под номером p в индексном массиве не занята (равна -1), мы сразу же записываем в элемент [p] адрес размещения "календарной" структуры. Если ячейка уже занята, пытаемся выбрать следующую по формуле p = (p + attempt) % n, где attempt — счетчик попыток (это наш закамуфлированный вариант списка элементов с совпавшим хэшем). Если количество неудачных попыток достигнет одной десятой части исходных данных, индексирование завершится ошибкой, но такое практически исключено при нашем выбранном с запасом размере индексного массива и известной природе хэшируемых данных (уникальных идентификаторов).
В результате хэширования массива структур мы получаем заполненный индексный массив (в нем остаются свободные места, но так и задумано), через который можно по идентификатору элемента календаря узнать расположение соответствующей структуры в массиве структур. Для этого имеется метод find, обратный по смыслу к place.
template<typename S>
|
Покажем, как это используется на практике. Среди стандартных функций календаря есть, в частности, CalendarCountryById и CalendarEventById. Когда потребуется протестировать какую-либо MQL-программу в тестере, она не сможет напрямую обратиться к ним, но зато сможет загрузить кэш календаря в объект CalendarCache, и потому в нем должны быть аналогичные методы.
bool calendarCountryById(ulong country_id, MqlCalendarCountry &cnt)
|
Они как раз и используют метод find и индексные массивы id4country и id4event.
Но это не самые востребованные возможности календаря. Гораздо чаще MQL-программа с новостной стратегией нуждается в функциях CalendarValueHistory, CalendarValueHistoryByEvent, CalendarValueLast или CalendarValueLastByEvent. С помощью них обеспечивается быстрый доступ к записям календаря по времени, по стране или валюте.
Значит, класс CalendarCache должен предоставить аналогичные методы. И здесь мы воспользуемся вторым методом "индексирования" — через двоичный поиск в отсортированном массиве.
Для реализации вышеперечисленных методов добавим в класс еще 4 двумерных массива для установления соответствия между новостью и видом события, новостью и страной, новостью и валютой, а также новостью и временем её публикации.
ulong value2event[][2]; // [0] - event_id, [1] - value_id
|
В первом элементе каждого ряда, то есть под индексами [i][0] будет записываться идентификатор события, страны, валюты или время, соответственно. Во втором элементе ряда, под индексами [i][1] разместятся идентификаторы конкретных новостей. После однократного заполнения всех массивов они сортируются с помощью ArraySort по значениям [i][0]. Затем мы сможем искать по идентификатору, например, события event_id все такие новости в массиве value2event: функция ArrayBsearch вернет номер первого подходящего элемента, за которым будут следовать остальные с таким же event_id, пока не встретится отличный идентификатор. Порядок во второй "колонке" не определен (может быть любым).

Быстрый поиск связанных структур на основе сортировки
Данная операция взаимной увязки структур разных типов осуществляется в методе bind. Размер каждого "связующего" массива идентичен размеру массива новостей. Проходя в цикле по всем новостям, мы пользуемся уже готовыми индексными массивами и методом find для быстрой адресации.
bool bind()
|
В случае валют в качестве идентификатора берется специальное число, получаемое из строки функцией currencyId.
static ulong currencyId(const string s)
|
Теперь мы можем, наконец, целиком представить конструктор класса CalendarCache.
CalendarCache(const string _context = NULL,
|
При запуске на онлайн-чарте созданный объект с параметрами по умолчанию соберет всю информацию календаря (update), проиндексирует её (hash) и свяжет между собой таблицы (bind). Если что-то пойдет не так на любом из этапов, признаком ошибки станет 0 в переменной t. В случае успеха там останется значение из функции TimeTradeServer (напомним, оно ставится внутри update). Такой готовый к работе объект можно экспортировать в файл методом save, описанным выше.
При запуске в тестере объект следует создавать с особым сочетанием параметров from и to (from > to), чтобы программа посчитала строку context именем файла и загрузила из него состояние календаря. Проще всего это сделать так:
CalendarCache calca("filename.cal", true); |
Внутри метода load мы также вызовем hash и bind, чтобы привести объект в рабочее состояние.
bool load(const string filename)
|
На примере функции CalendarValueLast покажем эквивалентную реализацию метода calendarValueLast (с точно таким же прототипом). В качестве идентификатора изменений кэш будет использовать текущее "серверное" время, за неимением открытого программного API для чтения таблицы изменений онлайн-календаря. Гипотетически мы могли бы воспользоваться информацией об идентификаторах изменений, сохраненных сервисом CalendarChangeSaver.mq5, но этот подход требует долговременного сбора статистики, прежде чем можно начать тестирование. Поэтому "серверное" время, генерируемое тестером, принято достаточно адекватной заменой.
Когда MQL-программа запросит изменения первый раз с нулевым идентификатором, просто вернем значение из TimeTradeServer.
int calendarValueLast(ulong &change, MqlCalendarValue &result[],
|
Если идентификатор изменений уже ненулевой, продолжаем основную ветвь алгоритма.
В зависимости от содержимого параметров code и currency, находим идентификаторы страны и валюты. По умолчанию она равны 0, что означает поиск всех изменений.
ulong country_id = 0;
|
Далее, используя переданный отсчет времени change как начало поиска, находим все новости в value2time вплоть до нового, текущего значения TimeTradeServer. Внутри цикла с помощью метода find находим индекс соответствующей структуры MqlCalendarValue в массиве values и при необходимости сравниваем страну и валюту связанного вида события с желаемыми. Все новости, удовлетворяющие критериям, записываются в выходной массив result.
const ulong past = change;
|
По похожему принципу реализованы методы calendarValueHistory, calendarValueHistoryByEvent, calendarValueLastByEvent (последний фактически делегирует всю работу рассмотренному методу calendarValueLast). С полным исходным кодом можно ознакомиться в прилагаемом файле CalendarCache.mqh.
На основе класса кэша логично создать класс-наследник CalendarFilter, который бы при обработке запросов обращался к кэшу, вместо календаря.
Готовое решение находится в файле CalendarFilterCached.mqh. Благодаря тому, что программный интерфейс кэша проектировался по кальке стандартного API, интеграция сводится лишь к пробрасыванию вызовов фильтра в объект-кэш (автоуказатель cache).
class CalendarFilterCached: public CalendarFilter
|
Для проверки работы календаря в тестере создадим новую версию индикатора CalendarMonitor.mq5 — CalendarMonitorCached.mq5.
Основные отличия заключаются в следующем.
Предполагаем, что некоторый файл кэша будет создан или уже создан под именем "xyz.cal" (в папке MQL5/Files) и потому подключаем его к MQL-программе директивой tester_file.
#property tester_file "xyz.cal" |
Напомним, эта директива обеспечивает передачу кэша на любые агенты, включая распределенные (что, впрочем, более актуально для экспертов, а не индикатора). Создать файл кэша с этим (или другим именем) позволяет новая входная переменная CalendarCacheFile. Если пользователь изменит имя по умолчанию на что-то другое, то для работы в тестере нужно будет подправить директиву (требует перекомпиляции!), или перенести файл в общую папку терминалов (эта возможность поддержана в классе кэша, но "оставлена за кадром"), правда такой файл уже недоступен для удаленных агентов.
input string CalendarCacheFile = "xyz.cal"; |
Объект CalendarFilter теперь описан как автоуказатель, потому что в зависимости от того, где запускается индикатор, он может использовать как исходный класс CalendarFilter, так и производный CalendarFilterCached.
AutoPtr<CalendarFilter> fptr;
|
В начале OnInit появился новый фрагмент, отвечающий за генерацию кэша и его чтение.
int OnInit()
|
Если файл кэша удалось прочитать, мы получим готовый объект CalendarCache, который передается в конструктор CalendarFilterCached. В противном случае программа проверяет, выполняется ли она в тестере или онлайн. Отсутствие кэша в тестере — это фатальный случай. На обычном же графике программа создает новый объект на основе данных встроенного календаря и сохраняет его в кэш под указанным именем. Если же имя файла сделать пустым, индикатор будет работать в точности, как исходный — напрямую с календарем.
Запустим индикатор на графике EURUSD. Пользователю будет выведено предупреждение о том, что указанный файл не найден и сделана попытка его сохранить. При условии, что календарь включен в настройках терминала, мы должны получить примерно следующие строки в журнале. Здесь приводится вариант с подробной диагностической информацией — её можно отключить, закомментировав в исходном коде директиву #define LOGGING.
Loading calendar cache xyz.cal
|
Теперь мы можем выбрать индикатор CalendarMonitorCached.mq5 в тестере и увидеть в динамике, на истории, как меняется таблица новостей.

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