Обзор Copy-функций для получения массивов котировок

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

Функция

Действие

CopyRates

Получение истории котировок в массив структур MqlRates

CopyTime

Получение истории времен открытия баров в массив типа datetime

CopyOpen

Получение истории цен открытия баров в массив типа double

CopyHigh

Получение истории максимальных цен баров в массив типа double

CopyLow

Получение истории минимальных цен баров в массив типа double

CopyClose

Получение истории цен закрытия баров в массив типа double

CopyTickVolume

Получение истории тиковых объемов в массив типа long

CopyRealVolume

Получение истории биржевых объемов в массив типа long

CopySpread

Получение истории спредов в массив типа int

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

int Copy***(const string symbolENUM_TIMEFRAMES timeframe, ...)

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

  • начальный индекс бара и количество баров — Copy***(..., int offset, int count, ...);
  • время начала диапазона и количество баров — Copy***(..., datetime start, int count, ...);
  • время начала и окончания диапазона — Copy***(..., datetime start, datetime stop, ...).

При этом в нотации параметров подразумевается, что запрашиваемые данные имеют направление индексации, как в таймсерии, то есть в позиции offset с индексом 0 хранятся данные текущего незавершенного бара, и увеличение индексов соответствует продвижению вглубь ценовой истории. Из-за этого, в частности, для второго варианта указанное количество баров count будет отсчитываться в прошлое от начала диапазона offset, то есть в сторону уменьшения времени.

Дополнительную гибкость предоставляет третий вариант: в нем не важно, в каком порядке задать стартовую и финишную даты (start/stop), — функции в любом случае вернут данные в диапазоне от меньшей даты до большей. Подходящие бары отбираются по такому принципу, чтобы их время открытия находилось между временными отсчетами start/stop или было равно одному из них, т.е. диапазон [start;stop] рассматривается с включением границ.

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

Напомним, что точность представления времени в типе datetime составляет 1 секунду. Значения start/stop не обязаны быть округленными до размера периода. Например, диапазон от 14:59 до 16:01 позволит выделить на таймфрейме H1 два бара для 15:00 и 16:00. Вырожденный диапазон с равными и округленными метками, например, 15:00 в котировках H1, соответствует одному бару.

Вы можете запрашивать бары на дневном таймфрейме даже при наличии в параметрах start/stop ненулевых значений часов/минут/секунд (несмотря на то, что на таймфрейме D1 метки баров имеют время 00:00). При этом в результат попадут только те бары D1, что имеют время открытия после минимального из start/stop и до максимального из start/stop (равенство с метками дневных баров в данном случае невозможно, поскольку в искомом времени присутствуют часы/минуты/секунды). Например, между отсечками D'2021.09.01 12:00' и D'2021.09.03 07:00' находятся два времени открытия баров D1 — D'2021.09.02' и D'2021.09.03'. Именно эти бары и попадут в результат. Бар D'2021.09.01' имеет время открытия 00:00 раньше, чем начало диапазона, и потому отбрасывается. Бар D'2021.09.03' включен в результат, несмотря на то, что в диапазон попало только 7 утренних часов из этого дня. С другой стороны, запрос нескольких часов внутри дня, например, между D'2021.09.01 12:00' и D'2021.09.01 15:00' не охватит ни одного дневного бара (время открытия бара D'2021.09.01' не попадает в этот диапазон), и потому приемный массив будет пуст.

Единственное отличие всех функций из таблицы заключается в типе массива, принимающего данные, — он передается последним параметром по ссылке. Например, функция CopyRates помещает запрошенные данные в массив структур MqlRates, а функция CopyTime — время открытия баров в массив типа datetime, и так далее.

Таким образом, общие прототипы функций можно представить так:

int Copy***(const string symbol, ENUM_TIMEFRAMES timeframe, int offset, int count, type &result[])

int Copy***(const string symbol, ENUM_TIMEFRAMES timeframe, datetime start, int count, type &result[])

int Copy***(const string symbol, ENUM_TIMEFRAMES timeframe, datetime start, datetime stop, type &result[])

Здесь type соответствует любому из типов MqlRates, datetime, double, long или int, в зависимости от конкретной функции.

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

Важно отметить, что в приемном массиве полученные данные всегда физически размещаются в хронологическом порядке, от прошлого к будущему. Таким образом, если для приемного массива используется стандартная индексация (то есть к нему не применялась функция ArraySetAsSeries), то элемент с 0-м индексом будет самым старым, а последний элемент — самым новым. Если же для массива была выполнена инструкция ArraySetAsSeries(result, true), то нумерация будет вестись в обратном порядке, как в таймсерии: 0-й элемент окажется самым новым в диапазоне, а последний — самым старым. Это иллюстрирует следующий рисунок.

