Среди множества
пользовательских индикаторов, работающих в среде терминала
Meta Trader 4,следует особо выделить целый
класс индикаторов, который прославился отображением информации на более ранних
периодах истории с учетом данных, полученных позднее. Проще говоря, такие
индикаторы рассчитывают свои значения "задним числом". Когда пользователь
впервые видит подобный индикатор, то у него возникает иллюзия, заключающаяся в
способности индикатора предсказывать будущие события с большой точностью. Но
стоит трейдеру, думающему, что он наконец-то нашел Грааль, понаблюдать за
работой волшебного индикатора, как он поймет, что намеренно или случайно был
введен в заблуждение. Ведь весь секрет волшебства программы заключается в том,
что необходимого значка в момент начала событий еще не было. Значок появляется
после свершившихся событий в том месте, где он ранее не наблюдался, что и
приводит к иллюзии пророчества.
В трейдерской
среде описанные волшебные индикаторы называют "перерисовывающимися". Этот термин
как нельзя лучше подходит к описанию возникающих с ними ситуаций. Некоторые из
волшебных индикаторов не только добавляют информацию, но и просто изменяют былые
данные. Во многих случаях доходит до того, что удаляется сигнал в одну сторону,
сменяясь сигналом противоположного направления. В итоге на истории
индикатор выглядит просто замечательно. Не иначе, как откровение.
Причин появления
перерисовывающихся индикаторов может быть несколько:
1. Банальная
ошибка в коде, которую программист не заметил, или заметил, но не знал, как
исправить, или просто не захотел этого делать. В таких случаях стоит лишь
разводить руками, понимая ситуацию, т.е. не пользоваться явно ошибочной
программой.
2. Программа
разрабатывается с явным намерением ввести в заблуждение пользователя (для
продажи сего "чуда" или для хвастовства). Как и в первом случае: "такой хоккей
нам не нужен".
3. Алгоритм работы
индикатора базируется на принципе обновления последнего значения. То есть
стабильным, неизменным, является только предпоследнее значение индикатора.
Использование последнего значения ничего не гарантирует. Такими индикаторами
пользоваться вполне возможно. Стоит только помнить об этой их особенности.
В некотором смысле
к третьему типу индикаторов относятся все существующие индикаторы, которые
отображают данные на формирующемся (нулевом) баре. Для опытных трейдеров
является аксиомой тот факт, что в торговле нельзя использовать данные
индикатора, соответствующие формирующемуся бару, т.к. они могут разительно
отличаться от тех значений, которые будут зафиксированы после закрытия
этого самого бара.
Тем не менее,
имеются такие индикаторы, которые могут изменять свое последнее значение, не
обязательно соответствующее формирующемуся бару. Ярким представителем
таких индикаторов является ZigZag. Его внешний вид на
истории при любых значениях настроечных параметров всегда выше всяческих похвал
(см. рис. 1). Так и хочется сказать: "Увидел пик индикатора - продавай, увидел
впадину индикатора - покупай!"
Рис. 1. Индикатор ZigZag.
Тем не менее,
практическое использование показаний индикатора ZigZag
достаточно ограничено. Дело в том, что, как сказано выше,окончание последнего отрезка в любой момент времени находится в стадии
формирования. Поэтому никогда нельзя рассчитывать на то, что появление нового
экстремума ZigZag означает окончание наблюдаемого
движения рынка и его разворот. Трейдеры-новички неоднократно спотыкались на этом
моменте, теряя значительные средства. Они, в основном, уповали на то, что "все
равно разворот уже близко, т.к. ZigZag сформировал
новый экстремум". Каково же бывает их удивление, а заодно и разочарование, когда
настоящий экстремум оказывается в нескольких сотнях пунктов от того места, где
ZigZag начал извещать о развороте.
Как у любого
продукта, у индикатора ZigZag, кроме очевидных
достоинств, имеются свои конструктивные недостатки. Одним из таких недостатков
является наличие трех настроечных параметров (ExtDepth,
ExtDeviation и ExtBackstep).Наличие параметров привязывает индикатор не к реальной рыночной ситуации,
а к области неизвестно откуда взявшихся цифр. В итоге не так уж редко можно
наблюдать, что, несмотря на очевидную смену направления движения рынка,
индикатор молчит, упорно не желая отмечать новый экстремум, т.к. для этого у
него не хватает "нужного" количества баров (см. рис. 2).
Рис. 2. При некоторых параметрах (30, 5, 3) ZigZag
упорно не признает новое движение.
Если же немного
изменить численные значения индикатора, то движение признается состоявшимся.
Это, на взгляд автора, и является ахиллесовой пятой программы - она не умеет
настраиваться на рынок сама, ей требуется указание трейдера. Возможно, на чей-то
взгляд такой подход покажется оправданным, т.к. все рыночные настроения
интерпретируются только трейдером и никем другим. Но не нужно забывать, что
любому, даже самому современному человеку, присуща инертность мышления. Эта
инертность, зачастую, не позволяет адекватно оценить быстропротекающие рыночные
процессы. Программе же чужды такие рассуждения, она в любой момент "говорит" только
то, что "видит". Поэтому далеко не всегда нужно взгромождать на себя весь груз
рыночной ответственности.
Попробуем создать
новый алгоритм, любимого многими, индикатора ZigZag,
стараясь уйти от "магии цифр" (настроечных параметров). Расположение всех пиков
и впадин будет диктоваться только существующими рыночными условиями и, конечно
же, алгоритмом, заложенным в программу. При этом мы не претендуем на то, что
избавим индикатор от его главного недостатка - неизменность последнего
экстремума, т.к. устранение этой "проблемки" означает нахождение алгоритма
предсказания рынка.
Техническое задание (ТЗ)
Новый
ZigZag будет ориентироваться на моменты пробоя
предыдущей свечи одним из экстремумов новой свечи. Проще говоря, если предыдущая
свеча пробита вверх, то имеем дело с зарождением восходящего движения, а если
вниз - нисходящего движения. При использовании такой формулировки
проблема возникает в тех случаях, когда свеча пробивает предыдущую свечу сразу в двух
направлениях. Спрашивается: какой вариант принимать? Решением проблемы является
использование дополнительного условия, которое гласит, что принимается вариант,
который соответствует типу новой свечи. Например, если свеча бычья, то тренд
признается восходящим, а если свеча медвежья - то нисходящим. Особняком стоит
свеча типа додж (не бычья и не медвежья). В этом случае принять решение тяжелее.
Поэтому оставим его без решения, т.е. небудем
фиксировать смену тренда (см. рис. 3).
Рис. 3. Формирование экстремумов нового ZigZag.
В местах,
отмеченными выносками, формируется новый экстремум (начало и
окончание синих отрезков), который будет перемещаться вслед за достижением
новых высот (или низов). Фиксация экстремума (использование его данных в
торговле) возможна только после появления
нового (противоположного) экстремума.
Кратко
сформулируем основные положения ТЗ для нового индикатора:
1. Смена
направления тренда
1.1 Нисходящим
направлением считается ситуация, при которой новая свеча своим минимумом
пробивает минимум предыдущей свечи или если медвежья свеча пробила предыдущую
свечу в обе стороны.
1.2 Восходящим
направлением считается ситуация, при которой новая свеча своим максимумом
пробивает максимум предыдущей свечи или если бычья свеча пробила предыдущую
свечу в обе стороны.
2. Отображение
экстремумов
2.1 Новый
экстремум отображается сразу же в момент смены направления тренда. Он
устанавливается на соответствующем экстремуме сигнальной свечи (той, на которой
произошла смена). Предыдущий (противоположный) экстремум фиксируется на
максимуме (или минимуме) цены, действующей на участке развития последнего
тренда.
2.2 Если
сигнальная свеча одновременно образует оба экстремума индикатора, то зафиксированный
экстремум отображается на своем месте, а новый (несформированный) передвигается
на следующую свечу.
Последний пункт ТЗ
требует отдельного пояснения, т.к. описывает достаточно редкую ситуацию, при
которой сигнальная свеча формирует экстремум предыдущего движения и одновременно
приводит к возникновению смены направления тренда (см. рис. 4).
Рис. 4. Висячие сады нового ZigZag.
Появление
упомянутой несуразности вызвано тем, что средствами MQL4 (по
крайней мере, без привлечения графических объектов)
невозможно отобразить два значения на одной свече. Напомним, что индикаторы
состоят из индикаторных буферов, т.е. массивов, значение каждого элемента
которых соответствует одному определенному бару. Невозможно поставить в
соответствие одному бару два значения одного индикаторного буфера.
Индикатор NeoZigZag_HighLow
Начальным этапом
разработки всякого индикатора является функция, вычисляющая индекс бара, с
которого будет начато построение значений индикатора. В наиболее поздних
проектах MQLabs за это отвечала небольшая функция
GetRecalcIndex. Но ее привычный вид не совсем
соответствует нуждам разрабатываемого индикатора, имеющего свои особенности:
//+-------------------------------------------------------------------------------------+
//| Определение индекса бара, с которого необходимо производить перерасчет |
//+-------------------------------------------------------------------------------------+
int GetRecalcIndex()
{
int counted_bars = IndicatorCounted();
if (counted_bars == 0) // Кол-во посчитанных баров - 0. Будут
{ // ..пересчитаны все буфера с самого..
ArrayInitialize(ZZBuf, EMPTY_VALUE); // ..начала. Очистка буферов
ArrayInitialize(UpDnBuf, 0);
return(Bars - 2); // Начинаем со второго бара истории
}
return(Bars - counted_bars - 1); // Начинаем с нового бара
}
Приведенная
функция учитывает момент, связанный с подкачкой новой истории. В этот момент
нарушается привычный ход расчетов значений индикатора, и расчет совершается с
начала истории. При этом не производится автоматическое очищение индикаторных
буферов, что приводит к появлению значений индикатора, оставшихся от предыдущих
расчетов, вместе с новыми значениями. Поэтому в теле функции
GetRecalcIndex производится принудительная очистка буферов
ZZBuf и UpDnBuf, если
количество посчитанных баров равно нулю.
Назначение буфера
ZZBuf - отображение экстремумов индикатора. Пустое
значение (EMPTY_VALUE) элемента буфера означает
отсутствие экстремума, непустое - наличие экстремума. Линии, видимые
пользователю, соединяют непустые элементы буфера.
Буфер
UpDnBuf является теневым, т.к. он не отображает
никаких данных. Пользователь вообще не подозревает о его существовании.
Назначение буфера - сохранение направления тренда для каждого бара. Это
позволяет всегда, без лишних поисков, определять направление текущего тренда.
Восходящему направлению соответствует значение элемента буфера - 1, а
нисходящему - (-1). Нулевое значение соответствует элементу, над которым еще не
производились действия.
Первый пункт
ТЗ описывает алгоритм определения направления тренда на любой
отдельно взятой свече. Реализация этого пункта воплощается всего лишь одной
функцией - GetTrend:
//+-------------------------------------------------------------------------------------+
//| Определение тенденции по соотношению указанной и предыдущей свечей |
//+-------------------------------------------------------------------------------------+
int GetTrend(int index)
{
// - 1 - == Новая свеча выше предыдущей =================================================
if (High[index] > High[index+1])
{
if (Low[index] >= Low[index+1]) // Новая свеча не пробивала предыдущую
return(TREND_UP); // ..вниз - рост. В противном случае..
if (Open[index] < Close[index]) // ..прибегаем к дополнительному..
return(TREND_UP); // ..критерию - типу свечи. Свеча..
// ..бычья - тренд восходящий
if (Open[index] > Close[index]) // Свеча медвежья - тренд нисходящий
return(TREND_DOWN);
return(NO_TREND); // Для дожи тренд не определен
}
// - 1 - == Окончание блока =============================================================
// - 2 - == Новая свеча ниже предыдущей =================================================
if (Low[index] < Low[index+1])
{
if (High[index] <= High[index+1]) // Новая свеча не пробивала предыдущую
return(TREND_DOWN); // ..вверх - падение. В противном..
// ..случае необходимо использовать
// ..дополнительный критерий - тип..
// ..свечи, но он учтен в предыдущем..
// ..блоке.
}
// - 2 - == Окончание блока =============================================================
return(NO_TREND); // Если свеча не пробита - нет тренда
}
В качестве
аргумента функция получает индекс бара, для которого необходимо определить
направление тренда.
Алгоритм явно
разделен на три ветви: свеча index пробила предыдущую
свечу (index+1)вверх, свеча
index пробила предыдущую свечу вниз и свеча
index не пробила предыдущую свечу ни в одном из
направлений. Первая ветвь реализована в первом блоке функции, вторая - во
втором, а третья приводит к строке, возвращающей состояние
NO_TREND - нет тренда (значение 0).
Первый блок
реализует пункт ТЗ 1.1 и, частично, 1.2. Если свеча index
пробила предыдущую свечу только вверх (минимум свечи выше или равен
минимуму предыдущей свечи), то возвращается состояние
TREND_UP - восходящий тренд (значение 1). Если выясняется, что предыдущая
свеча пробита в обоих направлениях, то возвращаемое функцией значение будет
зависеть от типа свечи. Если свеча бычья, то направление - восходящее, если
медвежья - нисходящее, а если додж - нет тренда.
Второй блок
несравненно короче первого, т.к. в первом блоке частично учтен пункт 1.2 ТЗ.
Поэтому второму блоку остается лишь проверить вариант, касающийся пробоя
минимума предыдущей свечи без пробоя максимума. В этом случае возвращаемое
функцией значение - TREND_DOWN (нисходящий тренд,
значение -1).
Согласно пункту
2.1 ТЗ, в момент смены направления тренда должен быть отображен зафиксированный
экстремум и новый формирующийся экстремум. Чтобы каждый раз не производить поиск
локального экстремума, соответствующего предыдущему участку тренда, можно
выполнять постоянное слежение за максимумом и минимумом цены. Тем более,
функция GetTrend определяет, какого рода пробой
имел место. К тому же, в буфере ZZBuf всегда хранится
значение последнего экстремума. Нам достаточно найти последний непустой
элемент этого буфера и сравнить с ним новые данные. Для этого потребуется две
функции: определение максимума (CheckHigh)и определение минимума (CheckLow):
//+-------------------------------------------------------------------------------------+
//| Сравнение последнего максимума с новым максимумом |
//+-------------------------------------------------------------------------------------+
void CheckHigh(int index)
{
int cnt = GetLastIndexNoEmptyValue(index); // Найдем последний непустой элемент..
// ..зиг-зага
if (cnt == Bars) // Если элемент не найден (достигнут..
{ // ..конец истории), то максимумом..
ZZBuf[index] = High[index]; // ..считается текущий максимум
return;
}
if (High[index] > ZZBuf[cnt]) // Элемент найден. Сравним его..
{ // ..значение с новым максимумом. Если
ZZBuf[cnt] = EMPTY_VALUE; // ..новый максимум выше, то..
ZZBuf[index] = High[index]; // ..предыдущий максимум уничтожается,
// ..а новый сохраняется
}
}
//+-------------------------------------------------------------------------------------+
//| Сравнение последнего минимума с новым минимумом |
//+-------------------------------------------------------------------------------------+
void CheckLow(int index)
{
int cnt = GetLastIndexNoEmptyValue(index); // Найдем последний непустой элемент..
// ..зиг-зага
if (cnt == Bars) // Если элемент не найден (достигнут..
{ // ..конец истории), то минимумом..
ZZBuf[index] = Low[index]; // ..считается текущий минимум
return;
}
if (Low[index] < ZZBuf[cnt]) // Элемент найден. Сравним его..
{ // ..значение с новым минимумом. Если
ZZBuf[cnt] = EMPTY_VALUE; // ..новый минимум ниже, то..
ZZBuf[index] = Low[index]; // ..предыдущий минимум уничтожается,
// ..а новый сохраняется
}
}
Обе функции в
качестве аргумента получают индекс бара, экстремумы которого должны сравниваться
с предыдущим экстремумом индикатора.
Предыдущий
экстремум эти функции определяют при помощи вызова функции
GetLastIndexNoEmptyValue (рассмотрена ниже). Ее
результат записывается в локальную переменную cnt.
Если значение cnt лежит за пределами существующей
истории (cnt == Bars), то в
качестве нового экстремума применяется экстремум свечи index.
Если же значение cnt представляет существующую
историю (предыдущий экстремум найден), то производится сравнение этого
экстремума с экстремумом свечи index. В случае пробоя
новым экстремумом уровня старого экстремума, старый экстремум стирается из
памяти, а новый экстремум записывается в элемент буфера ZZBuf
с индексом index.
Функция
GetLastIndexNoEmptyValue безупречно проста:
//+-------------------------------------------------------------------------------------+
//| Поиск последнего элемента ZZBuf с непустым значением |
//+-------------------------------------------------------------------------------------+
int GetLastIndexNoEmptyValue(int index)
{
while (ZZBuf[index] == EMPTY_VALUE && index < Bars)// Поиск по графику справа налево
index++; // Пока не будет найден экстремум или
// ..пока не достигнем конца истории
return(index); // Индекс бара с непустым значением..
// ..зиг-зага
}
Цикл "пока"
исполняется до тех пор, пока значение переменной index
неукажет на непустой элемент буфера
ZZBuf или на первый бар за пределами истории (Bars).
Достигнутое циклом значение является результатом исполнения функции.
Окончательная
реализация пунктов 2.1 и 2.2 ТЗ производится функцией
TrendChangeOrMissing:
//+-------------------------------------------------------------------------------------+
//| Смена тренда или его продолжение при отсутствии нового сигнала |
//+-------------------------------------------------------------------------------------+
void TrendChangeOrMissing(int trend, int i)
{
// - 1 - == Отсутствие нового сигнала - продолжение существующего тренда ================
if (trend == NO_TREND) // Новая свеча поглощена предыдущей
{ // Тренд сохраняется, последний..
UpDnBuf[i] = UpDnBuf[i+1]; // ..экстремум остается на своем месте
return;
}
// - 1 - == Окончание блока =============================================================
// - 2 - == Тренд изменился с нисходящего на восходящий =================================
UpDnBuf[i] = trend; // Сохранение признака направления
if (trend == TREND_UP) // Тренд изменился на восходящий
{
CheckLow(i); // Проверка появления нового минимума
if (ZZBuf[i] != EMPTY_VALUE) // Если минимум обновлен, то максимум
ZZBuf[i-1] = High[i]; // ..переносим на следующий бар
else // Если минимум не обновлен, то..
ZZBuf[i] = High[i]; // ..максимум отображается на текущем
return; // ..баре
}
// - 2 - == Окончание блока =============================================================
// - 3 - == Тренд изменился с восходящего на нисходящий =================================
CheckHigh(i); // Проверка появления нового максимума
if (ZZBuf[i] != EMPTY_VALUE) // Если максимум обновлен, то минимум
ZZBuf[i-1] = Low[i]; // ..отображается на следующем баре
else // Если максимум не обновлен, то..
ZZBuf[i] = Low[i]; // ..минимум отображается на текущем..
// ..баре
// - 3 - == Окончание блока =============================================================
}
Функции
передаются значения trend и i.
Первый аргумент содержит величину, которую вернула функция
GetTrend, т.е. TREND_UP, TREND_DOWN
или NO_TREND. Аргумент i
указывает на индекс бара, для которого было рассчитано направление
тренда.
Первый блок
функции поддерживает текущее направление тренда, переписывая предыдущий признак
направления тренда в текущую ячейку буфера UpDnBuf.
Других действий не требуется, т.к. новая свеча не пробила предыдущую ни в одном
из направлений, а, значит, имеющийся экстремум не мог быть обновлен.
Второй блок
обрабатывает ситуацию смены направления тренда с нисходящего на восходящее. При
этом новый признак направления сохраняется в текущем элементе буфера
UpDnBuf. Затем вызывается функция
CheckLow, которая определяет необходимость обновления минимума
предыдущего нисходящего движения. Если минимум обновлен, то максимум текущей
свечи перемещается на следующую свечу (см. рис. 4). В противном случае максимум
отображается на своем месте.
Третий блок
также начинается с проверки, но не минимума, а максимума, т.к. до текущей смены
направления развивался восходящий тренд. Обновление максимума приводит к
отображению минимума новой свечи на следующей свече. Если максимум не обновлен,
то минимум отображается на своем месте.
Выполнение
циклической обработки всех доступных исторических баров происходит в функции
ZigZag:
//+-------------------------------------------------------------------------------------+
//| Расчет значений индикатора |
//+-------------------------------------------------------------------------------------+
void ZigZag(int limit)
{
for (int i = limit; i > 0; i--) // По всем новым барам
{
int trend = GetTrend(i); // Получение направления на баре i
if (trend != UpDnBuf[i+1]) // Направление на текущем баре..
{ // ..отличается от направления на..
TrendChangeOrMissing(trend, i); // ..предыдущем баре.
continue;
}
UpDnBuf[i] = trend; // Направление не изменяется
if (trend == TREND_UP) // Сохранение восходящего тренда
{
CheckHigh(i); // Обновление максимума
continue;
}
if (trend == TREND_DOWN) // Сохранение нисходящего тренда
CheckLow(i); // Обновление минимума
}
}
Аргумент
limit содержит индекс бара, который был рассчитан
функцией GetRecalcIndex. Расчет значений индикатора
производится именно с этого бара.
Каждая итерация
цикла может пройти от двух до четырех стадий.
Стадия первая.
Получение направления тренда на текущей свече - вызов функции
GetTrend.
Стадия вторая.
Сравнение полученного направления с предыдущим направлением. Если направление
отличается, то вызывается функция TrendChangeOrMissing,
после исполнения которой происходит переход к следующей итерации цикла.
Стадия третья.
Если новое направление тренда не отличается от старого, то в буфер
UpDnBuf записывается признак направления тренда. При восходящем тренде
вызывается функция проверки максимума - CheckHigh. Далее происходит переход к следующей итерации цикла.
Стадия
четвертая. Указывает на наличие нисходящего тренда. Производится проверка
минимума.
Рассмотрение результатов
Индикатор
NeoZigZag_HighLow в значительной степени отличается от
своего предтечи - ZigZag'а (см. рис. 5).Синей линией отображен ZigZag со стандартными
параметрами - (12, 5, 3), а красной - NeoZigZag_HighLow.
Рис. 5. Отличия и сходства индикаторов.
На первый
взгляд, новый ZigZag слишком хаотичен, т.к.
подвергается частой смене направления. На этом фоне исходный
ZigZag кажется более основательным и сдержанным. Но не будем забывать,
что эта сдержанность продиктована ему свыше - пользователем, он сам не определял
ее. В этом плане NeoZigZag_HighLow является более
автономным.
В качестве еще
одной идеи ZigZag можно предложить более "спокойную"
версию - тот же NeoZigZag_HighLow, который основан не на простом
пробое свечи, а на пробое ценой закрытия. То есть сигналом смены тренда вверх
является закрытие свечи выше максимума предыдущей свечи, а сигналом смены тренда
вниз - закрытие свечи ниже минимума предыдущей свечи. Этот индикатор очень похож
на своего предшественника (NeoZigZag_HighLow)алгоритмически и более значительно - по внешнему виду (см. рис. 6).
NeoZigZag_Close отображен жирной зеленой линией.
Рис. 6. Три ZigZag.
Практическое
применение разработанных индикаторов будет описано в следующей части материала.
Там же будет проведен сравнительный анализ каждого из подходов к интерпретации
рыночных ситуаций.