English 中文 Español Deutsch 日本語 Português
Паттерн Флаг

Паттерн Флаг

MetaTrader 5Примеры | 5 июня 2017, 10:20
11 474 6
Dmitry Fedoseev
Dmitry Fedoseev

Содержание

Введение

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


Рис. 1. Флаг

В книгах и на сайтах, посвященных техническому анализу, паттерн Флаг часто рассматривается параллельно с паттерном Вымпел. Вымпел, в отличие от флага, имеет треугольное полотнище (рис. 2), поэтому в некоторых руководствах по техническому анализу паттерн Флаг рассматривается вместе с паттерном Треугольник.


Рис. 2. Вымпел

Казалось бы, Вымпел и Треугольник — просто разные названия одного и того же паттерна. Но в некоторых руководствах по техническому анализу, в частности в книге Томаса Булковского "Полная энциклопедия графических ценовых моделей", фигурируют они оба по отдельности. Также в этой книге описан паттерн Клин, подобный Треугольнику, но отраженному по горизонтали. Слева у него находится узкая часть, а справа цена двигается с нарастающим размахом (рис. 3).


Рис. 3. Клин


Кроме паттерна Клин, известен паттерн Расширяющийся треугольник, а также различные Прямоугольные формации,  аналогичные Флагу. Значит, должны существовать четкие правила отличия Вымпела от Треугольника, Клина от  Расширяющегося треугольника и Флага от Горизонтальной формации. Этот вопрос и будет рассмотрен в статье в первую очередь. Затем будут созданы индикаторы для поиска всех этих паттернов. 


Отличия паттернов Вымпел и Треугольник

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

  • Прямоугольная формация — Флаг;
  • Треугольник — Вымпел;
  • Расширяющийся треугольник — Клин.

В одну категорию попадают паттерны Прямоугольная формация, Треугольник и Расширяющийся треугольник, в другую — Флаг, Вымпел и Клин.

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


Рис. 4. Паттерны: a — Горизонтальная формация, b — Треугольник, c — Расширяющийся треугольник.  
Паттерны показаны для предполагаемого движения вверх (для покупки).

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


Рис. 5. Паттерны: a — Флаг, b — Вымпел, c — Клин.
Паттерны показаны для предполагаемого движения вверх (для покупки).

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


Горизонтальная формация

Начнем с рассмотрения паттернов первой категории. Для их определения удобно использовать индикатор Зигзаг. Первый паттерн в этой категории — Горизонтальная формация, которой во второй категории соответствует Флаг. Однако в большинстве руководств по техническому анализу именно Горизонтальную формацию и называют Флагом.

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


Рис. 6. Горизонтальные формации: a — из двух вершин/впадин, b — из трех вершин/впадин.
Паттерны показаны для предполагаемого движения вверх (для покупки) 

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


Рис. 7. a — Горизонтальная формация, b — формация с наклоном вниз, c — формация с наклоном вверх. 
Паттерны показаны для предполагаемого движения вверх (для покупки)

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

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

 
Рис. 8. Окончание формирования паттерна и момент открытия
позиции buy: a — для горизонтальной формации,
b — для формации с наклоном вниз 

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


Рис. 9. Определение момента входа (покупки) для формации с наклоном вверх

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

Цель для Горизонтальной формации определяется по размеру вертикального движения до момента формирования паттерна. Сколько цена прошла до образования формации, столько же она должна пройти и после нее (рис. 10).


Рис. 10. Определение цели. Дистанция L1, пройденная ценой до вхождения
в формацию, равна дистанции L2 после выхода из формации.  

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


Рис. 11. Простой способ определения цели. Дистанция L1, пройденная ценой до образования
первой вершины,
равна дистанции L2 от последней низины до цели


Сужающийся треугольник

Паттерн Cужающийся треугольник незначительно отличается от Горизонтальной формации. Единственное отличие заключается в том, что отрезки Зигзага, образующего паттерн, должны последовательно сужаться (рис. 12).


 Рис. 12. Сужающийся треугольник. Отрезок 3-4 должен быть
меньше отрезка 1-2, а отрезок 5-6 меньше отрезка 3-4
 

В остальном все так же, как у паттерна Горизонтальная формация: горизонтальное расположение треугольника или с наклоном вверх/вниз, вход по прорыву линии сопротивления, образованной двумя последними вершинами или горизонтального уровня от последней вершины, аналогичный расчет цели.


Расширяющийся треугольник

Все, что касалось Сужающегося треугольника, относится и к Расширяющемуся треугольнику, отличие только в том, что теперь отрезки Зигзага, составляющие паттерн, увеличиваются (рис. 13).


 Рис. 13. Расширяющийся треугольник. Отрезок 3-4 должен быть 
больше отрезка 1-2, а отрезок 5-6 больше отрезка 3-4
 

Такое значительное сходство всех трех паттернов располагает к созданию одного универсального индикатора для их поиска.  


Универсальный индикатор поиска горизонтальной формации и треугольников

Для создания индикатора потребуется индикатор iUniZigZagSW из статьи "Универсальный зигзаг". Для его работы необходимо еще несколько дополнительных файлов: CSorceData.mqh, CZZDirection.mqh и CZZDraw.mqh. Эти файлы, как и файл  iUniZigZagSW.mq5, находятся в приложении к статье "Универсальный зигзаг". Скачайте его, разархивируйте и скопируйте из него папку MQL5 в папку данных терминала. После копирования в папке MQL5/Indicators появится папка ZigZags с несколькими файлами (в том числе и файлом с iUniZigZagSW.mq5), а в папке MQL5/Includes — папка ZigZag с файлами CSorceData.mqh, CZZDirection.mqh и CZZDraw.mqh. После копирования файлов перезапустите терминал, чтобы индикаторы откомпилировались, или скомпилируйте их все по отдельности в редакторе MetaEditor. Обязательно убедитесь в работе индикатора iUniZigZagSW прикрепив его на график в терминале.

В статье "Волны Вульфа" на одной из промежуточных стадий создания индикатора был сохранен файл iWolfeWaves_Step_1.mq5. В нем выполняется обращение к индикатору iUniZigZagSW.mq5 через функцию iCustom() и формируется массив со всеми вершинами и впадинами зигзага. Скачайте приложение к статье "Волны Вульфа", разархивируйте его, файл iWolfeWaves_Step_1.mq5 скопируйте в папку MQL5/Indicators, переименуйте его в "iHorizontalFormation" и откройте в редакторе MetaEditor. Вся дальнейшая работа над индикатором поиска паттерна Горизонтальная формация будет выполняться в этом файле. Возможно, в файле потребуется изменить путь к индикатору iUniZigZagSW. Чтобы проверить это, откомпилируйте индикатор и попробуйте прикрепить его на график. Если при этом откроется окно с сообщением "Error load indicator", найдите в функции OnInit() вызов функции iCustom() и исправьте имя вызываемого индикатора с "iUniZigZagSW" на "ZigZags\\iUniZigZagSW". После исправления снова откомпилируйте индикатор и убедитесь, что теперь он прикрепляется на график без сообщений об ошибках. Рисовать индикатор пока ничего не должен.

Весь процесс поиска рассматриваемых здесь паттернов можно четко разделить на несколько почти независимых задач:

  1. Определение величины хода цены, предшествующего формированию паттерна.
  2. Определение формы паттерна.
  3. Определение наклона паттерна.
  4. Завершение формирования паттерна: сразу после формирования паттерна или ожидание прорыва уровня.
  5. Расчет цели.

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

Перечисление для выбора формы (типа паттерна):

enum EPatternType{
   PatternTapered,
   PatternRectangular,
   PatternExpanding
};

Соответствующая ему переменная в окне свойств:

input EPatternType         Pattern        =  PatternRectangular;

Этот параметр позволит делать выбор формы паттерна: PatternTapered — сужающийся треугольник, PatternRectangular — прямоугольник, PatternExpanding — расширяющийся треугольник.

Перечисление для выбора наклона паттерна:

enum EInclineType{
   InclineAlong,
   InclineHorizontally,
   InclineAgainst
};

Соответствующая ему переменная в окне свойств:

input EInclineType         Incline        =  InclineHorizontally; 

Этот параметр позволит выбирать наклон паттерна: InclineAlong — наклон по ходу предполагаемого движения (для покупок вверх, для продаж — вниз), InclineHorizontally — без наклона, InclineAgainst — наклон в обратную сторону предполагаемого движения цены.