Таймсерии терминала и приемный массив

Таймсерии терминала и приемный массив

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

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

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

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

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

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

Для пояснения принципов работы функций на практике рассмотрим скрипт SeriesCopy.mq5. Он содержит несколько вызовов функции CopyTime, что позволяет наглядно увидеть, как соотносятся временные метки и номера баров.

В скрипте определен динамический массив times для приема данных. Все запросы делаются для символа "EURUSD" и таймфрейма H1.

void OnStart()
{
   datetime times[];

Для начала делается запрос 10 баров, начиная с 5 сентября 2021, в прошлое. Поскольку этот день воскресенье, предыдущие бары были в пятницу 3-го числа (см. лог ниже).

   PRTF(CopyTime("EURUSD"PERIOD_H1D'2021.09.05', 10times)); // 10 / ok
   ArrayPrint(times);
   /*
   [0] 2021.09.03 14:00 2021.09.03 15:00 2021.09.03 16:00 2021.09.03 17:00 2021.09.03 18:00
   [5] 2021.09.03 19:00 2021.09.03 20:00 2021.09.03 21:00 2021.09.03 22:00 2021.09.03 23:00
   */

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

   PRTF(ArraySetAsSeries(timestrue)); // true / ok
   ArrayPrint(times);
   /*
   [0] 2021.09.03 23:00 2021.09.03 22:00 2021.09.03 21:00 2021.09.03 20:00 2021.09.03 19:00
   [5] 2021.09.03 18:00 2021.09.03 17:00 2021.09.03 16:00 2021.09.03 15:00 2021.09.03 14:00
   */

Для следующих экспериментов восстановим обычный порядок.

   PRTF(ArraySetAsSeries(timesfalse)); // true / ok

Теперь запросим неопределенное количество баров между двумя временными отсчетами (количество неизвестно, потому что в диапазоне могут оказаться праздники, например). Сделаем это двумя способами: в первом случае укажем диапазон от будущего в прошлое, а во втором — из прошлого в будущее. Оба результата совпадут.

   //                                      FROM                 TO
   PRTF(CopyTime("EURUSD"PERIOD_H1D'2021.09.06 03:00', D'2021.09.05 03:00', times));
   ArrayPrint(times)  //                   FROM                 TO
   PRTF(CopyTime("EURUSD"PERIOD_H1D'2021.09.05 03:00', D'2021.09.06 03:00', times));
   ArrayPrint(times);
   /*
   CopyTime(EURUSD,PERIOD_H1,D'2021.09.06 03:00',D'2021.09.05 03:00',times)=4 / ok
   2021.09.06 00:00 2021.09.06 01:00 2021.09.06 02:00 2021.09.06 03:00
   CopyTime(EURUSD,PERIOD_H1,D'2021.09.05 03:00',D'2021.09.06 03:00',times)=4 / ok
   2021.09.06 00:00 2021.09.06 01:00 2021.09.06 02:00 2021.09.06 03:00
   */

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

   PRTF(ArraySetAsSeries(timestrue)); // true / ok
   ArrayPrint(times);
   // 2021.09.06 03:00 2021.09.06 02:00 2021.09.06 01:00 2021.09.06 00:00

Хотя две временных метки отстоят друг от друга на 24 часа, что предполагает получение в массиве 25 элементов (вспоминаем, что начало и конец обрабатываются включительно), результат содержит только 4 бара. Дело в том, что 5-е сентября приходится на воскресенье, и потому из всего диапазона торговля велась только в утренние часы 6-го числа.

Также обратите внимание, что приемный массив был автоматически уменьшен в размере с 10 до 4 элементов.

Наконец, запросим 10 баров, начиная с 100-го бара (полученные результаты будут зависеть от вашего текущего времени и доступной истории).

   PRTF(CopyTime("EURUSD"PERIOD_H110010times)); // 10 / ok
   ArrayPrint(times);
   /*
   [0] 2021.10.04 19:00 2021.10.04 18:00 2021.10.04 17:00 2021.10.04 16:00 2021.10.04 15:00
   [5] 2021.10.04 14:00 2021.10.04 13:00 2021.10.04 12:00 2021.10.04 11:00 2021.10.04 10:00
   */
}

Из-за индексации как в таймсерии, массив выведен в обратном хронологическом порядке.