- Основные характеристики индикаторов
- Главное событие индикаторов: OnCalculate
- Два типа индикаторов: для главного и отдельного окна
- Настройка количества буферов и графических построений
- Назначение массива в качестве буфера: SetIndexBuffer
- Настройка графических построений: PlotIndexSetInteger
- Правила сопоставления буферов и диаграмм
- Применение директив для настройки графических построений
- Установка названий для графических построений
- Визуализация пропусков данных (пустых элементов)
- Индикаторы с собственным подокном: размер и уровни
- Общие свойства индикаторов: заголовок и точность значений
- Поэлементное раскрашивание диаграмм
- Пропуск отрисовки на начальных барах
- Ожидание данных и управление видимостью (DRAW_NONE)
- Мультивалютные и мультитаймфреймовые индикаторы
- Отслеживание формирования баров
- Тестирование индикаторов
- Ограничения и преимущества индикаторов
- Создание заготовки индикатора в Мастере MQL
Мультивалютные и мультитаймфреймовые индикаторы
До сих пор мы рассматривали индикаторы, работающие с котировками или тиками символа, являющегося текущим символом графика. Однако иногда необходимо проводить анализ нескольких финансовых инструментов или одного инструмента, отличного от текущего. В подобных случаях, как мы видели в случае анализа тиков, недостаточно стандартных таймсерий, передаваемых в индикатор через параметры OnCalculate. И встает задача каким-либо образом запросить "чужие" котировки, дождаться их построения, и только затем рассчитывать на их основе индикатор.
В этом смысле запрос и построение котировок для таймфрейма, отличного от текущего таймфрейма графика, не отличается от механизмов работы с другими символами. Поэтому в данном разделе мы продемонстрируем создание мультивалютных индикаторов, а мультитаймфреймовые можно организовать по аналогичному принципу.
Одной из проблем, которую потребуется в любом случае решить, является синхронизация баров по времени. В частности, для разных символов могут быть различные расписания торгов, выходные дни, и в общем случае нумерация баров на родительском графике и в котировках "чужого" символа не совпадает.
Для начала упростим задачу и ограничимся одним произвольным символом, который может отличаться от текущего. Довольно часто трейдеру желательно видеть одновременно несколько графиков различных символов (например, ведущий и ведомый в коррелированной паре). Разработаем индикатор IndSubChartSimple.mq5 для отображения котировки выбранного пользователем символа в подокне.
IndSubChartSimple
Чтобы повторить внешний вид основного графика предусмотрим во входных параметрах не только указание символа, но и режима рисования: DRAW_CANDLES, DRAW_BARS, DRAW_LINE. Первые два требуют 4 буфера и выводят полную четверку цен Open, High, Low, Close (японскими свечами или барами), а последний — довольствуется единственным буфером для показа линии по цене Close. Но чтобы поддержать все режимы выбираем максимальное требуемое количество буферов.
#property indicator_separate_window
|
Массивы для буферов описаны по названиям типов цен.
double open[];
|
По умолчанию включено отображение японских свечей. В этом режиме MQL5 позволяет указать не один цвет, а несколько. В директиве #property indicator_colorN они задаются через запятую. Если цветов два, то первый определяет цвет контуров свечей, а второй — заполнение. Если цветов три, как в нашем случае, то первый, как и в предыдущем случае, определяет цвет контуров свечей, а второй и третий — тело бычьей и тело медвежьей свечи соответственно.
В главе посвященной графикам мы познакомимся с перечислением ENUM_CHART_MODE, которое описывает 3 доступных режима работы графиков.
Элементы ENUM_CHART_MODE |
Элементы ENUM_DRAW_TYPE |
---|---|
CHART_CANDLES |
DRAW_CANDLES |
CHART_BARS |
DRAW_BARS |
CHART_LINE |
DRAW_LINE |
Они соответствуют выбранным нами режимам отрисовки, что в общем-то и не удивительно, потому что для этого индикатора намеренно выбирались способы рисования, повторяющие стандартные. ENUM_CHART_MODE удобно здесь использовать, поскольку оно содержит только 3 нужных нам элемента, в отличие от ENUM_DRAW_TYPE, в котором много других способов отрисовки.
Таким образом, входные переменные получают следующие определения.
input string SubSymbol = ""; // Symbol
|
Для перевода ENUM_CHART_MODE в ENUM_DRAW_TYPE реализована простая функция.
ENUM_DRAW_TYPE Mode2Style(const ENUM_CHART_MODE m)
|
Пустая строка во входном параметре SubSymbol означает текущий символ графика. Однако поскольку MQL5 не позволяет редактировать входные переменные, нам придется добавить глобальную переменную для хранения реального рабочего символа и присваивать её в обработчике OnInit.
string symbol;
|
Попутно необходимо проверить наличие введенного пользователем символа и добавить его в окно Обзор рынка: этим занимается функция SymbolSelect, которую мы изучим в главе про символы.
Для обобщения настройки буферов и диаграмм в исходном коде выделено несколько вспомогательных функций:
- InitBuffer — настройка одного буфера
- InitBuffers — настройка всего набора буферов
- InitPlot — настройка одной диаграммы
Отдельные функции объединяют в себе несколько действий, повторяющихся при регистрации идентичных сущностей, а также открывают дорогу для дальнейшего развития данного индикатора в главе про графики: там мы поддержим интерактивное изменение настроек рисования в ответ на действия пользователя с графиком (см. полную версию индикатора IndSubChart.mq5 в разделе Режимы отображения графика).
void InitBuffer(const int index, double &buffer[],
|
Обратите внимание, что при включении режима линейной диаграммы используется только массив close и ему назначается индекс 0. Остальные три массива полностью скрываются от пользователя за счет свойства INDICATOR_CALCULATIONS. В режимах свечей и баров задействованы все xtnsht массива, и их нумерация соответствует стандарту OHLC, как того требуют типы отрисовки DRAW_CANDLES и DRAW_BARS. Всем массивам назначается свойство "серийности", то есть индексации справа налево.
Функция InitBuffers возвращает заголовок для буферов в Окне данных.
В функции InitPlot устанавливаются все необходимые атрибуты диаграммы.
void InitPlot(const int index, const string name, const int style,
|
Начальная настройка единственной диаграммы (с индексом 0) производится с помощью новых функций в обработчике OnInit.
int OnInit()
|
Хотя в данной версии индикатора настройка выполняется единожды, это происходит динамически с учетом входного параметра Mode, в отличие от статической настройки, которую предоставляют директивы #property. И в дальнейшем, в полной версии индикатора мы сможем вызывать InitPlot по много раз, меняя внешнее представление индикатора "на ходу".
Заполнение буферов производится в OnCalculate. В простейшем случае, когда заданный символ совпадает с графиком, нам было бы достаточно написать примерно такую реализацию.
int OnCalculate(const int rates_total, const int prev_calculated,
|
Однако при обработке произвольного символа параметры-массивы не содержат нужных котировок, да и общее число доступных баров наверняка отличается. Более того, при первом размещении индикатора на графике котировки "чужого" символа вообще могут быть не готовы, если для него заранее не открыт по соседству другой график. Да и загрузка котировок стороннего символа будет происходить асинхронно, из-за чего в любой момент может "прибыть" новая партия баров, требующая полного пересчета.
Поэтому создадим переменные, контролирующие количество баров на стороннем символе (lastAvailable), редактируемый "клон" константного аргумента prev_calculated, а также признак готовности котировок.
static bool initialized; // флаг готовности котировок символа
|
В начале OnCalculate добавим проверку на одномоментное появление более одного бара: в этом нам помогает переменная lastAvailable, которую мы заполняем на основе значения iBars(symbol, _Period) перед предыдущим штатным выходом из функции, то есть в случае успешного расчета. При обнаружении докачки истории следует сбросить _prev_calculated и количество баров в 0, а также убрать признак готовности, чтобы пересчитать индикатор заново.
int OnCalculate(const int rates_total, const int prev_calculated,
|
Слово "ожидание" в комментарии не случайно взято в кавычки. Как мы помним, в индикаторах нельзя реально ждать (чтобы не тормозить интерфейсный поток терминала), и вместо этого недостаток данных должен просто приводить к выходу из функции. Таким образом "ожидание" будет заключаться в ожидании следующего события для расчета: по приходу тика или в ответ на запрос обновления графика.
Проверкой готовности котировок займется следующий фрагмент кода.
int OnCalculate(const int rates_total, const int prev_calculated,
|
Основную работу в нем выполняет особая функция QuoteRefresh. Она принимает в качестве аргументов интересующий нас символ, таймфрейм и время самого первого (старого) бара на текущем графике — более ранние даты нас не интересуют, но не факт, что на "чужом" символе имеется история на всю эту глубину. Именно поэтому удобно скрыть все сложности проверок в отдельной функции.
Функция вернет true, когда данные будут скачаны и синхронизированы в доступном объеме. Её внутреннее устройство рассмотрим через минуту.
Когда синхронизация выполнена, используем функцию iBarShift для нахождения синхронных баров и копирования их значений OHLC (функции iOpen, iHigh, iLow, iClose).
ArraySetAsSeries(time, true); // обход из настоящего в прошлое
|
Альтернативный и на первый взгляд более эффективный способ копирования массивов цен целиком с помощью Copy-функций здесь не подходит из-за того, что в бары с равными индексами могут соответствовать на разных символах разным меткам времени. Поэтому после копирования пришлось бы анализировать даты и перемещать элементы внутри буферов, подгоняя под время на текущем графике.
Поскольку в функцию iBarShift последним параметром передается true, функция будет искать точное соответствие времени баров, и если в другом символе какой-либо бар отсутствует, мы получим -1 и отобразим на графике пустоту (EMPTY_VALUE).
После успешного полного расчета новые бары будут обсчитываться в экономном режиме, т.е. с учетом _prev_calculated и rates_total.
А теперь обратимся к функции QuoteRefresh. Как универсальная и полезная, она вынесена в заголовочный файл QuoteRefresh.mqh.
В самом начале делается проверка на то, не запрашивается ли таймсерия текущего символа и текущего таймфрейма из MQL-программы типа индикатор. Такие запросы запрещены, поскольку "родная" таймсерия, на которой запущен индикатор, уже находится в процессе построения терминалом или готова: подгонять его еще раз — чревато зацикливанием или блокировкой. Поэтому мы просто возвращаем признак синхронизации (SERIES_SYNCHRONIZED), и если она пока не готова, индикатору следует проверить данные позднее (на следующих тиках, по таймеру или как-то еще).
bool QuoteRefresh(const string asset, const ENUM_TIMEFRAMES period,
|
Вторая проверка касается количества баров: если оно уже равно максимальному разрешенному на графиках, докачивать что-либо не имеет смысла.
if(Bars(asset, period) >= TerminalInfoInteger(TERMINAL_MAXBARS))
|
Далее начинается фрагмент кода, который последовательно узнает у терминала начальные даты доступных котировок:
- по заданному таймфрейму (SERIES_FIRSTDATE);
- без привязки к таймфрейму (SERIES_TERMINAL_FIRSTDATE) в локальной базе терминала;
- без привязки к таймфрейму (SERIES_SERVER_FIRSTDATE) на сервере.
Если на каком-либо этапе запрашиваемая дата уже входит в область доступных данных, получим true как признак готовности. В противном случае выполняется запрос данных из локальной базы терминала или с сервера, с последующим построением таймсерии (все это делается асинхронно и автоматически в ответ на наши вызовы CopyTime, можно было использовать другие Copy-функции).
datetime times[1];
|
Индикатор готов. Откомпилируем и запустим его, например, на графике EURUSD,H1, указав в качестве дополнительного символа USDRUB. В журнале появятся примерно такие записи:
Host EURUSD 20001 bars up to 2018.08.09 13:00:00
|
После индикации завершения процесса ("Done"), в подокне будут показаны свечи "чужого" графика.
Индикатор IndSubChartSimple — DRAW_CANDLES с котировками стороннего символа
Важно отметить, что из-за сокращенной торговой сессии значащие бары для USDRUB занимают лишь дневную часть каждого суточного интервала.
IndUnityPercent
Вторым индикатором, который мы создадим в рамках данного раздела, является настоящий мультивалютный (строго говоря, мультисимвольный) индикатор IndUnityPercent.mq5. Его идея заключается в том, чтобы отобразить относительную силу всех независимых валют (активов), входящих в состав заданных финансовых инструментов. Например, если мы торгуем корзиной из двух тикеров EURUSD и XAUUSD, то фактически в стоимости учитываются доллар, евро и золото — каждый из этих активов обладает относительной стоимостью по сравнению с другими.
В каждый момент времени существуют текущие цены, которые описываются очевидными формулами:
EUR / USD = EURUSD
|
где переменные EUR, USD, XAU — некие самостоятельные "стоимости" активов, а EURUSD и XAUUSD — константы (известные котировки).
Для нахождения переменных дополним систему еще один уравнением, ограничив сумму квадратов переменных единицей (отсюда и первое слово в названии индикатора — Unity):
EUR * EUR + USD * USD + XAU * XAU = 1 |
Переменных может быть гораздо больше, и их логично обозначить как xi, причем x0 — основная валюта (общая для всех инструментов: она обязательно должна быть).
Тогда в общем виде формулы расчета переменных запишутся следующим образом (для краткости мы опустим процесс их выведения):
x0 = sqrt(1 / (1 + sum(C(xi, x0)2))), i = 1..n
|
где n — количество переменных, C(xi,x0) — котировка i-ой пары, включающей соответствующие переменные. Обратите внимание, что количество переменных на 1 больше, чем инструментов.
Поскольку котировки, участвующие в расчете, обычно сильно отличаются (например, как в случае EURUSD и XAUUSD), а кроме того выражаются только друг через друга (то есть без привязки к какой-либо стабильной базе), имеет смысл перейти от абсолютных значений к процентным изменениям. Таким образом, при написании алгоритмов по вышеприведенным формулам будем вместо котировки C(xi,x0) брать отношение C(xi,x0)[0] / C(xi,x0)[1], где индексы в квадратных скобках означают текущий [0] и предыдущий [1] бар. Кроме того, для ускорения расчета можно избавиться от возведения в квадрат и взятия квадратного корня.
Для визуализации линий предусмотрим некое максимальное допустимое количество валют и индикаторных буферов. Разумеется, в расчете можно использовать не все, если пользователь введет меньше символов. Но повысить лимит динамически нельзя: потребуется изменить директивы и перекомпилировать индикатор.
#define BUF_NUM 15
|
При реализации данного индикатора решим попутно одну неприятную проблему. Поскольку предполагается множество однотипных буферов, стандартный подход предполагает их экстенсивное кодирование "размножением" (пресловутый нерекомендуемый стиль программирования "copy & paste").
double buffer1[];
|
Это неудобно, неэффективно и чревато ошибками. Вместо этого применим ООП: создадим класс, который будет хранить массив для индикаторного буфера и отвечать за его единообразную настройку — ведь наши буфера должны быть одинаковыми (за исключением цветов и, возможно, увеличенной толщины для тех валют, которые составляют символ текущего графика, но это донастраивается позднее — после введения входных параметров пользователем).
При наличии такого класса достаточно распределить массив его объектов, и индикаторные буфера будут автоматически подключены и настроены в необходимом количестве. Схематично данный подход иллюстрируется следующим псевдокодом.
// код "движка" с поддержкой массива унифицированных индикаторных буферов
|
Благодаря перегрузкам операторов мы можем придерживаться привычного синтаксиса для присваивания значений элементам объекта-буфера: buffer[i] = value.
В коде индикатора вместо множества строк с описаниями отдельных массивов достаточно будет определить один "массив массивов".
// код индикатора
|
Полная версия классов, реализующих данный механизм, приводится в файле IndBufArray.mqh. Следует отметить, что в нем обеспечена поддержка только буферов, но не диаграмм. В идеале набор классов должен быть расширен новыми, позволяющими создавать готовые объекты-диаграммы, которые занимали бы в массиве буферов необходимое их количество согласно типу конкретной диаграммы. Изучить и дополнить файл предлагается самостоятельно. В частности, в коде имеется класс-менеджер массива индикаторных буферов BufferArray для создания "массивов массивов" с одинаковыми значениями свойств, таких как тип ENUM_INDEXBUFFER_TYPE, направление индексации, "пустое" значение. Мы его используем в новом индикаторе следующим образом:
BufferArray buffers(BUF_NUM, true); |
Здесь в первом параметре конструктора передается требуемое количество буферов, а во втором — признак индексации как в таймсерии (об этом — чуть ниже).
После этого определения мы можем в любом месте кода применять удобную нотацию для установки значения j-го бара i-го буфера (она использует двойную перегрузку оператора[] — не только в объекте-буфере, но и в массиве буферов):
buffers[i][j] = value; |
Во входных переменных индикатора позволим пользователю задать перечень интересных ему символов через запятую, а также ограничим количество баров для расчета на истории, чтобы управлять длительностью загрузки и синхронизации потенциально большого набора инструментов. Если вы решите показывать всю доступную историю, следует выявить и применить наименьшее количество баров среди доступных у разных инструментов, с контролем докачки с сервера.
input string Instruments = "EURUSD,GBPUSD,USDCHF,USDJPY,AUDUSD,USDCAD,NZDUSD";
|
При старте программы следует произвести разбор списка символов и сформировать из них отдельный массив Symbols размера SymbolCount.
string Symbols[];
|
У всех символов должна быть одна общая валюта (обычно "USD") для выявления взаимных соотношений. В зависимости от того, является эта общая валюта в конкретном символе базовой (на первом месте в паре, если речь о Forex) или валютой котирования (на втором месте в паре Forex), в расчетах должны участвовать её прямые или обратные котировки (1.0 / курс). Это направление будем хранить в массиве Direction.
Приведем с некоторыми сокращениями функцию InitSymbols, которая выполняет описанные действия. В случае успешного анализа списка она возвращает название общей валюты. Получить базовую валюту и валюту котирования любого финансового инструмента позволяет встроенная функция SymbolInfoString: мы изучим её в главе про финансовые инструменты.
string InitSymbols()
|
В цикле ведется учет встречаемости каждой валюты в составе всех инструментов с помощью вспомогательного шаблонного класса MapArray. Такой объект описан в индикаторе на глобальном уровне и требует подключения заголовочного файла MapArray.mqh.
#include <MQL5Book/MapArray.mqh>
|
Поскольку данный класс выполняет вспомогательную роль, здесь он подробно не описан. Желающие могут ознакомиться с исходным кодом. Суть в том, что при вызове его метода inc для нового названия валюты, оно добавляется во внутренний массив с начальным значением счетчика равным 1, а если название уже встречалось — счетчик увеличивается на 1.
Впоследствии мы находим общую валюту, как ту, у которой счетчик больше 1. При правильных настройках остальные валюты должны встретиться ровно по разу. Вот продолжение функции InitSymbols.
...
|
Имея готовую функцию InitSymbols, мы можем написать OnInit (приводится с упрощениями).
int OnInit()
|
Теперь познакомимся с обработчиком главного события OnCalculate.
Важно отметить, что порядок обхода баров в главном цикле — обратный, как в таймсерии, от настоящего к прошлому. Данный подход более удобен для мультивалютных индикаторов, потому что глубина истории разных символов может оказаться разной, и имеет смысл обсчитывать бары от текущего назад, вплоть до первого обнаружения недостатка данных по любому из символов. При этом следует трактовать досрочное прерывание цикла не как ошибку и вернуть rates_total, чтобы отобразить на графике значения для уже посчитанных, наиболее актуальных баров.
Однако в данной упрощенной версии IndUnityPercent мы этого не делаем и обходимся более простым и жестким подходом: пользователь должен определить безусловную глубину запроса истории с помощью параметра BarLimit. Иными словами, по всем символам должны быть данные плоть до временной метки бара с номером BarLimit на символе графика — иначе индикатор будет пытаться скачать недостающие данные.
int OnCalculate(const int rates_total,
|
Расчет значений для всех буферов на i-ом баре осуществляет функция Calculate (см. далее). В случае недостатка данных она вернет false, и мы запустим таймер, чтобы дать время на построение таймсерий по всем требуемым инструментам. В обработчике таймера привычным образом отправим терминалу запрос на обновление графика.
void OnTimer()
|
В функции Calculate первым делом определяем диапазон дат текущего и предыдущего бара, на которых будут рассчитываться изменения.
bool Calculate(const int bar)
|
Две даты потребовалось, чтобы вызвать далее функцию CopyClose в том её варианте, где указывается интервал дат. В данном индикаторе мы не можем использовать вариант с количеством баров, потому что на любом символе могут быть произвольные пропуски в барах, отличные от пропусков на других символах. Например, если на одном символе существуют бары t (текущий) и t-1 (предыдущий), то для него возможно корректно рассчитать изменение Close[t]/Close[t-1]. Однако на другом символе бар t может отсутствовать, и запрос двух баров вернет "ближайшие" слева (в прошлом) бары, причем это прошлое может отстоять от "настоящего" достаточно далеко (например, соответствовать торговой сессии за предыдущий день, если символ не торгуется круглосуточно).
Чтобы такого не происходило, индикатор запрашивает котировки строго в интервале, и если он оказывается пустым для конкретного символа, это означает отсутствие изменений.
При этом возможны ситуации, когда такой запрос вернет больше 2 баров, и в этом случае всегда берутся два последних (правых) бара, как наиболее актуальные. Например, при размещении на графике USDRUB,H1 индикатор будет "видеть", что после бара в 17:00 каждого рабочего дня идет бар 10:00 следующего рабочего дня. Однако для основных валютных пар Forex, таких как EURUSD, между ними будет 16 вечерних, ночных и утренних баров H1.
bool Calculate(const int bar)
|
Когда изменения получены, алгоритм работает по приводившимся ранее формулам и записывает значения в индикаторные буфера.
double sum = 1.0;
|
Посмотрим, как индикатор работает с настройками по умолчанию, на наборе основных инструментов Forex (при первом размещении может потребовать заметное время для получения таймсерий, если для инструментов не были открыты графики).
Мультисимвольный индикатор IndUnityPercent с основными валютами Forex
Расстояние между линиями двух валют в окне индикатора равно изменению соответствующей котировки в процентах (между двумя последовательными ценами Close). Отсюда второе слово в названии индикатора — Percent.
В следующей главе про программное использование индикаторов мы представим продвинутую версию IndUnityPercentPro.mq5 с заменой Copy-функций на вызов встроенных индикаторов iMA, что позволит без лишних усилий реализовать сглаживание и расчет по произвольному типу цен.