Перечисление для выбора способа завершения формирования паттерна:

enum EEndType{
   Immediately,
   OneLastVertex,
   TwoLastVertices
};

Соответствующая ему переменная в окне свойств:

input EEndType             CompletionType =  Immediately;

Этот параметр позволит выбирать следующие варианты: Immediately — сразу по формированию паттерна, OneLastVertex — после прорыва горизонтального уровня, образованного последней вершиной паттерна, TwoLastVertices — после прорыва уровня, образованного двумя последними вершинами паттерна.

Перечисление для выбора варианта расчета цели:

enum ETargetType{
   FromVertexToVertex,
   OneVertex,
   TwoVertices
};

Соответствующая ему переменная в окне свойств:

input ETargetType          Target         =  OneVertex;

Этот параметр позволит делать выбор между следующими вариантами: FromVertexToVertex — от вершины до вершины (рис. 11), OneVertex — по одной вершине (рис. 10), TwoVertices — по двум вершинам (используются две начальные впадины паттерна, см. рис. 14).


Рис. 14. Паттерн из трех вершин. Вариант определения цели — TwoVertices,
способ завершения паттерна — OneLastVertex.

При варианте завершения паттерна Immediately параметр Target не действует, поскольку возможен только один вариант определения цели — FromVertexToVertex. Для двух других вариантов завершения паттерна (OneLastVertex и TwoLastVertices), возможно различное сочетание со всеми тремя вариантами параметра CompletionType. Обратите внимание на одну особенность: в вариантах определения цели OneVertex и TwoVertices для определения значения цели используются одна или две первых впадины (точка 2, или точки 2 и 4 на рис. 14), а для определения уровня пробоя используются одна или две последние вершины (точка 5 или точки 3 и 5 на рис. 14). Если бы использовался паттерн из двух вершин, то использовалась бы точка 3 или точки 1 и 3.

Для решения задачи 1 потребуется параметр, определяющий величину хода цены, предшествующего паттерну:

input double               K1             =  1.5;

Высота отрезка 1-2 (см. рис 14) принимается за базис паттерна (эталонный размер), все проверки размеров будут выполняться относительно неё. Параметр K1 определяет, во сколько раз отрезок 0-1 должен быть больше высоты отрезка 1-2.

Для решения задачи 2 (определения формы паттерна) используется параметр K2:

input double               K2             =  0.25;

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

Для решения задачи 3 (определения наклона паттерна) используется параметр K3:

input double               K3             =  0.25;

Чем меньше величина параметра, тем ровнее должен быть расположен паттерн. При поиске наклонных паттернов увеличение параметра K2 позволит находить паттерны только с четко выраженным наклоном.

Наконец, один из главных параметров:

input int                  N              =  2;

Параметр N определяет количество вершин паттерна. 

В итоге получаем следующий набор внешних параметров (кроме параметров Зигзага):

input EPatternType         Pattern        =  PatternRectangular;
input EInclineType         Incline        =  InclineHorizontally;     
input double               K1             =  1.5;
input double               K2             =  0.25;
input double               K3             =  0.25;
input int                  N              =  2;
input EEndType             CompletionType =  Immediately;
input ETargetType          Target         =  OneVertex;

Используя параметр N, вычислим, сколько всего точек зигзага потребуется для определения паттерна. Сначала объявим глобальную переменную:

int RequiredCount;

В функции OnInit() вычислим ее значение:

RequiredCount=N*2+2;

2*N — количество вершин, образующих паттерн (N верхних и N нижних). Еще одна вершина определяет предшествующий ход цены и еще одна — последняя точка нового отрезка зигзага (в расчетах не используется).

Вся дальнейшая работа будет выполняться в функции OnTick(). Новый код будет добавляться в самый конец главного индикаторного цикла. Проверка условий формирования паттерна будет выполняться при условии наличия достаточного количества точек зигзага и только в моменты смены его направления. Слежение за ценой и уровнем будет выполняться при каждом изменении зигзага:

if(CurCount>=RequiredCount){
   if(CurDir!=PreDir){      
      // проверка условий

   }
   // слежение за ценой и уровнем

} 

Сначала вычисляется базовая величина — высота отрезка 1-2 (см. рис. 14). Эта величина будет использоваться при проверке всех условий формирования паттерна. Затем выполняется проверка условия задачи 1 — величины предшествующего хода:

int li=CurCount-RequiredCount;                                            // индекс начальной точки паттерна в массиве PeackTrough
double base=MathAbs(PeackTrough[li+1].Val-PeackTrough[li+2].Val);         // базовая величина
double l1=MathAbs(PeackTrough[li+1].Val-PeackTrough[li].Val);             // высота отрезка 1-2
   if(l1>=base*K1){                                                       // проверка величины предшествующего хода
        // остальные проверки

   }

Дальнейшие проверки будут зависеть от того, вверх или вниз направлен последний отрезок зигзага.

if(CurDir==1){              // последний отрезок зигзага направлен вверх
   // проверка условия для направления вверх  
             
}
else if(CurDir==-1){        // последний отрезок зигзага направлен вниз
   // проверка условия для направления вниз

}

Рассмотрим проверку условия для направления вверх:

if(CheckForm(li,base) && CheckInclineForBuy(li,base)){      // проверка формы и направления
   if(CompletionType==Immediately){ 
      // сразу ставим стрелку индикатора
      UpArrowBuffer[i]=low[i];
      // ставим точку цели
      UpDotBuffer[i]=PeackTrough[CurCount-1].Val+l1;
   }
   else{
      // установка параметров пробиваемого уровня
      SetLevelParameters(1);
      // установка параметров цели
      SetTarget(1,li);
   }
} 

Проверка условия формирования паттерна выполняется двумя функциями: CheckForm() — проверка формы паттерна (задача 2) и CheckInclineForBuy() — проверка наклона (задача 3). Если проверки формы и наклона пройдены, то в зависимости от типа завершения паттерна выполняется или установка стрелки и точки цели на график, или установка параметров пробиваемого уровня, после чего индикатор следит за уровнем.

Функция CheckForm(). В функцию передается индекс первой точки паттерна в массиве PeackTrough и базовая величина base. Разберем код функции:

bool CheckForm(int li,double base){               
   switch(Pattern){
      case PatternTapered: 
         // сужающийся
         return(CheckFormTapered(li,base));
      break;               
      case PatternRectangular: 
         // прямоугольный
         return(CheckFormRectangular(li,base));
      break;
      case PatternExpanding: 
         // расширяющийся
         return(CheckFormExpanding(li,base));
      break;
   }
   return(true);
}

В функции, в зависимости от значения параметра Pattern, выполняется вызов соответствующих функций: CheckFormTapered() — сужающийся треугольник, CheckFormRectangular() — прямоугольная формация, CheckFormExpanding() — расширяющийся треугольник,

Функция CheckFormTapered():

bool CheckFormTapered(int li,double base){
   // цикл от 1, первый отрезок не проверяется,
   // но все последующие отрезки проверяются относительно него 
   for(int i=1;i<N;i++){ 
      // вычисление индекса очередной верхней точки паттерна 
      int j=li+1+i*2;
      // величина очередного отрезка 
      double lv=MathAbs(PeackTrough[j].Val-PeackTrough[j+1].Val);
      // величина предыдущего отрезка
      double lp=MathAbs(PeackTrough[j-2].Val-PeackTrough[j-1].Val);
      // предыдущий отрезок должен быть больше, если не так,
      // функция возвращает false   
      if(!(lp-lv>K2*base)){
         return(false);
      }
   } 
   return(true);
}

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

Функция CheckFormExpanding() аналогична, отличие только в одном условии:

if(!(lv-lp>K2*base)){
   return(false);
}

Для выполнения этого условия каждый последующий отрезок должен быть больше предыдущего.

Функция CheckFormRectangular():

bool CheckFormRectangular(int li,double base){   
   // цикл по всем верхним вершинам, кроме первой      
   for(int i=1;i<N;i++){
      // вычисление индекса очередной вершины 
      int j=li+1+i*2; 
      // вычисление размера очередного отрезка
      double lv=MathAbs(PeackTrough[j].Val-PeackTrough[j+1].Val);
      // отрезок не должен сильно отличаться от базовой величины 
      if(MathAbs(lv-base)>K2*base){
         return(false); 
      }
   }
   return(true);
}

В этой функции выполняется сравнение каждого из отрезков с базовой величиной. Если отличие значительное, функция возвращает false.

Если проверка формы прошла успешно, выполняется проверка наклона. Функция CheckInclineForBuy():

bool CheckInclineForBuy(int li,double base){                 
   switch(Incline){
      case InclineAlong:
         // наклон по ходу движения
         return(CheckInclineUp(li,base));
      break;
      case InclineHorizontally:
         // без наклона
         return(CheckInclineHorizontally(li,base));
      break;                     
      case InclineAgainst:
         // наклон против движения
         return(CheckInclineDn(li,base));
      break;
   } 
   return(true);
}  

Функция проверки наклона для продажи отличается только двумя строками:

bool CheckInclineForSell(int li,double base){                 
   switch(Incline){
      case InclineAlong:
         // наклон по ходу движения
         return(CheckInclineDn(li,base));
      break;
      case InclineHorizontally:
         // без наклона
         return(CheckInclineHorizontally(li,base));
      break;                     
      case InclineAgainst:
         // наклон против движения
         return(CheckInclineUp(li,base));
      break;
   } 
   return(true);
} 

Для покупки в случае Incline, равном InclineAlong (по ходу движения) вызывается функция CheckInclineUp(), а для продажи CheckInclineDn(). В случае  Incline, равном InclineAgainst (против хода движения) — наоборот.

Функция проверки наклона паттерна вверх — CheckInclineUp():

bool CheckInclineUp(int li,double base){   
   // цикл по всем верхним вершинам, кроме первой      
   for(int v=1;v<N;v++){
      // вычисление индекса очередной верхней вершины
      int vi=li+1+v*2;
      // вычисление середины очередного отрезка зигзага
      double mc=(PeackTrough[vi].Val+PeackTrough[vi+1].Val)/2;
      // вычисление середины предыдущего отрезка зигзага
      double mp=(PeackTrough[vi-2].Val+PeackTrough[vi-1].Val)/2;
      // очередной отрезок должен быть выше предыдущего
      if(!(mc>mp+base*K3)){
         return(false);
      }
   }
   return(true);
} 

В функции выполнятся проход по все отрезкам зигзага, для каждого из них вычисляется значение его середины и сравнивается с серединой предыдущего отрезка. Каждый отрезок должен быть выше предыдущего на величину base*K3.

Функция проверки наклона паттерна вниз — CheckInclineDn() отличается только одним условием:

if(!(mc<mp-base*K3)){
   return(false);
}

Для выполнения этого условия каждый отрезок должен располагаться ниже предыдущего.

Функция CheckInclineHorizontally():

bool CheckInclineHorizontally(int li,double base){ 
   // середина базового отрезка
   double mb=(PeackTrough[li+1].Val+PeackTrough[li+2].Val)/2;        
   for(int v=1;v<N;v++){
      // индекс очередной верхней точки
      int vi=li+1+v*2;
      // середина очередного отрезка
      double mc=(PeackTrough[vi].Val+PeackTrough[vi+1].Val)/2;
      // середина очередного отрезка не должна сильно отклоняться 
      // от середины базового отрезка
      if(MathAbs(mc-mb)>base*K3){
         return(false);
      }
   }                  
   return(true);
}

Если проверки условий по форме и наклону пройдены, выполняется следующий участок кода:

if(CompletionType==Immediately){                    // вход сразу
   UpArrowBuffer[i]=low[i];
   UpDotBuffer[i]=PeackTrough[CurCount-1].Val+l1;
}
else{                                               // ждать пробоя уровня
   SetLevelParameters(1);
   SetTarget(1,li);
}

При варианте завершения паттерна Immediately индикатор сразу рисует стрелку и ставит точку с целью, в остальных случаях выполняется установка уровня пробоя функцией SetLevelParameters() и установка цели функцией SetTarget().

Функция SetLevelParameters():

void SetLevelParameters(int dir){
   CurLevel.dir=dir;   
   switch(CompletionType){
      case OneLastVertex:                            // по одной точке
          CurLevel.v=PeackTrough[CurCount-3].Val;
      break;
      case TwoLastVertices:                          // по двум точкам
         CurLevel.x1=PeackTrough[CurCount-5].Bar;
         CurLevel.y1=PeackTrough[CurCount-5].Val;
         CurLevel.x2=PeackTrough[CurCount-3].Bar;
         CurLevel.y2=PeackTrough[CurCount-3].Val;
      break;
   }
} 

В функции SetLevelParameters() для сохранения параметров уровня используется структура SLevelParameters:

struct SLevelParameters{
   int x1;
   double y1;
   int x2;
   double y2;       // от x1 до y2 - параметры наклонного уровня
   double v;        // величина горизонтального уровня
   int dir;         // направления
   double target;   // цель
   // метод для расчета значения наклонного уровня
   double y3(int x3){
      if(CompletionType==TwoLastVertices){
            return(y1+(x3-x1)*(y2-y1)/(x2-x1));
      }
      else{
         return(v);
      }
   }
   // метод для инициализации или сброса параметров
   void Init(){
      x1=0;
      y1=0;
      x2=0;
      y2=0;
      v=0;
      dir=0;   
   }
};

Структура содержит поля для параметров линии: x1, y1, x2, y2; поле v для значения горизонтального уровня; d - направление паттерна; target - цель. Цель может быть задана как непосредственно ценовым уровнем (при варианте FromVertexToVertex), так и величиной от уровня пробоя (для вариантов OneVertex и TwoVertices). Метод y3() используется для расчета значения наклонного уровня. Метод Init() используется для инициализации или сброса значений.

При выполнении всех условий по формированию паттерна вызывается функция SetLevelParameter(), в этой функции, в зависимости от выбранного типа уровня (горизонтальный или наклонный) устанавливаются параметры наклонного уровня (поля x1, y1, x2, y2) или одно значение горизонтального уровня - v. В методе y3() выполняется вычисление значения уровня с использованием полей x1, y1, x2, y2 или же возвращается значение поля v.

В индикаторе объявлено две переменных типа SLevelParameters:

SLevelParameters CurLevel;
SLevelParameters PreLevel;

Эта пара переменных используется аналогично парам переменных CurCount-PreCount и CurDir-PreDir - перед начальным расчетом индикатора выполняется сброс значений переменных (участок кода расположен в самом начале функции OnTick()):

int start;

if(prev_calculated==0){           // первый расчет по всем барам
   start=1;      
   CurCount=0;
   PreCount=0;
   CurDir=0;
   PreDir=0;  
   CurLevel.Init();    
   CurLevel.Init();
   LastTime=0;
}
else{                           // расчет новых баров и формирующегося бара 
   start=prev_calculated-1;
}

При обсчете каждого бара выполняется перемещение значений в этих переменных (код располагается в начале индикаторного цикла):

if(time[i]>LastTime){
   // первый обсчет нового бара
   LastTime=time[i];
   PreCount=CurCount;
   PreDir=CurDir;
   PreLevel=CurLevel;
}
else{
   // повторный обсчет бара
   CurCount=PreCount;
   CurDir=PreDir;
   CurLevel=PreLevel;
}

Вызовом функции SetTarget() выполняется установка параметров цели:

void SetTarget(int dir,int li){
   switch(Target){
      case FromVertexToVertex:
         // вариант "от вершины до вершины"
         if(dir==1){
            CurLevel.target=PeackTrough[CurCount-1].Val+(PeackTrough[li+1].Val-PeackTrough[li].Val);
         }
         else if(dir==-1){
            CurLevel.target=PeackTrough[CurCount-1].Val-(PeackTrough[li].Val-PeackTrough[li+1].Val);
         }
      break;
      case OneVertex:
         // по одной вершине
         CurLevel.target=MathAbs(PeackTrough[li].Val-PeackTrough[li+2].Val);
      break;
      case TwoVertices:
         // по двум вершинам
         SetTwoVerticesTarget(dir,li);
      break;
   }
}

Для варианта FromVertexToVertex выполняется расчет ценового значения. Для варианта OneVertex полю target присваивается величина хода цены от уровня пробоя до цели. Расчет для варианта SetTwoVerticesTarget выполняется в функции SetTwoVerticesTarget():

void SetTwoVerticesTarget(int dir,int li){
   // координаты начальной (длинной) линии
   // паттерна - от низины до вершины  
   double x11=PeackTrough[li].Bar;
   double y11=PeackTrough[li].Val;
   double x12=PeackTrough[li+1].Bar;
   double y12=PeackTrough[li+1].Val;
   // координаты линии по двум впадинам для покупки
   // или по двум вершинам для продажи
   double x21=PeackTrough[li+2].Bar;
   double y21=PeackTrough[li+2].Val;
   double x22=PeackTrough[li+4].Bar;
   double y22=PeackTrough[li+4].Val;
   // значение в точке пересечения линий
   double t=TwoLinesCrossY(x11,y11,x12,y12,x21,y21,x22,y22);
   // установка значения цели в зависимости от направления
   if(dir==1){
      CurLevel.target=t-PeackTrough[li].Val;
   }
   else if(dir==-1){
      CurLevel.target=PeackTrough[li].Val-t;         
   }
}

Для варианта SetTwoVerticesTarget поле target получает значения хода цены от пробиваемого уровня до цели, как и для варианта OneVertex.

Рассмотрим, как выполняется слежение за ценой и уровнем (CompletionType не равно Immediately):

// используется уровень
if(CompletionType!=Immediately){
   // есть изменения зигзага
   if(PeackTrough[CurCount-1].Bar==i){
      if(CurLevel.dir==1){                // ждем пробоя вверх
         // в переменную cl получаем значение уровня
         double cl=CurLevel.y3(i); 
         // зигзаг пробивает уровень
         if(PeackTrough[CurCount-1].Val>cl){
            // установка стрелки вверх
            UpArrowBuffer[i]=low[i];
            // установка точки цели
            if(Target==FromVertexToVertex){
               // в поле target цена
               UpDotBuffer[i]=CurLevel.target;                        
            }
            else{
               // в поле target дистанция от уровня
               UpDotBuffer[i]=cl+CurLevel.target;
            }
            // обнуление поля dir для прекращения отслеживания уровня
            CurLevel.dir=0;
         }
      }
      else if(CurLevel.dir==-1){         // ждем пробоя вниз
         // в переменную cl получаем значение уровня
         double cl=CurLevel.y3(i);
         // зигзаг пробивает уровень
         if(PeackTrough[CurCount-1].Val<cl){
            // установка стрелки вниз
            DnArrowBuffer[i]=low[i];
            // установка точки цели
            if(Target==FromVertexToVertex){
               // в поле target цена
               DnDotBuffer[i]=CurLevel.target;
            }
            else{                     
               // в поле target дистанция от уровня
               DnDotBuffer[i]=cl-CurLevel.target;
            }
            // обнуление поля dir для прекращения отслеживания уровня
            CurLevel.dir=0;
         }         
      }         
   }
} 

Эта проверка выполняется при каждом изменении зигзага. Все вершины зигзага сохраняются в массиве PeackTrough, изменение в зигзаге определяется по соответствию индекса последней точки зигзага индексу текущего бара:

if(PeackTrough[CurCount-1].Bar==i){

При помощи метода y3() вычисляется текущее значение уровня:

double cl=CurLevel.y3(i); 

Выполняется проверка, не пробил ли последний отрезок зигзага этот уровень:

if(PeackTrough[CurCount-1].Val>cl){

Если уровень пробит, индикатор рисует стрелку и ставит точку цели. Поле target может содержать ценовое значение цели, в этом случае значение используется непосредственно, а может содержать величину хода цены до цели, в этом случае значение цели вычисляется с учетом текущего значения уровня: 

if(Target==FromVertexToVertex){
   UpDotBuffer[i]=CurLevel.target;                        
}
else{
   UpDotBuffer[i]=cl+CurLevel.target;
}

В конце поле dir обнуляется, чтобы больше не следить за ценой до появления следующего паттерна:

CurLevel.dir=0;

На этом создание индикатора завершено, несколько фрагментов его работы показаны на рис. 15.


Рис. 15. Несколько сигналов индикатора iHorizontalFormation

Дополнительно в индикатор была добавлена функция алерта. Полностью готовый индикатор можно найти в приложении к статье, имя файла — iHorizontalFormation.


Универсальный индикатор поиска Флага, Вымпела и Клина

Теперь создадим индикатор для поиска паттернов второй категории. Они образуют свою форму, заполняя площадь фигуры барами. Начинается паттерн с сильного движения цены, в данном случае — с длинного бара. Для определения длинных баров используем индикатор ATR с большим периодом. Бар будет считаться длинным, если размер его тени превышает значение ATR, умноженное на коэффициент. Значит, в индикаторе потребуются внешние параметры для периода ATR и коэффициент:

input int                  ATRPeriod            =  50;
input double               K1                   =  3;

Объявим глобальную переменную индикатора для хэндла ATR:

int h;

В функции OnInit() загрузим индикатор ATR и получим значение хэндла:

h=iATR(Symbol(),Period(),ATRPeriod);
if(h==INVALID_HANDLE){
   Alert("Error load indicator");
   return(INIT_FAILED);
}

В главном индикаторном цикле получим значение ATR:

double atr[1];
if(CopyBuffer(h,0,rates_total-i-1,1,atr)==-1){
   return(0);
}

Используя полученное значение ATR, проверим величину бара. Если размер тени бара превышает пороговое значение, задаваемое коэффициентом, то будем определять направление предполагаемого движения цены. Направление определяется по цвету бара (по значениям цен open и close). Если цена close выше цены open, предполагается дальнейшее движение цены вверх. Если цена close ниже цены open, предполагается движение вниз:

if(high[i]-low[i]>atr[0]*K1){    // длинный бар
   if(close[i]>open[i]){         // бар направлен вверх
      Cur.Whait=1;
      Cur.Count=0;
      Cur.Bar=i;
   }
   else if(close[i]<open[i]){    // бар направлен вниз
      Cur.Whait=-1;   
      Cur.Count=0;
      Cur.Bar=i;
   }
}

При выполнении условий по размеру бара и его направлению полям структуры Cur устанавливаются соответствующие значения: в поле Whait указывается предполагаемое направление (1 — вверх, -1 — вниз), выполняется сброс поля Count. Ему присваивается значение 0, это поле будет использоваться для подсчета количества баров паттерна. В поле Bar сохраняется индекс начального (длинного) бара паттерна.

Рассмотрим подробнее структуру Cur. Всего у структуры три поля и метод Init() для быстрого обнуления всех полей:

struct SCurPre{
   int Whait;
   int Count;
   int Bar;
   void Init(){
      Whait=0;
      Count=0;
      Bar=0;
   }
};

В начале функции OnTick() объявлены две статических переменных данного типа и переменная типа datetime:

static datetime LastTime=0;   
static SCurPre Cur;          
static SCurPre Pre;

Затем выполняется расчет индекса первого бара, с которого начинается обсчет индикатора и выполняется инициализация переменных Cur и Pre:

int start=0;

if(prev_calculated==0){           // первый расчет индикатора
   
   start=1;      
   
   Cur.Init();
   Pre.Init();             
     
   LastTime=0;      
}
else{                             // расчет новых баров и формирующегося бара
   start=prev_calculated-1;
}

В начале главного индикаторного цикла выполняется перемещение значений в переменных Cur и Pre:

if(time[i]>LastTime){       // первый обсчет бара
   LastTime=time[i];
   Pre=Cur;              
}
else{                      // повторный обсчет бара
   Cur=Pre;
}  

Этот прием с переменными подробно рассмотрен в статье "Волны Вульфа" (переменные PreCount и СurCount). В этой статье он использовался при создании индикатора iHorizontalFormation (переменные с префиксами Cur и Pre).

Если переменная Cur.Count не равна нулю, значит, индикатор находится в режиме уточнения условия определения паттерна. При этом выполняется подсчет количества баров, составляющих паттерн — увеличивается переменная CurCount. Первый бар после длинного бара пропускается, а начиная с третьего бара, выполняются уточняющие проверки:

if(Cur.Whait!=0){
   Cur.Count++;            // подсчет количества баров
   if(Cur.Count>=3){
      // уточняющие проверки

   }
}

Основным показателем уточняющих проверок будет величина перекрытия баров (рис. 16).

Рис. 16. Перекрытие двух баров L определяется как разница между
минимальной ценой high и максимальной ценой low

Величина перекрытия вычисляется с использованием двух баров как разница между наименьшим максимумом и наибольшим минимумом: 

Overlapping=MathMin(high[i],high[i-1])-MathMax(low[i],low[i-1]);

Перекрытие с начальным баром не проверяется, поэтому выполнение проверок на перекрытие начинается с третьего бара, а не со второго.

Величина перекрытия двух баров должна превышать некоторое пороговое значение. Если задавать это значение в пунктах, работа индикатора будет сильно зависеть от таймфрейма, так как значение параметра будет очень сильно отличаться на разных таймфреймах. Чтобы не зависеть от таймфрейма, определим базовое значение по проверяемым барам — используем максимальную тень из двух баров:

double PreSize=MathMax(high[i-1]-low[i-1],high[i]-low[i]);

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

if(!(Overlapping>=PreSize*MinOverlapping))

Если два очередных бара не являются перекрытыми, значит, закончился ряд непрерывно расположенных перекрытых баров. В этом случае проверяем количество баров в ряду:

if(Cur.Count-2>=MinCount){
   // уточняющие проверки
}
Cur.Whait=0;

Если количество баров в ряду превышает значение переменной MinCount, то выполняются дополнительные уточняющие проверки, иначе прекращается ожидание формирования паттерна путем обнуления переменной CurCount. В вышеприведенном коде при проверке условия из переменной CurCount вычитается 2, это первый длинный бар и завершающий бар, на котором не выполняются условия перекрытия.

Переменные MinOverlapping и MinCount — внешние переменные индикатора:

input double               MinOverlapping       =  0.4;
input int                  MinCount             =  5;

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

double AverSize,AverBias,AverSizeDif;
PatternParameters(high,low,i-1,Cur.Count-2,AverSize,AverBias,AverSizeDif);

Параметры определяются в функции PatternParameters() и возвращаются по ссылкам в переменных AverSize, AverBias, AverSizeDif. В переменной AverSize возвращается средний размер бара, в переменной AverBias — среднее смещение центра баров, в AverSizeDif — средняя разница размеров двух соседних баров. Чтобы точно понять, как вычисляются эти параметры, разберем подробно функцию PatternParameters():

void PatternParameters( const double & high[],
                        const double & low[],
                        int i,
                        int CurCnt,
                        double & AverSize,
                        double & AverBias,
                        double & AverSizeDif
){
            
   // средний размер бара            
   AverSize=high[i-CurCnt]-low[i-CurCnt];
   // среднее смещение бара
   AverBias=0;
   // средняя разница размеров двух соседних баров
   AverSizeDif=0;
   
   for(int k=i-CurCnt+1;k<i;k++){      // по всем барам ряда кроме первого
      // средний размер
      AverSize+=high[k]-low[k];
      // среднее смещение
      double mc=(high[k]+low[k])/2;
      double mp=(high[k-1]+low[k-1])/2;
      AverBias+=(mc-mp);
      // средняя разница размеров
      double sc=(high[k]-low[k]);
      double sp=(high[k-1]-low[k-1]);
      AverSizeDif+=(sc-sp);               
      
   }
   
   // деление сумм на количества
   AverSize/=CurCnt;
   AverBias/=(CurCnt-1);
   AverSizeDif/=(CurCnt-1); 
} 

В функцию передаются два массива: high и low, индекс бара, на котором ряд перекрытых баров заканчивается, длина ряда баров и три переменные для возвращаемых значений. Подсчет показателей выполняется в цикле for, но поскольку показатели AverBias и AverDiff рассчитываются по двум соседним барам, первый бар ряда пропускается:

for(int k=i-CurCnt+1;k<i;k++)

Поэтому перед циклом переменные  AverBias и AverDiff обнуляются, а переменной AverSize присваивается значение, рассчитанное по пропущенному в цикле бару.

В цикле к переменной AverSize прибавляются размеры баров:

AverSize+=high[k]-low[k];

Для показателя AverBias (смещения) рассчитываются средние точки баров и их разница, полученная разница суммируется:

double mc=(high[k]+low[k])/2;
double mp=(high[k-1]+low[k-1])/2;
AverBias+=(mc-mp);

Для показателя AverSizeDif вычисляются размеры двух соседних баров и их разница, полученная разница суммируется:

double sc=(high[k]-low[k]);
double sp=(high[k-1]-low[k-1]);
AverSizeDif+=(sc-sp);    

После цикла все сумы делятся на количество просуммированных значений:

AverSize/=CurCnt;
AverBias/=(CurCnt-1);
AverSizeDif/=(CurCnt-1); 

После расчета параметров проверяется форма паттерна. Эта проверка не зависит от направления предполагаемого движения цены. Для проверки формы используются три функции: FormTapered() — сужающаяся форма (Вымпел), FormHorizontal() — прямоугольная форма (Флаг), FormExpanding() — расширяющаяся форма (Клин):

if(   FormTapered(AverSizeDif,AverSize) ||
      FormHorizontal(AverSizeDif,AverSize) ||
      FormExpanding(AverSizeDif,AverSize)
){ 
   // проверки направления
}

У индикатора iHorizontalFormation в настройках можно было выбрать только одну форму из трех, здесь же все три формы включаются независимо. Это связано с более редким выполнением условий и, соответственно, с более редкими торговыми сигналами. Для включения/выключения каждой из форм в параметры индикатора добавлено три переменных. Кроме того, для каждой из форм в окно свойств добавлено по коэффициенту:

input bool                 FormTapered          =  true;
input double               FormTaperedK         =  0.05;
input bool                 FormRectangular      =  true;
input double               FormRectangularK     =  0.33;
input bool                 FormExpanding        =  true;
input double               FormExpandingK       =  0.05;

 Разберем функции проверки формы. Функция FormTapered():

bool FormTapered(double AverDif, double AverSize){
   return(FormTapered && AverDif<-FormTaperedK*AverSize);
}

Если средняя разница размеров баров меньше отрицательного порогового значения, значит, размеры баров снижаются, что соответствует заостренной форме паттерна:

Функция FormHorizontal():

bool FormHorizontal(double AverDif, double AverSize){
   return(FormRectangular && MathAbs(AverDif)<FormRectangularK*AverSize);
}

Если абсолютное значение средней разницы размеров баров меньше порогового значения, значит, все бары имеют примерно одинаковый размер, что соответствует прямоугольной форме:

Функция FormExpanding():

bool FormExpanding(double AverDif, double AverSize){
   return(FormExpanding && AverDif>FormExpandingK*AverSize);
}

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

Если проверка формы пройдена, проводится проверка наклона паттерна. Эта проверка зависит от предполагаемого направления движения цены. Для направления вверх используется функция CheckInclineForBuy(), для направления вниз — CheckInclineForSell():

if(Cur.Whait==1){
   if(CheckInclineForBuy(AverBias/AverSize)){
      // дополнительные проверки для направления вверх

   }
}
else if(Cur.Whait==-1){
   if(CheckInclineForSell(AverBias/AverSize)){   
      // дополнительные проверки для направления вниз

   }
}

Варианты проверки наклона, как и варианты проверки формы, включаются раздельно. Для этого в окне свойств имеются соответствующие переменные. Также для каждого варианта наклона в окне свойств есть свой коэффициент:

input bool                 InclineAlong         =  true;
input double               InclineAlongK        =  0.1;
input bool                 InclineHorizontal    =  true;
input double               InclineHorizontalK   =  0.1;
input bool                 InclineAgainst       =  true;
input double               InclineAgainstK      =  0.1;

Функция CheckInclineForBuy():

bool CheckInclineForBuy(double Val){
   return(  (InclineAlong && Val>InclineAlongK) || 
            (InclineHorizontal && MathAbs(Val)<InclineHorizontalK) || 
            (InclineAgainst && Val<-InclineAgainstK)
   );
}   

В функцию передается значение относительного смещения баров AverBias/AverSize. Если оно выше положительного порога, значит, паттерн наклонен вверх, если ниже отрицательного —  значит, наклон вниз. Если же значение находится в пределах порогового значения без учета знака, значит, паттерн расположен горизонтально:

bool CheckInclineForBuy(double Val){
   return(  (InclineAlong && Val>InclineAlongK) || 
            (InclineHorizontal && MathAbs(Val)<InclineHorizontalK) || 
            (InclineAgainst && Val<-InclineAgainstK)
   );
}   

Аналогично для направления вниз:

bool CheckInclineForSell(double Val){
   return(  (InclineAlong && Val<-InclineAlongK) || 
            (InclineHorizontal && MathAbs(Val)<InclineHorizontalK) || 
            (InclineAgainst && Val>InclineAgainstK)
   );
}  

Только теперь направлению по ходу движения соответствует наклон вниз, а против хода движения — вверх.

Остается выполнить последнюю проверку — направление завершающего бара. Может быть два варианта последней проверки: завершающий бар направлен либо по направлению паттерна, либо против него. Для включения вариантов завершающей проверки в окно свойств добавлены параметры:

input bool                 EnterAlong           =  true;
input bool                 EnterAgainst         =  true;

Проверка для направления вверх выполняется следующим образом:

if((EnterAlong && close[i]>open[i]) || (EnterAgainst && close[i]<open[i])){
   Label1Buffer[i]=low[i];
   Label3Buffer[i]=close[i]+(high[Cur.Bar]-low[Cur.Bar]);
}

Если выбран вариант EnterAlong и бар направлен вверх или выбран вариант EnterAgainst и бар направлен вниз, то индикатор рисует стрелку и точку цели. Цель находится на дистанции, равной величине начального большого бара.

Аналогично для направления вниз:

if((EnterAlong && close[i]<open[i]) || (EnterAgainst && close[i]>open[i])){
   Label2Buffer[i]=high[i];                  
   Label4Buffer[i]=close[i]-(high[Cur.Bar]-low[Cur.Bar]);
}

На этом индикатор можно считать полностью готовым. Индикатор с функцией алерта можно найти в приложении к статье, имя файла — iFlag. 


Индикатор-тестер

Самый простой и удобный способ протестировать эффективность индикатора — это эксперт и тестер стратегий в терминале. В статье "Волны Вульфа" создавался простой эксперт, который после незначительной доработки можно использовать и для тестирования индикаторов, созданных в этой статье. Индексация буферов у индикаторов iHorizontalFormation и iFlag соответствует индексации буферов у индикатора iWolfeWaves, поэтому достаточно изменить внешние параметры эксперта и вызов функции iCustom().

Есть и другой очень интересный способ тестирования индикаторов, который позволяет оценивать их эффективность на лету — индикатор-тестер. В дополнительном индикаторе моделируется торговля по стрелкам основного индикатора, а на графике отображаются линии эквити и баланса.

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

Другой вариант — сделать так, чтобы индикатор-тестер рисовал на графике стрелки графическими объектами.

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

Но есть и четвертый вариант, не менее удобный, чем третий, но более простой с точки зрения реализации. К тому же возможно создать один универсальный индикатор-тестер, который можно использовать как с индикатором iHorizontalFormation, так и с индикатором iFlag, после их незначительной доработки.

Доработка iHorizontalFormation и iFlag заключается в создании внешней переменной ID: 

input int                  ID             =  1;

Затем в функции OnInit, используя значение этой переменной, индикатору устанавливается короткое имя:

string ShortName=MQLInfoString(MQL_PROGRAM_NAME)+"-"+IntegerToString(ID);
IndicatorSetString(INDICATOR_SHORTNAME,ShortName);

Короткое имя представляет собой имя файла индикатора, к которому добавляется знак "-" и значение переменой ID. По этому имени индикатор-тестер будет находить основной индикатор (получать его хэндл).

Индикатор iHorizontalFormation работает на основе Зигзага, который может рассчитываться по ценам high-low, а также по ценам close и по другим индикаторам. При расчете по ценам high-low появившаяся стрелка уже не исчезнет с графика. То есть, при использовании индикатора для торговли за ним можно следить на формирующемся баре. В остальных случаях при расчете Зигзага по ценам close и другим индикаторам надо следить за появлением стрелки на сформированном баре. Значит, индикатор-тестер надо будет каким-то образом уведомить, на каком баре надо следить за появлением стрелок.

Индикаторы iHorizontalFormation и iFlag рисуют точки-цели, которые можно использовать для установки тейк-профита. Однако для индикатора iHorizontalFormation это возможно только в том случае, когда Зигзаг рассчитывается по цене. Значит, индикатору-тестеру надо указать, использовать ему точки-цели или дополнительные параметры тейк-пфрофита и стоп-лосса. Первая возникающая идея — использовать для передачи данных глобальную переменную. Однако терминал MetaTrader 5 имеет одну особенность: при смене параметров индикатора выполняется загрузка нового экземпляра индикатора с новым хэндлом, но предыдущий экземпляр не выгружается из памяти сразу. Поэтому, если вернуть внешние параметры индикатора, то новая загрузка и расчет индикатора не будет выполняться, а значит, не будет выполнена функция OnInit() и не обнулится переменная prev_calculated. Таким образом, глобальные переменные не получат нового значения. 

Необходимые индикатору-тестеру параметры будут передаваться через индикаторный буфер. Для этого достаточно одного элемента буфера. Используем имеющийся — самый первый и его первый (крайний слева) элемент. Всего необходимо передать два значения. Одно из них определяет, нужно ли следить за основным индикатором на сформированном баре или же можно использовать формирующийся, а второе — можно ли использовать точку-цель для тейкпрофита позиции. В функцию OnCalculate() при prev_calculate=0 добавляется код:

int ForTester=0;       // переменная для значения
if(!(SrcSelect==Src_HighLow)){
   // работа по сформированному бару
   ForTester+=10;
}   
if(!(SrcSelect==Src_HighLow || SrcSelect==Src_Close)){
   // можно использовать точку-цель
   ForTester+=1;
}     
UpArrowBuffer[0]=ForTester;  

Через один элемент массива надо передать два числа, не превышающие 10, поэтому одно из них умножается на 10, а к нему прибавляется второе число. Поскольку передаваемые числа могут иметь значения только 0 или 1, можно было бы использовать число с основанием 2, но объем передаваемых данных незначителен, поэтому здесь не обязательно экономить байты.

Подобную доработку надо выполнить и с индикатором iFlag. В функции OnInit():

string ShortName=MQLInfoString(MQL_PROGRAM_NAME)+"-"+IntegerToString(ID);
IndicatorSetString(INDICATOR_SHORTNAME,ShortName);

В функции OnCalculate():

Label1Buffer[0]=11;

Индикатор iFlag нужно всегда смотреть на сформированном баре, и всегда можно использовать точку-цель. Поэтому присваивается значение 11 без вычислений.

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

Теперь займемся исключительно индикатором-тестером. Создаем новый индикатор с именем iTester, добавляем внешние параметры:

input int                  ID             =  1;
input double               StopLoss_K     =  1;
input bool                 FixedSLTP      =  false;
input int                  StopLoss       =  50;
input int                  TakeProfit     =  50;

Здесь:

  • ID — идентификатор основного индикатора
  • StopLoss_K — коэффициент расчета стоп-лосса относительно тейк-профита при использовании точки-цели
  • FixedSLTP — использовать переменные StopLoss и TakeProfit или же использовать точку-цели и переменную StopLoss_K.

Будет очень удобно, если индикатор-тестер в левом верхнем углу будет отображать не просто свое имя, а имя индикатора, по стрелкам которого он работает. Но пока тестируемый индикатор еще не прикреплен к графику, будет отображаться имя самого индикатора-тестера. Для этого объявим глобальную переменную:

string IndName;

В функции OnInit() присвоим ей имя индикатора-тестера:

IndName=MQLInfoString(MQL_PROGRAM_NAME);

Информация о каждой открытой индикатором-тестером позиции сохраняется в массиве структур SPos, переменная PosCnt используется для учета количества открытых позиций:

struct SPos{
   int dir;
   double price;
   double sl;
   double tp;
   datetime time; 
};
SPos Pos[];
int PosCnt;

Сделка должна добавляться в массив только один раз, для этого используется переменная:

datetime LastPosTime;

При добавлении позиции в массив выполняется проверка времени из переменной LastPosTime и времени бара, на котором открывается позиция. Если позиция добавляется, то переменной LastPosTime присваивается новое время. Если же время бара равно времени LastPosTime, значит, позиция уже открыта.

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

int Closed;
datetime CloseTime;

Переменной Closed присваивается прибыль от закрытых на одном баре позиций, а переменной CloseTime — время этого бара. Ниже рассмотрим подробнее, как это работает.

Все вспомогательные переменные и функция OnInit() рассмотрены, переходим к функции OnCalculate(). Сначала объявляется несколько вспомогательных переменных:

string name;
static int last_handle=-1;
static int shift=0;
static int use_target=0;
int handle=-1;     
int start=2;  

Опишем переменные:

  • name будет использоваться для получения имени индикатора функцией ChartIndicatorName();
  • статическая переменная last_handle — для сохранения хэндла тестируемого индикатора;
  • статические shift и use_target — для параметров, переданных из тестируемого индикатора;
  • handle — для получения хэндла функцией ChartIndicatorGet();
  • start — для начала обсчета индикатора.

Разберем код для поиска тестируемого индикатора. Сначала определяем количество индикаторов, прикрепленных на график цены:

int it=ChartIndicatorsTotal(0,0);

Проходим по ним в цикле:

for(int i=0;i<it;i++){        // по всем индикаторам графика
   // получение имени очередного индикатора
   name=ChartIndicatorName(0,0,i);
   // поиск подстроки "-"
   int p=StringFindRev(name,"-");
   if(p!=-1){
      // подстрока найдена, проверяется значение идентификатора
      if(StringSubstr(name,p+1,StringLen(name)-p-1)==IntegerToString(ID)){
         // идентификатор соответствует, получаем хэндл
         handle=ChartIndicatorGet(0,0,name);
      }
   }
} 

Рассмотрим приведенный выше участок кода подробней. Переменой name присваивается имя индикатора, полученного функцией ChartIndicatorName() — эта функция возвращает имя индикатора по его индексу. Полученное имя проверяется на соответствие идентификатору. Для этого выполняется поиск последнего вхождения подстроки "-". Если знак "-" найден, извлекается часть строки после него. Если она соответствует идентификатору, переменной handle присваивается значение хэндла, полученное функцией ChartIndicatorGet() — эта функция возвращает хэндл по имени индикатора. 

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

if(handle!=last_handle){
   if(handle==-1){                    // нет хэндла
      // установка исходного имени
      IndicatorSetString(INDICATOR_SHORTNAME,IndName);
      ChartRedraw(0);
      return(0);
   }
   // проверка, завершен ли расчет тестируемого индикатора
   int bc=BarsCalculated(handle);
   if(bc<=0)return(0);                // если не завершен, работа функции прерывается
   // копирование данных с параметрами тестирования
   double sh[1];
   if(CopyBuffer(handle,0,rates_total-1,1,sh)==-1){
      // при неудачном копировании работа функции прерывается до
      // следующего тика
      return(0);
   }
   // извлечение отдельных параметров
   shift=((int)sh[0])/10;              // сформированный или формирующийся бар
   use_target=((int)sh[0])%10;         // можно ли использовать точку-цель
   last_handle=handle;                 // сохранение значения хэндла
   // установка имени индикатора
   IndicatorSetString(INDICATOR_SHORTNAME,name);
   ChartRedraw(0);
}
else if(prev_calculated!=0){
   // если нет нового хэндла, выполняется расчет только новых баров 
   // и формирующегося бара
   start=prev_calculated-1;
}

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

Переменная start по умолчанию имеет значение 2. Если необходимо выполнить полный перерасчет индикатора (а это необходимо при смене хэндла или при значении prev_calculated, равном 0), в этом случае необходимо произвести сброс некоторых дополнительных переменных:

if(start==2){
   PosCnt=0;
   BalanceBuffer[1]=0;
   EquityBuffer[1]=0;
   LastPosTime=0;
   Closed=0;
   CloseTime=0;      
}

При сбросе обнуляется: количество открытых позиций — PosCnt, первые элементы индикаторных буферов для баланса и эквити — BalanceBuffer[1] и EquityBuffer[1], время последней позиции — LastPosTime, прибыль закрытых на одном баре позиций — Closed и время бара закрытия — ClosedTime.

Теперь разберем основной индикаторный цикл. Сначала приводится его весь код с комментариями, далее он рассматривается построчно:

for(int i=start;i<rates_total;i++){

   // перенос ранее известных значений баланса и эквити
   BalanceBuffer[i]=BalanceBuffer[i-1];
   EquityBuffer[i]=EquityBuffer[i-1];

   if(CloseTime!=time[i]){ 
      // начало обсчета нового бара
      Closed=0; // обнуление переменной для прибыли
      CloseTime=time[i];          
   }

   // получение данных тестируемого индикатора
   double buy[1],sell[1],buy_target[1],sell_target[1],enter[1];
   int ind=rates_total-i-1+shift;
   if(CopyBuffer(last_handle,0,ind,1,buy)==-1 || 
      CopyBuffer(last_handle,1,ind,1,sell)==-1 ||
      CopyBuffer(last_handle,2,ind,1,buy_target)==-1 || 
      CopyBuffer(last_handle,3,ind,1,sell_target)==-1        
   ){
      return(0);
   }
  
   if(shift==0){
      // если тестирование на формирующемся баре, получаем цену 
      // открытия из дополнительного буфера тестируемого индикатора       
      if(CopyBuffer(last_handle,4,ind,1,enter)==-1){
         return(0);
      } 
   }
   else{
      // при тестировании на сформированном баре используется цена 
      // открытия бара
      enter[0]=open[i];
   }

   // стрелка покупки
   if(buy[0]!=EMPTY_VALUE){
      AddPos(1,enter[0],buy_target[0],spread[i],time[i],use_target);      
   }
   // стрелка продажи
   if(sell[0]!=EMPTY_VALUE){
      AddPos(-1,enter[0],sell_target[0],spread[i],time[i],use_target);       
   }

   // проверка позиций на необходимость закрытия
   CheckClose(i,high,low,close,spread);

   // линия баланса
   BalanceBuffer[i]+=Closed;
   
   // линия эквити
   EquityBuffer[i]=BalanceBuffer[i]+SolveEquity(i,close,spread);

}

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

// перенос ранее известных значений баланса и эквити
BalanceBuffer[i]=BalanceBuffer[i-1];

При первом обсчете каждого бара выполняется обнуление переменной для прибыли закрываемых на этом баре позиций:

if(CloseTime!=time[i]){ 
   Closed=0;
   CloseTime=time[i];          
}

Копируются данные тестируемого индикатора:

// получение данных тестируемого индикатора
double buy[1],sell[1],buy_target[1],sell_target[1],enter[1];
int ind=rates_total-i-1+shift;
if(CopyBuffer(last_handle,0,ind,1,buy)==-1 || 
   CopyBuffer(last_handle,1,ind,1,sell)==-1 ||
   CopyBuffer(last_handle,2,ind,1,buy_target)==-1 || 
   CopyBuffer(last_handle,3,ind,1,sell_target)==-1        
){
   return(0);
}

В массивы buy и sell копируются данные буферов со стрелками, а в массивы  buy_target и sell_target — данные о точках-целях. Перед копированием вычисляется индекс бара ind с учетом переменной shift.

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

if(shift==0){
   // если тестирование на формирующемся баре, получаем цену 
   // открытия из дополнительного буфера тестируемого индикатора       
   if(CopyBuffer(last_handle,4,ind,1,enter)==-1){
      return(0);
   } 
}
else{
   // при тестировании на сформированном баре используется цена 
   // открытия бара
   enter[0]=open[i];
}

Если на обсчитываемом баре выявлены стрелки, выполняется открытие позиции вызовом функции AddPos():

// стрелка покупки
if(buy[0]!=EMPTY_VALUE){
   AddPos(1,enter[0],buy_target[0],spread[i],time[i],use_target);      
}
// стрелка продажи
if(sell[0]!=EMPTY_VALUE){
   AddPos(-1,enter[0],sell_target[0],spread[i],time[i],use_target);       
}

В функции CheckClose() выполняется проверка на необходимость закрытия позиций. В случае выполнения закрытия полученная прибыль будет находиться в переменной Closed:

// проверка позиций на необходимость закрытия
CheckClose(i,high,low,close,spread);

Закрытая прибыль из переменной Closed прибавляется к балансу:

// линия баланса
BalanceBuffer[i]+=Closed;

Эквити формируется из баланса и незакрытой прибыли, рассчитываемой функцией SolveEquity():

EquityBuffer[i]=BalanceBuffer[i]+SolveEquity(i,close,spread);

Рассмотрим функции AddPos(), CheckClose(), SolveEquity(), ниже приводится код каждой функции с подробными комментариями.  

Функция AddPos():

void AddPos(int dir, double price,double target,int spread,datetime time,bool use_target){

   if(time<=LastPosTime){
      // позиция со временем time уже добавлена
      return;
   }
   
   // в массиве нет свободного места
   if(PosCnt>=ArraySize(Pos)){
      // размер массива увеличивается блоком на 32 элемента
      ArrayResize(Pos,ArraySize(Pos)+32);
   }
   
   // сохраняется направление позиции
   Pos[PosCnt].dir=dir;
   // время открытия позиции
   Pos[PosCnt].time=time;
   // цена открытия позиции
   if(dir==1){
      // для покупки цена ask
      Pos[PosCnt].price=price+Point()*spread;  
   }
   else{
      // для продажи цена бид
      Pos[PosCnt].price=price;  
   }

   // вычисляется стоплосс и тейкпрофит
   if(use_target && !FixedSLTP){ 
      // используется точка-цель и вычисляется стоп-лосс
      if(dir==1){
         Pos[PosCnt].tp=target;
         Pos[PosCnt].sl=NormalizeDouble(Pos[PosCnt].price-StopLoss_K*(Pos[PosCnt].tp-Pos[PosCnt].price),Digits());
      }
      else{
         Pos[PosCnt].tp=target+Point()*spread;
         Pos[PosCnt].sl=NormalizeDouble(Pos[PosCnt].price+StopLoss_K*(Pos[PosCnt].price-Pos[PosCnt].tp),Digits());
      }   
   }
   else{
      // используются переменные StopLoss и TakeProfit
      if(dir==1){
         Pos[PosCnt].tp=Pos[PosCnt].price+Point()*TakeProfit;
         Pos[PosCnt].sl=Pos[PosCnt].price-Point()*StopLoss;
      }
      else{
         Pos[PosCnt].tp=Pos[PosCnt].price-Point()*TakeProfit;
         Pos[PosCnt].sl=Pos[PosCnt].price+Point()*StopLoss;
      }     
   }
   
   PosCnt++;
   
}  

Функция CheckClose():

void CheckClose(int i,const double & high[],const double & low[],const double & close[],const int & spread[]){
   for(int j=PosCnt-1;j>=0;j--){                                       // по всем позициям
      bool closed=false;                                               // значение false означает, что позиция пока открыта  
      if(Pos[j].dir==1){                                               // покупка
         if(low[i]<=Pos[j].sl){                                        // цена ниже или равна стоп-лоссу
            // прибыль в пунктах
            Closed+=(int)((Pos[j].sl-Pos[j].price)/Point());
            closed=true;                                               // позиция с индексом j закрыта
         }
         else if(high[i]>=Pos[j].tp){                                  // достигнут тейк-профит
            // прибыль в пунктах
            Closed+=(int)((Pos[j].tp-Pos[j].price)/Point());    
            closed=true;                                               // позиция с индексом j закрыта        
         }
      }
      else{ // продажа
         if(high[i]+Point()*spread[i]>=Pos[j].sl){                     // достигнут стоп-лосс
            // прибыль в пунктах
            Closed+=(int)((Pos[j].price-Pos[j].sl)/Point());
            closed=true;                                               // позиция с индексом j закрыта
         }
         else if(low[i]+Point()*spread[i]<=Pos[j].tp){                 // цена ниже или равна тейк-профиту
            // прибыль в пунктах
            Closed+=(int)((Pos[j].price-Pos[j].tp)/Point());              
            closed=true;                                               // позиция с индексом j закрыта
         }         
      }
      // позиция закрыта, надо удалить ее из массива
      if(closed){ 
         int ccnt=PosCnt-j-1;
         if(ccnt>0){
            ArrayCopy(Pos,Pos,j,j+1,ccnt);
         }
         PosCnt--;
      }
   }
}

В функции CheckClose() выполняется проход по всем позициям, сохраненным в массиве Pos, и сравниваются значения их стоп-лоссов и тейк-профитов с текущими ценами high или low. Если позиция закрывается, ее прибыль в пунктах прибавляется к переменой Closed, после этого позиция удаляется из массива.

Функция SolveEquity():

int SolveEquity(int i,const double & close[],const int & spread[]){
   int rv=0;                                // переменная для результата
   for(int j=PosCnt-1;j>=0;j--){            // по всем позициям
      if(Pos[j].dir==1){                    // покупка
                                            // прибыль
         rv+=(int)((close[i]-Pos[j].price)/Point());
      }
      else{                                // продажа
         // прибыль
         rv+=(int)((Pos[j].price+Point()*spread[i]-close[i])/Point());         
      }
   }
   return(rv);
}  

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

Мы завершили рассмотрение индикатора iTester. Готовый индикатор можно найти в приложении к статье под именем iTester.  На рис. 17 показан график с индикаторами iHorizontalFormation (стрелки) и iTester в подокне. Зеленая линия — эквити, красная — баланс.


Рис. 17. Индикатор iTester (в подокне), работающий по индикатору iHorizontalFormation (стрелки на графике)


Заключение

Рассмотренные в статье методы определения паттернов вполне приемлемо решают возложенную на них задачу: на графике отчетливо видны выявляемые индикаторами различные формы: флаги, вымпелы, треугольники, клинья. Рассмотренные методы не следует считать единственно возможными и абсолютно правильными. Можно придумать и другие способы выявления тех же самых паттернов. Например, можно использовать линейную регрессию — выполнить отдельные расчеты по ценам high и low, затем проверять наклон и схождение/расхождение этих линий. Еще больше идей может возникнуть по решению отдельных подзадач, составляющих общую задачу поиска паттернов. Как бы то ни было, методы анализа цен, рассмотренные при создании индикаторов в этой статье, могут оказаться полезны и для решения других задач технического анализа. 


Файлы приложения

Все индикаторы, созданные в статье, находятся в приложении:

  • iHorizontalFormation
  • iFlag
  • iTester

Прикрепленные файлы |
files.zip (9.87 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (6)
Viktar Dzemikhau
Viktar Dzemikhau | 28 авг. 2018 в 20:21
Реter Konow:

Очень хорошая статья.


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


Спасибо за серьезную работу.

Полностью согласен. Очень полезная статья.

[Удален] | 22 авг. 2021 в 22:44
Здесь тоже есть паттерны, и флаг ,и клин и ещё много чего
Dmitry Fedoseev
Dmitry Fedoseev | 23 авг. 2021 в 11:32
Vladimir Baskakov:
Здесь тоже есть паттерны, и флаг ,и клин и ещё много чего

А у курицы крылья есть.

[Удален] | 23 авг. 2021 в 15:58
Dmitry Fedoseev:

А у курицы крылья есть.

Получается, что вводишь людей в заблуждение.
Dmitry Fedoseev
Dmitry Fedoseev | 23 авг. 2021 в 16:35
Vladimir Baskakov:
Получается, что вводишь людей в заблуждение.

Получается, что в голове твоей опилки.

Walk-Forward оптимизация в MetaTrader 5 - своими руками Walk-Forward оптимизация в MetaTrader 5 - своими руками
В статье рассматриваются подходы, позволяющие достаточно точно эмулировать walk-forward оптимизацию с помощью встроенного тестера и вспомогательных библиотек, реализованных на MQL.
Кроссплатфоменный торговый советник: Менеджер ордеров Кроссплатфоменный торговый советник: Менеджер ордеров
В статье обсуждается создание менеджера ордеров для кроссплатформенного торгового советника. Менеджер ордеров отвечает за открытие и закрытие экспертом ордеров или позиций, а также за ведение независимой записи о них, и будет доступен для обеих версий терминала.
Кроссплатформенный торговый советник: Сигналы Кроссплатформенный торговый советник: Сигналы
В статье обсуждаются классы CSignal и CSignals, которые будут использоваться в кроссплатформенных торговых советниках. Рассмотрены различия между MQL4 и MQL5 в организации данных, необходимых для оценки полученных торговых сигналов. Итог — код, совместимый с компиляторами обеих версий.
Паттерны, доступные при торговле корзинами валют. Часть III Паттерны, доступные при торговле корзинами валют. Часть III
Это заключительная статья, посвященная паттернам, возникающим при торговле корзинами валютных пар. Рассмотрены трендовые объединенные индикаторы и применение обычных графических построений.