preview
Индикатор CandleCode: Формализация свечных моделей в MQL5

Индикатор CandleCode: Формализация свечных моделей в MQL5

MetaTrader 5Примеры |
76 0
Artyom Trishkin
Artyom Trishkin

Содержание


Введение

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

Еще в 1999–2001 годах Виктор Лиховидов в журнале "Stocks & Commodities" предложил концепцию CandleCode, чтобы перевести структуру графика в цифру. В развитие темы, в марте 2001 года, была опубликована вторая часть исследования (Coding Candlesticks II), где автор предложил оптимизированный алгоритм вычислений, объединяющий расчеты параметров тела свечи и теней в единую функцию.

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

Суммарный вес этих характеристик формирует уникальный код, который позволяет заменить субъективный визуальный поиск паттернов строгими алгоритмическими расчетами. Если первые реализации метода, например, для TradeStation (EasyLanguage) или MetaStock подтвердили его применимость для статистического анализа, то сегодня современные мощности позволяют интегрировать этот подход в сложные нейросетевые и паттерн-поисковые системы.

Использование кодов Лиховидова превращает свечу в цифровой шифр. Это не избавляет от субъективности настроек, но дает трейдеру математический фундамент: теперь можно четко задать параметры поиска и мгновенно найти в прошлом тысячи похожих ситуаций. Сегодня возможности MQL5 позволяют превратить эту идею в полноценный инструмент анализа рыночной цикличности.

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


Индикатор Лиховидова Candle Code

Для лучшего понимания рассмотрим алгоритм кодирования свечей, предложенный В. Лиховидовым, где параметры свечи кодируются в число от 0 до 127.

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

Итоговый код — это сумма цвета, тела и теней:

Code = Color + Body + UpperShadow + LowerShadow

Каждое слагаемое вычисляется отдельно на основе сравнения текущего элемента с "нормой" — динамическим порогом волатильности, рассчитываемым как значение Bollinger Bands.

Фундаментом числового кода свечи является Базовый вес и Код тела, определяющие основной вектор движения.

Логика выбора коэффициентов зависит от типа свечи:

Тип свечи Условие Базовый вес (Color) Код тела (Body) 
БычьяClose > Open64Большое тело: +48 / Среднее тело: +32 / Маленькое тело: +16
МедвежьяClose < Open0Маленькое тело: +32 / Среднее тело: +16 / Большое тело: 0
ДоджиClose == Open64 или -64 (* в зависимости от отношения размеров теней)Всегда 0

* Если свеча имеет нулевое тело (доджи), то ее базовый вес имеет значение 64 в случае, если верхняя тень больше, либо равна нижней, и -64, в случае, если нижняя тень больше верхней.

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

Код верхней тени определяется из ее размера, представляющего давление со стороны продавцов (сопротивление росту):

Размер тени  Условие (относительно порогов BB)Код (US) 
ОтсутствуетДлина = 00
МалаяДлина < Нижний порог+4
СредняяНижний порог <= Длина < Верхний порог+8
ДлиннаяДлина >= Верхний порог+12

Код нижней тени определяется из ее размера, представляющего поддержку со стороны покупателей. Используется инвертированная шкала (отсутствие тени — признак силы):

Размер тени Условие (относительно порогов BB) Код (LS) 
ОтсутствуетДлина = 0+3
МалаяДлина < Нижний порог+2
СредняяНижний порог <= Длина < Верхний порог+1
ДлиннаяДлина >= Верхний порог0

Какова же механика расчета порогов для определения значений кодов свечей?

Как мы знаем, индикатор Bollinger Bands динамически подстраивается под рыночную волатильность на основе стандартного отклонения, и ширина его "ленты" зависит от текущей активности рынка. На спокойном рынке полосы Боллинджера сужаются, показывая узкий коридор значений. Как только рынок оживает, полосы расширяются, освобождая пространство для движения цен.

Эта особенность полос Боллинджера используется для определения пороговых значений при расчете кодов свечей.

Для каждого элемента свечи (тело и тени — верхняя и нижняя) производится собственный расчет по нормированию размеров относительно ширины полос Боллинджера. 

Для понимания сути таких расчетов представим, что для оценки свечи индикатор использует коридор Боллинджера как "фильтр нормальности". Границы коридора постоянно расширяются или сужаются, подстраиваясь под текущую волатильность, а сравниваемые размеры представлены в пунктах:

  • Если размер измеряемой части свечи находится внутри границ — это "Рыночная норма". Элемент свечи (тело или тень) признается средним.
  • Если размер измеряемой части свечи находится выше верхней границы — это "Выход за пределы нормы". Элемент признается большим (значимый импульс).
  • Если размер измеряемой части свечи находится ниже нижней границы — это "Рыночное затишье". Элемент признается малым (незначительный шум).

Например, для тела берется среднее значение размеров тел в пунктах за 55 периодов (по умолчанию). Это средняя точка. От этой средней точки откладывается размер стандартного отклонения вниз и вверх. Получаем две точки — нижняя и верхняя границы значений (в пунктах размера тела свечи). Допустим, ширина между этими значениями 20 пунктов, а размер стандартного отклонения равен пяти пунктам. Нижняя граница будет равна 15 пунктам, а верхняя 25.

Если тело свечи, его размер, попадает в диапазон от 15 до 25 пунктов, то это нормальное тело. Если размер ниже 15 пунктов — это маленькое тело. Соответственно, размер тела больше 25 пунктов — это большое тело.

Такая "динамическая линейка" позволяет индикатору сохранять точность оцифровки независимо от таймфрейма и текущей активности рынка.

Теперь в папке \MQL5\Indicators\STOCKS_COMMODITIES\Viktor_Likhovidov_Coding Candlestic\ напишем индикатор с именем CandleCode.mq5, рассчитывающий коды для каждой свечи, и выводящий их значения в отдельном окне графика:

//+------------------------------------------------------------------+
//|                                                   CandleCode.mq5 |
//|                                  Copyright 2026, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 5
#property indicator_plots   2
//--- plot Code
#property indicator_label1  "Code"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrDodgerBlue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- plot CodeAvg
#property indicator_label2  "CodeAvg"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrOrangeRed
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1

//--- input parameters
input int      InpBBLength  = 55;   // BB Length (Thresholds)
input double   InpBBNumDevs = 0.5;  // BB Deviations
input int      InpAvgLength = 9;    // Average Length (SMA)

//--- indicator buffers
double         BufferCCode[];       // Код свечи
double         BufferCCodeAvg[];    // Среднее кодов свечей
double         BufferBD[];          // Тело свечи
double         BufferUS[];          // Верхняя тень
double         BufferLS[];          // Нижняя тень

//--- global variables
int period_bb;
int period_sm;

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,BufferCCode,INDICATOR_DATA);
   SetIndexBuffer(1,BufferCCodeAvg,INDICATOR_DATA);
   SetIndexBuffer(2,BufferBD,INDICATOR_CALCULATIONS);
   SetIndexBuffer(3,BufferUS,INDICATOR_CALCULATIONS);
   SetIndexBuffer(4,BufferLS,INDICATOR_CALCULATIONS);

//--- Индексация как в таймсериях
   ArraySetAsSeries(BufferCCode,true);
   ArraySetAsSeries(BufferCCodeAvg,true);
   ArraySetAsSeries(BufferBD,true);
   ArraySetAsSeries(BufferUS,true);
   ArraySetAsSeries(BufferLS,true);

//--- Корректировка введенных значений
   period_bb=(InpBBLength<1 ? 55 : InpBBLength);
   period_sm=(InpAvgLength<1 ? 9 : InpAvgLength);
   
//--- Настройки индикатора
   IndicatorSetString(INDICATOR_SHORTNAME,StringFormat("CandleCode(%d,%.1f,%d)",period_bb,InpBBNumDevs,period_sm));
   IndicatorSetInteger(INDICATOR_DIGITS,0);

//--- Успешно
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t rates_total,
                const int32_t prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int32_t &spread[])
  {
//--- Проверка количества доступных баров
   if(rates_total<period_bb+period_sm)
      return(0);

//--- Массивы для расчета - как таймсерии
   ArraySetAsSeries(open, true);
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low,  true);
   ArraySetAsSeries(close,true);

//--- Проверка и расчет количества просчитываемых баров
   int limit=rates_total-prev_calculated;

//--- Если первый запуск или изменения исторических данных
   if(limit>1)
     {
      limit=rates_total-period_bb-1;
      ArrayInitialize(BufferCCode,0);
      ArrayInitialize(BufferCCodeAvg,0);
      ArrayInitialize(BufferBD,0);
      ArrayInitialize(BufferUS,0);
      ArrayInitialize(BufferLS,0);
     }

//--- Основной цикл
   for(int i=limit;i>=0;i--)
     {
      //--- Верх и низ текущей свечи
      double hi=fmax(open[i],close[i]);
      double lo=fmin(open[i],close[i]);

      //--- Записываем в буферы размеры тела, верхней и нижней теней
      BufferBD[i]=hi-lo;
      BufferUS[i]=high[i]-hi;
      BufferLS[i]=lo-low[i];

      //--- Переменные для порогов (Bollinger Bands)
      double bot_bd=0, top_bd=0, bot_us=0, top_us=0, bot_ls=0, top_ls=0;

      //--- Расчет статистических порогов для каждого элемента свечи
      CalcBBThresholds(BufferBD,i,period_bb,InpBBNumDevs,bot_bd,top_bd);
      CalcBBThresholds(BufferUS,i,period_bb,InpBBNumDevs,bot_us,top_us);
      CalcBBThresholds(BufferLS,i,period_bb,InpBBNumDevs,bot_ls,top_ls);

      int color_code=0, body_code=0, us_code=0, ls_code=0;

      //--- Определение кодов цвета и тела свечи
      //--- Бычье тело
      if(close[i]>open[i])
        {
         color_code=64;
         body_code=(BufferBD[i]<bot_bd ? 16 : BufferBD[i]<top_bd ? 32 : 48);
        }
      //--- Медвежье тело
      else if(close[i]<open[i])
        {
         color_code=0;
         body_code=(BufferBD[i]<bot_bd ? 32 : BufferBD[i]<top_bd ? 16 : 0);
        }
      //--- Doji (Open == Close)
      else
        {
         color_code=(BufferUS[i]>=BufferLS[i] ? 64 : -64);
         body_code=0;
        }

      //--- Код верхней тени
      us_code=(BufferUS[i]==0 ? 0 : BufferUS[i]<bot_us ? 4 : BufferUS[i]<top_us ? 8 : 12);
      
      //--- Код нижней тени
      ls_code=(BufferLS[i]==0 ? 3 : BufferLS[i]<bot_ls ? 2 : BufferLS[i]<top_ls ? 1 : 0);

      //--- Итоговый код свечи Лиховидова
      BufferCCode[i]=double(color_code+body_code+us_code+ls_code);
     }

//--- Сглаживание
   for(int i=limit;i>=0;i--)
     {
      double sum=0;
      for(int j=0;j<period_sm;j++)
         sum+=BufferCCode[i+j];
      BufferCCodeAvg[i]=sum/period_sm;
     }

//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| Расчет пороговых значений на основе Bollinger Bands              |
//+------------------------------------------------------------------+
void CalcBBThresholds(const double &data[], int pos, int period, double dev, double &bot, double &top)
  {
   double sum=0, avg=0, sq_sum=0, std_dev=0;
//--- Среднее (SMA)
   for(int i=0;i<period;i++)
      sum+=data[pos+i];
   avg=sum/period;

//--- Сумма квадратов отклонений для StdDev
   for(int i=0;i<period;i++)
      sq_sum+=pow(data[pos+i]-avg,2);
   std_dev=sqrt(sq_sum/period);

//--- Расчет значений BB
   bot=avg-dev*std_dev;
   top=avg+dev*std_dev;
  }

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

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

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

Напишем в той же папке еще один индикатор с именем CandleCodeColorHist.mq5:

//+------------------------------------------------------------------+
//|                                          CandleCodeColorHist.mq5 |
//|                                  Copyright 2026, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 6
#property indicator_plots   2
//--- plot Code
#property indicator_label1  "Code"
#property indicator_type1   DRAW_COLOR_HISTOGRAM
#property indicator_color1  clrRed,clrOrangeRed,clrLimeGreen,clrGreen
#property indicator_style1  STYLE_SOLID
#property indicator_width1  2
//--- plot CodeAvg
#property indicator_label2  "CodeAvg"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrOrangeRed
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1

//--- input parameters
input int      InpBBLength  = 55;   // BB Length (Thresholds)
input double   InpBBNumDevs = 0.5;  // BB Deviations
input int      InpAvgLength = 9;    // Average Length (SMA)

//--- indicator buffers
double         BufferCCode[];       // Код свечи
double         BufferColor[];       // Цвет свечи
double         BufferCCodeAvg[];    // Среднее кодов свечей
double         BufferBD[];          // Тело свечи
double         BufferUS[];          // Верхняя тень
double         BufferLS[];          // Нижняя тень

//--- global variables
int period_bb;
int period_sm;

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,BufferCCode,INDICATOR_DATA);
   SetIndexBuffer(1,BufferColor,INDICATOR_COLOR_INDEX);
   SetIndexBuffer(2,BufferCCodeAvg,INDICATOR_DATA);
   SetIndexBuffer(3,BufferBD,INDICATOR_CALCULATIONS);
   SetIndexBuffer(4,BufferUS,INDICATOR_CALCULATIONS);
   SetIndexBuffer(5,BufferLS,INDICATOR_CALCULATIONS);

//--- Индексация как в таймсериях
   ArraySetAsSeries(BufferCCode,true);
   ArraySetAsSeries(BufferColor,true);
   ArraySetAsSeries(BufferCCodeAvg,true);
   ArraySetAsSeries(BufferBD,true);
   ArraySetAsSeries(BufferUS,true);
   ArraySetAsSeries(BufferLS,true);

//--- Корректировка введенных значений
   period_bb=(InpBBLength<1 ? 55 : InpBBLength);
   period_sm=(InpAvgLength<1 ? 9 : InpAvgLength);
   
//--- Настройки индикатора
   IndicatorSetString(INDICATOR_SHORTNAME,StringFormat("CandleCodeCH(%d,%.1f,%d)",period_bb,InpBBNumDevs,period_sm));
   IndicatorSetInteger(INDICATOR_DIGITS,0);

//--- Успешно
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t rates_total,
                const int32_t prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int32_t &spread[])
  {
//--- Проверка количества доступных баров
   if(rates_total<period_bb+period_sm)
      return(0);

//--- Массивы для расчёта - как таймсерии
   ArraySetAsSeries(open, true);
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low,  true);
   ArraySetAsSeries(close,true);

//--- Проверка и расчёт количества просчитываемых баров
   int limit=rates_total-prev_calculated;

//--- Если первый запуск или изменения исторических данных
   if(limit>1)
     {
      limit=rates_total-period_bb-1;
      ArrayInitialize(BufferCCode,0);
      ArrayInitialize(BufferCCodeAvg,0);
      ArrayInitialize(BufferBD,0);
      ArrayInitialize(BufferUS,0);
      ArrayInitialize(BufferLS,0);
     }

//--- Основной цикл
   for(int i=limit;i>=0;i--)
     {
      //--- Верх и низ текущей свечи
      double hi=fmax(open[i],close[i]);
      double lo=fmin(open[i],close[i]);

      //--- Записываем в буферы размеры тела, верхней и нижней теней
      BufferBD[i]=hi-lo;
      BufferUS[i]=high[i]-hi;
      BufferLS[i]=lo-low[i];

      //--- Переменные для порогов (Bollinger Bands)
      double bot_bd=0, top_bd=0, bot_us=0, top_us=0, bot_ls=0, top_ls=0;

      //--- Расчет статистических порогов для каждого элемента свечи
      CalcBBThresholds(BufferBD,i,period_bb,InpBBNumDevs,bot_bd,top_bd);
      CalcBBThresholds(BufferUS,i,period_bb,InpBBNumDevs,bot_us,top_us);
      CalcBBThresholds(BufferLS,i,period_bb,InpBBNumDevs,bot_ls,top_ls);

      int color_code=0, body_code=0, us_code=0, ls_code=0;

      //--- Определение кодов цвета и тела свечи
      //--- Бычье тело
      if(close[i]>open[i])
        {
         color_code=64;
         body_code=(BufferBD[i]<bot_bd ? 16 : BufferBD[i]<top_bd ? 32 : 48);
        }
      //--- Медвежье тело
      else if(close[i]<open[i])
        {
         color_code=0;
         body_code=(BufferBD[i]<bot_bd ? 32 : BufferBD[i]<top_bd ? 16 : 0);
        }
      //--- Doji (Open == Close)
      else
        {
         color_code=(BufferUS[i]>=BufferLS[i] ? 64 : -64);
         body_code=0;
        }

      //--- Код верхней тени
      us_code=(BufferUS[i]==0 ? 0 : BufferUS[i]<bot_us ? 4 : BufferUS[i]<top_us ? 8 : 12);
      
      //--- Код нижней тени
      ls_code=(BufferLS[i]==0 ? 3 : BufferLS[i]<bot_ls ? 2 : BufferLS[i]<top_ls ? 1 : 0);

      //--- Итоговый код свечи Лиховидова
      double code=double(color_code+body_code+us_code+ls_code);
      BufferCCode[i]=code;
      //--- Цвет гистограммы
      BufferColor[i]=(code<32 ? 0 : code<64 ? 1 : code<96 ? 2 : 3);
     }

//--- Сглаживание
   for(int i=limit;i>=0;i--)
     {
      double sum=0;
      for(int j=0;j<period_sm;j++)
         sum+=BufferCCode[i+j];
      BufferCCodeAvg[i]=sum/period_sm;
     }

//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| Расчет пороговых значений на основе Bollinger Bands              |
//+------------------------------------------------------------------+
void CalcBBThresholds(const double &data[], int pos, int period, double dev, double &bot, double &top)
  {
   double sum=0, avg=0, sq_sum=0, std_dev=0;
//--- Среднее (SMA)
   for(int i=0;i<period;i++)
      sum+=data[pos+i];
   avg=sum/period;

//--- Сумма квадратов отклонений для StdDev
   for(int i=0;i<period;i++)
      sq_sum+=pow(data[pos+i]-avg,2);
   std_dev=sqrt(sq_sum/period);

//--- Расчёт значений BB
   bot=avg-dev*std_dev;
   top=avg+dev*std_dev;
  }
//+------------------------------------------------------------------+

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

Зеленые столбцы обозначают бычие свечи, красные — медвежие. Чем ближе значение к нулю — тем более медвежьей является свеча. Чем больше значение столбца — тем более бычьей является свеча.

Но есть одно исключение: если есть столбцы с отрицательным значением, то это сильные бычьи паттерны — пин-бары и доджи с более длинной нижней тенью:

Что же позволяет получить такой метод кодирования свечей?

Оцифровка графика позволяет выявить скрытые закономерности:

  • Экстремумы (коды 0–10 и 115–127). Констатация "чистого" медвежьего или бычьего импульса, где все параметры свечи работают в одном направлении.
  • Отрицательная зона. Математический маркер специфических бычьих разворотов (Доджи с длинной нижней тенью).
  • Код в районе значения 64. Состояние равновесия или неопределенности.

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

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

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

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

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

Для визуализации сделаем несколько удобных инструментов:

  • окно-дублер: лучший найденный паттерн будет отображаться во вложенном графическом объекте прямо на основном чарте;
  • информационная панель: блок статистики покажет вероятность исхода и средний процент совпадения найденных моделей;
  • интерактивный режим: зажав клавишу Ctrl, можно кликнуть в любую точку графика, чтобы мгновенно получить "прогноз" на тот момент и найти исторический аналог для выбранного бара.

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


Поиск паттернов на истории

В папке \MQL5\Experts\STOCKS_COMMODITIES\Viktor_Likhovidov_Coding_Candlestic\ создадим новый файл советника под именем ExpCodePatternFinder.mq5.

Начнем наполнять файл советника. Объявим структуры и макроподстановки:

//+------------------------------------------------------------------+
//|                                         ExpCodePatternFinder.mq5 |
//|                                  Copyright 2026, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Перечисления                                                     |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Структуры                                                        |
//+------------------------------------------------------------------+
struct SPatternStep                    // Структура одного бара паттерна
  {
   int               code;             // Код Лиховидова
   datetime          time;             // Время бара
   double            open;             // Цена открытия
   double            high;             // Цена High
   double            low;              // Цена Low
   double            close;            // Цена Close
   double            open_base;        // Цена открытия базовой точки
   datetime          time_base;        // Время базовой точки
   double            rel_pos;          // Позиция относительно базовой точки (Open[i] - open_base) / ATR
  };  

struct SPattern                        // Структура паттерна
  {
   SPatternStep      steps[];          // Глубина паттерна
   ENUM_POSITION_TYPE type;            // POSITION_TYPE_BUY (Low ZZ) или POSITION_TYPE_SELL (High ZZ)
   datetime          time;             // Время экстремума
   //--- Устанавливает глубину паттерна
   bool              SetSteps(const int size){ return(ArrayResize(steps,size)==size); }
  };

struct SCandleParts                    // Структура кода Лиховидова
  {
   int               body;             // Градация тела (0-3)
   int               us;               // Верхняя тень (0-3)
   int               ls;               // Нижняя тень (0-3)
   ENUM_POSITION_TYPE direction;       // Направление (BUY/SELL)
  };  
  
struct SMatchResult                    // Структура результатов поиска
  {
   int               best_idx;         // Индекс лучшего паттерна
   string            signal;           // Текст сигнала
   int               cnt_buy;          // Количество совпадений BUY
   int               cnt_sell;         // Количество совпадений SELL
   double            avg_buy;          // Среднее сходство BUY
   double            avg_sell;         // Среднее сходство SELL
   
   //--- Конструктор
   SMatchResult() : best_idx(WRONG_VALUE), signal("UNCERTAIN"), cnt_buy(0), cnt_sell(0), avg_buy(0), avg_sell(0) {}
  };  
  
//+------------------------------------------------------------------+
//| Макроподстановки                                                 |
//+------------------------------------------------------------------+
#define  ATTEMPTS_MAX   10             // Максимальное количество попыток получения базы паттернов
#define  MAX_POS_DIST   0.5            // 0.5 ATR - максимальный предел смещения (0% сходства вертикальной позиции)

Структура SPatternStep — хранит код одной свечи, описывает один "кадр" в последовательности паттерна. Здесь объединяется в одну ячейку все, что необходимо знать о свече. Поле rel_pos (относительная позиция) хранит расстояние от открытия этой свечи до точки разворота (цена открытия базовой свечи паттерна), нормированное на ATR. Именно благодаря этому полю, советник при поиске совпадений внутри паттернов понимает: "Эта свеча такая же по форме, как в эталонном паттерне, но находится она гораздо выше относительно точки разворота, и не подходит".

Структура SPattern — цифровой слепок рыночной конфигурации — паттерн. Если SPatternStep — это один шаг паттерна, одна "буква", то SPattern — это целое слово (последовательность свечей, шагов). Структура объединяет массив шагов (steps[]) и результат, к которому они привели (тип сигнала BUY или SELL по ZigZag). В ней хранится "время триггера" (time) — момент, когда ZigZag зафиксировал излом. Это позволяет советнику мгновенно найти это место на истории и показать его в окне.

SCandleParts— служебная структура, которая нужна "переводчику из логики Лиховидова в обычную математическую размерность" — функции DecomposeCode. Код Лиховидова (например, число 84) — это "черный ящик". Внутри него закодированы данные о тенях и теле. Структура позволяет разложить это число на понятные составляющие: body (тело), us (верхняя тень) и ls (нижняя тень) со значениями от 0 до 3, что дает возможность математически точно сравнивать свечи: "У этой свечи тело на 1 единицу больше, а тени идентичны".

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

При сравнении свечей мы не ограничиваемся только их внешним видом (кодом Лиховидова). Важно понимать, где именно находится свеча относительно базовой точки паттерна — выше или ниже начала разворота. Для этого мы используем макроподстановку MAX_POS_DIST, которая работает как универсальная линейка волатильности.

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

Во входные параметры советника перенесем параметры индикатора CandleCode и других используемых советником индикаторов:

//+------------------------------------------------------------------+
//| Входные параметры                                                |
//+------------------------------------------------------------------+
//--- ZigZag
sinput string  InpParamZZ     =  "---";   // --- ZigZag Parameters ---
input int      InpZZDepth     =  12;      // ZZ Depth
input int      InpZZDeviation =  5;       // ZZ Deviation
input int      InpZZBackstep  =  3;       // ZZ Back Step

//--- Candle Code
sinput string  InpParamCC     =  "---";   // --- Candle Code Parameters ---
input int      InpCCBBLength  =  55;      // Candle Code BB Length (Thresholds)
input double   InpCCBBNumDevs =  0.3;     // Candle Code BB Deviations
input int      InpCCAvgLength =  9;       // Candle Code Average Length (SMA)

//--- ATR
sinput string  InpParamATR    =  "---";   // --- ATR Parameters ---
input int      InpATRPeriod   =  14;      // ATR Period

//--- Параметры поиска паттернов
sinput string  InpParamPattSrc=  "---";   // --- Pattern search Parameters ---
input int      InpPatternDepth=  5;       /* Pattern Depth (Bars) */                         // Размер паттерна в барах
input uint     InpMaxGradeDiff=  1;       /* Maximum difference in gradations (0-3) */       // Максимальная разница по градациям тела/теней для совпадения (0-3)
input double   InpMinCandleSim=  50.0;    /* Candles similarity (0-100%) */                  // Сходство свечей (0-100%)
input double   InpMinPosSim   =  50.0;    /* Relative position similarity (0-100%) */        // Сходство относительного расположения (0-100%)
input double   InpTotalSim    =  65.0;    /* Final pattern similarity threshold (0-100%) */  // Итоговый порог похожести паттерна (0-100%)

//--- Параметры окна паттерна
sinput string  InpParamPattWnd=  "---";   // --- Pattern window Parameters ---
input int      InpWndX        =  30;      /* Pattern Window X offset */                      // Смещение окна по оси X
input int      InpWndY        =  30;      /* Pattern Window Y offset */                      // Смещение окна по оси Y
input int      InpWndW        =  500;     /* Pattern Window Width */                         // Ширина окна паттерна
input int      InpWndH        =  250;     /* Pattern Window Height */                        // Высота окна паттерна

//--- Параметры текста статистики
sinput string  InpParamStatsTxt= "---";   // --- Statistics text Parameters ---
input int      InpStatsX      =  10;      /* Statistics X offset */                          // Смещение текста по оси X
input int      InpStatsY      =  20;      /* Statistics Y offset */                          // Смещение текста по оси Y
input int      InpStatsStep   =  15;      /* Statistics Line Step */                         // Вертикальный шаг между строками
input bool     InpStatsBack   =  true;    /* Use background for statistics */                // Использовать подложку для текста статистики

//--- Параметры ручного режима
sinput string  InpParamManMode=  "---";   // --- Manual mode Parameters ---
input bool     InpManualMode  =  true;    /* Manual bar selection (Ctrl + Click) */          // Включить ручной выбор бара
input color    InpAnalysColor =  clrGray; /* Analysis line color */                          // Цвет линии выбранного бара

//--- Параметры советника
sinput string  InpParamExp    =  "---";   // --- Expert Parameters ---
input ulong    InpMagic       =  123;     /* Magic Number */                                 // Идентификатор эксперта

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

  1. Параметры индикатора ZigZag
  2. Параметры пользовательского индикатора Candle Code
  3. Параметры индикатора ATR
  4. Параметры поиска и сопоставления паттернов:
    Ключевые переменные, отвечающие за точность алгоритма распознавания образов
  5. Параметры графического интерфейса
    Настройки визуализации результатов поиска
  6. Системные настройки
    InpManualMode — переключатель режима работы: true — активация поиска по событию клика (Ctrl + Click), false — автоматический расчет при открытии каждого нового бара.
    InpAnalysColor — цвет вертикальной линии, маркирующей выбранную пользователем точку начала анализа.
    InpMagic — уникальный идентификатор эксперта. Используется в префиксе имен всех графических объектов, и также может использоваться для торговых операций при их реализации.

Добавим блок глобальных переменных советника:

//+------------------------------------------------------------------+
//| Глобальные переменные                                            |
//+------------------------------------------------------------------+
int      handle_zz;                    // Хэндл ZigZag
int      handle_cc;                    // Хэндл Candle Code
int      handle_atr;                   // Хэндл ATR
int      zz_depth;                     // ZZ Depth
int      zz_dev;                       // ZZ Deviation
int      zz_back;                      // ZZ Back Step
int      cc_bb_len;                    // Candle Code BB Length (Thresholds)
int      cc_avg_len;                   // Candle Code Average Length (SMA) 
int      atr_period;                   // ATR Period

int      ExtPatternDepth;              // Размер паттерна в барах
uint     ExtMaxGradeDiff;              // Максимальная разница по градациям тела/теней для совпадения (0-3)
double   ExtMinCandleSim;              // Сходство свечей (0-100%)
double   ExtMinPosSim;                 // Сходство относительного расположения (0-100%)
double   ExtTotalSim;                  // Итоговый порог похожести паттерна (0-100%)

int      attempts_count;               // Счётчик попыток сбора базы паттернов

SPattern patterns[];                   // Массив паттернов
SMatchResult match_result;             // Структура результатов поиска
bool     base_is_ready;                // Флаг готовности базы паттернов

string   prefix;                       // Префикс имён объектов
int      start_search_index;           // Бар начала поиска

Здесь объявлены хэндлы всех индикаторов, копии корректируемых входных параметров, массив структур patterns[], где будем хранить найденные паттерны на изломах зигзага, флаг base_is_ready, указывающий на то, смогли ли мы получить и заполнить базу данных, префикс графических объектов для их удаления по этому префиксу и идентификатор эксперта.

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- create timer
   EventSetTimer(60);
   
//--- Инициализируем массив паттернов
   ArrayFree(patterns);
   
//--- Устанавливаем и корректируем входные параметры индикаторов
//--- ZigZag
   zz_depth=(InpZZDepth   <1 ? 12 : InpZZDepth);
   zz_dev=(InpZZDeviation <1 ?  5 : InpZZDeviation);
   zz_back=(InpZZBackstep <1 ?  3 : InpZZBackstep);
   
//--- Candle Code
   cc_bb_len=(InpCCBBLength  <1 ? 55 : InpCCBBLength);
   cc_avg_len=(InpCCAvgLength<1 ?  9 : InpCCAvgLength);
   
//--- ATR
   atr_period=(InpATRPeriod<1 ? 14 : InpATRPeriod);
     
//--- Создаём хэндлы индикаторов
//--- ATR
   handle_atr=iATR(_Symbol,_Period,atr_period);
   if(handle_atr==INVALID_HANDLE)
     {
      PrintFormat("%s: Failed to create iATR(%d) handle",__FUNCTION__,atr_period);
      return INIT_FAILED;
     }
//--- ZigZag   \MQL5\Indicators\Examples\ZigZag.mq5
   handle_zz=iCustom(_Symbol,_Period,"Examples\\ZigZag",zz_depth,zz_dev,zz_back);
   if(handle_zz==INVALID_HANDLE)
     {
      PrintFormat("%s: Failed to create ZigZag(%d,%d,%d) handle",__FUNCTION__,zz_depth,zz_dev,zz_back);
      return INIT_FAILED;
     }
//--- CandleCode
   handle_cc=iCustom(_Symbol,_Period,"CandleCode",cc_bb_len,InpCCBBNumDevs,cc_avg_len);
   if(handle_cc==INVALID_HANDLE)
     {
      PrintFormat("%s: Failed to create CandleCode(%d,%.1f,%d) handle",__FUNCTION__,cc_bb_len,InpCCBBNumDevs,cc_avg_len);
      return INIT_FAILED;
     }
   
//--- Инициализируем флаг готовности и количество попыток получения базы паттернов
   base_is_ready=false;
   attempts_count=0;
   
//--- Корректируем введённые значения поиска паттернов
   ExtPatternDepth=(InpPatternDepth<0 ? 5 : InpPatternDepth);
   ExtMinCandleSim=(InpMinCandleSim<0 ? 80.0 : InpMinCandleSim>100 ? 100.0 : InpMinCandleSim);
   ExtMinPosSim=(InpMinPosSim<0 ? 80.0 : InpMinPosSim>100 ? 100.0 : InpMinPosSim);
   ExtMaxGradeDiff=int(InpMaxGradeDiff>3 ? 3 : InpMaxGradeDiff);
   ExtTotalSim=(InpTotalSim<0 ? 85.0 : InpTotalSim>100 ? 100.0 : InpTotalSim);
   start_search_index=1;
   
   prefix=MQLInfoString(MQL_PROGRAM_NAME)+(string)InpMagic;
   
//--- Пытаемся сформировать базу паттернов сразу при инициализации
   base_is_ready=CreatePatternBase(ATTEMPTS_MAX,ExtPatternDepth,cc_bb_len,attempts_count,patterns);
   
//--- Если база готова - ищем паттерны и выводим статистику
   if(base_is_ready)
     {
      int total=ArraySize(patterns);
      PrintFormat("%s: The pattern database has been successfully created. Total patterns: %d",__FUNCTION__,total);
      
      FindAndDisplayMatches(ExtPatternDepth,ExtTotalSim,ExtMinCandleSim,ExtMinPosSim,ExtMaxGradeDiff,patterns,match_result,prefix+"_BestMatchWindow",
                            InpWndX, InpWndY, InpWndW, InpWndH,InpStatsX, InpStatsY, InpStatsStep,InpStatsBack,start_search_index,InpAnalysColor);
     }   
   
//--- Всё успешно
   return(INIT_SUCCEEDED);
  }

В обработчике OnDeinit() освобождаем массив паттернов и удаляем по префиксу все созданные советником графические объекты:

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- destroy timer
   EventKillTimer();
   
//--- Освобождаем массив паттернов
   ArrayFree(patterns);
   
//--- Удаляем графические объекты
   ObjectsDeleteAll(ChartID(),prefix);
  }

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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Если база паттернов ещё не готова - пытаемся создать
   if(!base_is_ready)
     {
      if(CreatePatternBase(ATTEMPTS_MAX,ExtPatternDepth,cc_bb_len,attempts_count,patterns))
        {
         base_is_ready=true;
         int total=ArraySize(patterns);
         PrintFormat("%s: The pattern database has been successfully created. Total patterns: %d",__FUNCTION__,total);
        }
     }

//--- Если база всё еще не готова - выходим до следующего тика
   if(!base_is_ready)
      return;
   
//--- Поиск совпадений при появлении нового бара
   if(IsNewBar()) 
     { 
      //--- Если работа в автоматическом режиме, устанавливаем индекс на прошлый бар (с индексом 1)
      if(!InpManualMode)
        {
         //--- Выполняем поиск и визуализацию результатов, начиная от первого бара
         PrintFormat("%s: New Bar. Automatic search for matches from index 1",__FUNCTION__);
         start_search_index=1;
         FindAndDisplayMatches(ExtPatternDepth,ExtTotalSim,ExtMinCandleSim,ExtMinPosSim,ExtMaxGradeDiff,patterns,match_result,prefix+"_BestMatchWindow",
                            InpWndX,InpWndY,InpWndW,InpWndH,InpStatsX,InpStatsY,InpStatsStep,InpStatsBack,start_search_index,InpAnalysColor);
        }
     }     
  }

В обработчике OnChartEvent() обработаем щелчок мышкой по графику вызовом специального обработчика ChartClickHandler(), запускающего поиск совпадений по индексу бара, указанного щелчком мышки:

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int32_t id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Если режим автоматический - уходим
   if(!InpManualMode)
      return;

//--- Обработка клика по графику в ручном режиме
   if(id==CHARTEVENT_CLICK)
     {
      ChartClickHandler(lparam,dparam,start_search_index);
     }   
  }

Функция, возвращающая флаг открытия нового бара:

//+------------------------------------------------------------------+
//| Возвращает флаг открытия нового бара                             |
//+------------------------------------------------------------------+
bool IsNewBar(void)
  {
   static datetime lastbar=0;
   datetime bar=iTime(_Symbol,_Period,0);
   if(bar==lastbar || bar==0)
      return false;
   lastbar=bar;
   return true;
  }

Функция хранит время открытия последнего обработанного бара в static-переменной. На каждом тике она сравнивает его со временем открытия текущего бара. Если время совпадает — функция возвращает false (новый бар не наступил), как только время изменилось — возвращает true, сообщая об открытии нового бара.

Для получения данных цен и данных индикаторов напишем функции, возвращающие требуемые данные с указанного бара (index) в указанном количестве (count), и для ZigZag — с указанного буфера (buffer):

//+------------------------------------------------------------------+
//| Возвращает данные цен с указанного бара в заданном количестве    |
//+------------------------------------------------------------------+
bool GetPriceData(const int index,const int count,MqlRates &array[])
  {
   ResetLastError();
   if(CopyRates(_Symbol,_Period,index,count,array)!=count)
     {
      PrintFormat("%s: CopyRates(%d,%d) failed. Error %d",__FUNCTION__,index,count,GetLastError());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//| Возвращает данные индикатора ATR                                 |
//| с указанного бара в заданном количестве                          |
//+------------------------------------------------------------------+
bool GetATRData(const int index,const int count,double &array[])
  {
   ResetLastError();
   if(CopyBuffer(handle_atr,0,index,count,array)!=count)
     {
      PrintFormat("%s: CopyBuffer(%d,%d) failed. Error %d",__FUNCTION__,index,count,GetLastError());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//| Возвращает данные ZigZag в заданном количестве                   |
//| с указанного буфера и бара                                       |
//+------------------------------------------------------------------+
bool GetZZData(const int buffer,const int index,const int count,double &array[])
  {
   if(buffer>2)
     {
      PrintFormat("%s: Error: Invalid buffer number (%d). Must be 0 - 2",__FUNCTION__,buffer);
      return false;
     }
   ResetLastError();
   if(CopyBuffer(handle_zz,buffer,index,count,array)!=count)
     {
      PrintFormat("%s: CopyBuffer(%d,%d) failed. Error %d",__FUNCTION__,index,count,GetLastError());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+
//| Возвращает данные индикатора Candle Code                         |
//| с указанного бара в заданном количестве                          |
//+------------------------------------------------------------------+
bool GetCandleCodeData(const int index,const int count,double &array[])
  {
   ResetLastError();
   if(CopyBuffer(handle_cc,0,index,count,array)!=count)
     {
      PrintFormat("%s: CopyBuffer(%d,%d) failed. Error %d",__FUNCTION__,index,count,GetLastError());
      return false;
     }
   return true;
  }

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

//+------------------------------------------------------------------+
//| Сбор паттернов по всей истории                                   |
//+------------------------------------------------------------------+
bool CollectHistoryData(const int pattern_size,const int limit,SPattern &array[])
  {
//--- Проверяем наличие необходимой истории
   int bars_total=iBars(_Symbol,_Period);
   if(bars_total<limit+pattern_size+2)
      return false;

//--- Очищаем массив паттернов
   if(ArraySize(array)>0)
      ArrayFree(array);
   
//--- Массивы для получения данных
   MqlRates rates_hist[];
   double   atr_hist[],zz0_hist[],zz1_hist[],zz2_hist[],cc_hist[];

//--- Массивы как таймсерии
   ArraySetAsSeries(rates_hist,true);
   ArraySetAsSeries(atr_hist,true);
   ArraySetAsSeries(zz0_hist,true);
   ArraySetAsSeries(zz1_hist,true);
   ArraySetAsSeries(zz2_hist,true);
   ArraySetAsSeries(cc_hist,true);

//--- Получаем исторические данные цен и индикаторов
   if(!GetPriceData(0,bars_total,rates_hist))
      return false;
   if(!GetATRData(0,bars_total,atr_hist))
      return false;
   if(!GetZZData(0,0,bars_total,zz0_hist))
      return false;
   if(!GetZZData(1,0,bars_total,zz1_hist))
      return false;
   if(!GetZZData(2,0,bars_total,zz2_hist))
      return false;
   if(!GetCandleCodeData(0,bars_total,cc_hist))
      return false;

   bool is_first_found=false; // Флаг для пропуска текущего излома ZZ

//--- Проходим по всей истории
   for(int i=0; i<bars_total-limit-pattern_size; i++)
     {
      //--- Если в основном буфере ZZ пусто - это точно не излом
      if(zz0_hist[i] == 0 || zz0_hist[i] == EMPTY_VALUE)
         continue;

      int current_type=-1;
      
      //--- Оределяем тип
      //--- Если значение в основном буфере ZZ пришло из буфера вершин (1)
      if(zz1_hist[i] != 0 && zz1_hist[i] != EMPTY_VALUE && MathAbs(zz1_hist[i] - zz0_hist[i]) < _Point)
         current_type=POSITION_TYPE_SELL;
      
      //--- Если значение в основном буфере ZZ пришло из буфера впадин (2)
      else if(zz2_hist[i] != 0 && zz2_hist[i] != EMPTY_VALUE && MathAbs(zz2_hist[i] - zz0_hist[i]) < _Point)
         current_type=POSITION_TYPE_BUY;
      
      //--- Тип не определён - идём далее
      if(current_type==-1)
         continue;
      
      //--- Если это самое первое не пустое значение ZZ,
      //--- пропускаем его, так как это перерисовывающаяся крайняя "нога"
      if(!is_first_found)
        {
         is_first_found=true;
         continue;
        }

      //--- Инициализируем новый паттерн
      SPattern pattern;
      if(!pattern.SetSteps(pattern_size))
         return false;

      //--- Устанавливаем базовые данные паттерна
      pattern.type=(ENUM_POSITION_TYPE)current_type;
      pattern.time=rates_hist[i].time;

      //--- Подготавливаем базовые точки для расчёта относительной позиции
      double open_base=rates_hist[i].open;
      double atr_base=atr_hist[i];
      if(atr_base==0)
         continue;

      //--- Собираем данные по каждой свече, входящей в паттерн
      for(int j=0; j<pattern_size; j++)
        {
         int idx=i+j;
         //--- Сохраняем код Лиховидова и время бара
         pattern.steps[j].code=(int)cc_hist[idx];
         pattern.steps[j].time=rates_hist[idx].time;
         //--- Сохраняем цены OHLC текущей свечи шага
         pattern.steps[j].open=rates_hist[idx].open;
         pattern.steps[j].high=rates_hist[idx].high;
         pattern.steps[j].low=rates_hist[idx].low;
         pattern.steps[j].close=rates_hist[idx].close;
         //--- Сохраняем данные базовой точки
         pattern.steps[j].open_base=open_base;
         pattern.steps[j].time_base=rates_hist[i].time;
         pattern.steps[j].rel_pos=(rates_hist[idx].open-open_base)/atr_base;
        }

      //--- Увеличиваем размер массива паттернов
      int size=ArraySize(array);
      if(ArrayResize(array,size+1)!=size+1)
         return false;
      //--- Записываем паттерн в массив
      array[size]=pattern;
     }

//--- Всё успешно
   return true;
  }

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

По сути функция делает инвентаризацию истории. Она находит изломы индикатора ZigZag, которые служат точками подтвержденных разворотов. Для каждого такого случая советник "фотографирует" несколько баров, идущих перед пиком или впадиной, записывая их форму через коды Лиховидова и положение через ATR. При этом алгоритм игнорирует самый свежий излом, колено ZigZag, так как он еще может перерисоваться, и берет в работу только те движения, которые уже окончательно зафиксированы.

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

Оригинальные коды Лиховидова имеют неудобную особенность: шкалы теней и тел могут быть инвертированы (где-то 0 — это минимум, а где-то — максимум). Напишем функцию, устраняющую эту путаницу, выравнивая все значения так, чтобы 0 всегда означал минимальный размер, а 3 — максимальный:

//+------------------------------------------------------------------+
//| Декомпозиция кода Лиховидова в унифицированную структуру         |
//+------------------------------------------------------------------+
void DecomposeCode(const int code,SCandleParts &code_struct)
  {
//--- Определение направления свечи (64-127 бычья, 0-63 медвежья)
   bool bullish=(code>=64);
   code_struct.direction=(bullish ? POSITION_TYPE_BUY : POSITION_TYPE_SELL);
   
//--- Извлечение геометрических составляющих (градации 0-3)
   int temp=code-(bullish ? 64 : 0);
   int raw_body=temp/16;            // Тело
   int raw_us=(temp%16)/4;          // Верхняя тень
   int raw_ls=temp%4;               // Нижняя тень

//--- Приведение к единому формату (0 - минимум, 3 - максимум)
//--- Согласно спецификации индикатора, инверсия шкал различается для каждого элемента

//--- Тело: инвертировано только у медвежьих свечей (0 - максимум)
   code_struct.body=(bullish ? raw_body : 3-raw_body);

//--- Верхняя тень: в исходном коде всегда имеет прямую шкалу (0 - минимум)
   code_struct.us=raw_us;

//--- Нижняя тень: в исходном коде всегда имеет обратную шкалу (3 - минимум)
   code_struct.ls=3-raw_ls;
  }     

Функция работает как дешифратор. Ее задача — разложить итоговое число кода Лиховидова обратно на "физические" составляющие свечи: тело, верхнюю и нижнюю тени. Это необходимо, потому что для математического сравнения нам недостаточно знать, что код равен, например, 84 — нам нужно понимать, насколько большая у этой свечи тень или тело по шкале от 0 до 3. По сути, функция приводит данные к единому стандарту.

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

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

Напишем функцию для сравнения двух баров:

//+------------------------------------------------------------------+
//| Сравнивает две свечи по форме и расположению                     |
//+------------------------------------------------------------------+
double CalculateStepSimilarity(const SPatternStep &base_step,  // Данные эталонной свечи из истории
                               const SPatternStep &curr_step,  // Данные текущей свечи с рынка
                               const double min_candle_sim,    // Минимально допустимый процент сходства формы
                               const double min_pos_sim,       // Минимально допустимый процент сходства позиции
                               const int max_grade_diff)       // Максимально допустимая разница по градациям тела/теней
  {
//--- Разбираем оба кода на физические параметры свечей
   SCandleParts b,c;
   DecomposeCode(base_step.code,b);
   DecomposeCode(curr_step.code,c);

//--- Сравниваем цвет. Если направление не совпало - свечи разные, возвращаем 0
   if(b.direction!=c.direction)
      return 0.0;

//--- Настраиваемый фильтр: если различие по телу или теням больше max_grade_diff - свечи считаются непохожими
   if(fabs(b.body-c.body)>max_grade_diff || fabs(b.us-c.us)>max_grade_diff || fabs(b.ls-c.ls)>max_grade_diff)
      return 0.0;

//--- Вычисляем, насколько сильно различаются размеры тел и теней обеих свечей
   int diff_total=fabs(b.body-c.body)+fabs(b.us-c.us)+fabs(b.ls-c.ls);
   double s_candle=100.0*(1.0-(double)diff_total/9.0);

//--- Вычисляем, насколько сильно отличаются вертикальные позиции свечей относительно базовой свечи
   double diff_pos=fabs(base_step.rel_pos-curr_step.rel_pos);
   double s_pos=fmax(0.0,100.0*(1.0-diff_pos/MAX_POS_DIST));

//--- Возвращаем взвешенную оценку сходства: баланс между формой свечи (50%) и её положением в ATR (50%)
   return(s_candle*0.5+s_pos*0.5);
  }  

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

По сути функция применяет двойной фильтр. Сначала проверяется вид свечи — ее цвет и форма через коды Лиховидова. Если цвета не совпадают или разница в размерах тел и теней слишком велика, свеча сразу же отбраковывается. Если же форма прошла проверку, то далее работает второй фильтр — относительная позиция. Алгоритм смотрит, на каком расстоянии от точки разворота находится свеча, используя ATR как универсальную линейку.

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

Чтобы мы могли сравнить слепок текущего рынка с историческим паттерном, необходимо сначала получить паттерн с текущего рынка. Напишем такую функцию:

//+------------------------------------------------------------------+
//| Возвращает паттерн, сформированный на текущих рыночных данных    |
//+------------------------------------------------------------------+
bool GetCurrentPattern(SPattern &pattern,const int start_index,const int depth)
  {
//--- Устанавливаем размер (глубину) данных паттерна
   if(!pattern.SetSteps(depth))
      return false;

//--- Получаем базовые данные (цену и волатильность) для точки отсчёта
   MqlRates r_base[];
   double   atr_val[];
   if(!GetPriceData(start_index,1,r_base))
      return false;
   if(!GetATRData(start_index,1,atr_val))
      return false;

//--- Сохраняем значения для расчёта относительного расположения свечей в паттерне
   double open_base=r_base[0].open;
   double atr_base=atr_val[0];

//--- Проверка на нулевую волатильность
   if(atr_base==0)
      return false;

//--- Собираем данные по каждой свече паттерна
   for(int j=0; j<depth; j++)
     {
      int idx=start_index+j;
      MqlRates rates[];
      double   ccode[];
      
      //--- Запрашиваем цену и CandleCode для каждого бара паттерна
      if(!GetPriceData(idx,1,rates))
         return false;
      if(!GetCandleCodeData(idx,1,ccode))
         return false;

      //--- Записываем свойства свечи в структуру
      pattern.steps[j].code=(int)ccode[0];
      pattern.steps[j].time=rates[0].time;
      
      //--- Сохраняем цены OHLC текущей свечи
      pattern.steps[j].open=rates[0].open;
      pattern.steps[j].high=rates[0].high;
      pattern.steps[j].low=rates[0].low;
      pattern.steps[j].close=rates[0].close;
      
      //--- Вычисляем положение относительно базовой точки в единицах ATR
      pattern.steps[j].rel_pos=(rates[0].open-open_base)/atr_base;
      
      //--- Записываем данные базовой точки
      pattern.steps[j].open_base=open_base;
      pattern.steps[j].time_base=r_base[0].time;
     }

//--- Паттерн успешно получен
   return true;   
  }

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

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

//+------------------------------------------------------------------+
//| Сравнивает два паттерна и возвращает процент сходства            |
//+------------------------------------------------------------------+
double ComparePatterns(const SPattern &base,const SPattern &curr,const double min_candle_sim,const double min_pos_sim,const int max_grade_diff)
  {
   double summ=0;
   int depth=ArraySize(base.steps);

//--- Проверка на соответствие размеров
   if(depth!=ArraySize(curr.steps) || depth==0)
      return 0.0;
   
//--- Сравнение каждой свечи в паттерне
   for(int i=0; i<depth; i++)
     {
      //--- Сравниваем каждую последующую свечу исторического паттерна со свечой текущего
      double res=CalculateStepSimilarity(base.steps[i],curr.steps[i],min_candle_sim,min_pos_sim,max_grade_diff);
      
      //--- Если хотя бы одна свеча не прошла локальные фильтры - паттерны не похожи (очень жёсткий фильтр)
      // if(res==0)
      //    return 0.0; 
      
      //--- Накапливаем общую сумму процентов сходства всех свечей в паттерне
      summ+=res;
     }
   
//--- Возвращаем среднее арифметическое сходство всей фигуры
   return(summ/depth);
  }  

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

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

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

Напишем функцию, возвращающую описание паттерна:

//+------------------------------------------------------------------+
//| Возвращает текстовое описание паттерна                           |
//+------------------------------------------------------------------+
string PatternDescription(const SPattern &pattern)
  {
   int depth=ArraySize(pattern.steps);
   if(depth==0)
      return "Empty pattern";

//--- Получаем точность котировок текущего символа
   int dg=_Digits;

   string desc=StringFormat("Pattern %s (%s):\n",
                            (pattern.type==POSITION_TYPE_BUY ? "BUY" : "SELL"),
                            TimeToString(pattern.time));

//--- Собираем данные по каждому бару паттерна
   for(int i=0; i<depth; i++)
     {
      SCandleParts p;
//--- Разбираем оригинальный код Лиховидова
      DecomposeCode(pattern.steps[i].code,p);

      //--- Формируем строку описания свойств текущего в цикле бара
      desc+=StringFormat("Bar %d: %5s [O:%.*f H:%.*f L:%.*f C:%.*f] [Code:%3d] [B:%d US:%d LS:%d] Pos:%5.2f ATR\n",
                         i,
                         (p.direction==POSITION_TYPE_BUY ? "Bull" : "Bear"),
                         dg,pattern.steps[i].open,
                         dg,pattern.steps[i].high,
                         dg,pattern.steps[i].low,
                         dg,pattern.steps[i].close,
                         pattern.steps[i].code,
                         p.body,
                         p.us,
                         p.ls,
                         pattern.steps[i].rel_pos);
     }

//--- Возвращаем созданное описание
   return desc;
  }  
//+------------------------------------------------------------------+
//| Выводит описание паттерна в журнал                               |
//+------------------------------------------------------------------+
void PatternPrint(const SPattern &pattern)
  {
   Print(PatternDescription(pattern));
  }

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

Функция PatternPrintпросто выводит этот текст в журнал экспертов.

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

//+------------------------------------------------------------------+
//| Создаёт графическую метку на указанном графике                   |
//+------------------------------------------------------------------+
bool CreateMarker(const long chart_id,const datetime time,const double price,const ENUM_POSITION_TYPE type)
  {
   string name=prefix+"_ZZ_Marker";

//--- Если объекта нет - создаём его
   if(ObjectFind(chart_id,name)<0)
     {
      if(!ObjectCreate(chart_id,name,OBJ_ARROW,0,time,price))
         return false;
     }
   
//--- Обновляем свойства объекта (позицию и визуализацию)
   ObjectSetDouble( chart_id,name,OBJPROP_PRICE,price);
   ObjectSetInteger(chart_id,name,OBJPROP_TIME,time);
   ObjectSetInteger(chart_id,name,OBJPROP_ARROWCODE,82);
   ObjectSetInteger(chart_id,name,OBJPROP_WIDTH,1);
   ObjectSetInteger(chart_id,name,OBJPROP_COLOR, (type==POSITION_TYPE_SELL ? clrRed : clrDodgerBlue));
   ObjectSetInteger(chart_id,name,OBJPROP_ANCHOR,(type==POSITION_TYPE_SELL ? ANCHOR_BOTTOM : ANCHOR_TOP));
   ObjectSetInteger(chart_id,name,OBJPROP_SELECTABLE,false);
   
//--- Всё успешно
   return true;
  }

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

Все эти визуальные метки будут отображаться не в главном окне графика, а будет построен графический объект-график, и уже в нем мы будем отмечать и показывать исторические паттерны. Это даст возможность сравнить текущее состояние рынка с историческим паттерном, отображаемым в этом вложенном окне графика. И в нем же будем выводить статистику о количестве найденных паттернов и их свойствах.

Для создания информационной панели со строками внутри указанного графика напишем функцию:

//+------------------------------------------------------------------+
//| Создает объекты текстового блока статистики                      |
//+------------------------------------------------------------------+
bool CreateStatisticsObjects(const long chart_id, // Идентификатор графика
                             const int  count,    // Количество строк (объектов)
                             const int  x,        // Координата X
                             const int  y,        // Координата Y первой строки
                             const int  step,     // Шаг по вертикали
                             const bool use_back) // Флаг использования подложки
  {
//--- Проверка валидности идентификатора графика
   if(chart_id<=WRONG_VALUE)
      return false;

   string back_name=prefix+"_Stats_Back";
//---Если нужна подложка (фон)
   if(use_back)
     {
      if(ObjectFind(chart_id,back_name)<0)
        {
         if(!ObjectCreate(chart_id,back_name,OBJ_RECTANGLE_LABEL,0,0,0))
           {
            PrintFormat("%s: Error. ObjectCreate(%s,OBJ_RECTANGLE_LABEL) failed",__FUNCTION__,back_name);
            return false;
           }
         
         //--- Устанавливаем свойства созданного объекта
         ObjectSetInteger(chart_id,back_name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
         ObjectSetInteger(chart_id,back_name,OBJPROP_BGCOLOR,clrBisque);
         ObjectSetInteger(chart_id,back_name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
         ObjectSetInteger(chart_id,back_name,OBJPROP_SELECTABLE,false);
         ObjectSetInteger(chart_id,back_name,OBJPROP_HIDDEN,true);
         ObjectSetInteger(chart_id,back_name,OBJPROP_BACK,false);
        }
      //--- Обновляем размеры и позицию
      ObjectSetInteger(chart_id,back_name,OBJPROP_XDISTANCE,x-5);
      ObjectSetInteger(chart_id,back_name,OBJPROP_YDISTANCE,y-5);
      ObjectSetInteger(chart_id,back_name,OBJPROP_XSIZE,180);
      ObjectSetInteger(chart_id,back_name,OBJPROP_YSIZE,count*step+10);
     }
   else
     {
      //--- Если подложка не нужна, но объект существует - удаляем его
      if(ObjectFind(chart_id,back_name)>=0)
         ObjectDelete(chart_id,back_name);
     }

//--- Цикл создания объектов строк
   for(int i=0; i<count; i++)
     {
      string obj_name=StringFormat("%s_Stats_Line_%d",prefix,i);
      if(ObjectFind(chart_id,obj_name)<0)
        {
         if(!ObjectCreate(chart_id,obj_name,OBJ_LABEL,0,0,0))
           {
            PrintFormat("%s: Error. ObjectCreate(%s,OBJ_LABEL) failed",__FUNCTION__,obj_name);
            return false;
           }
            
         ObjectSetInteger(chart_id,obj_name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
         ObjectSetInteger(chart_id,obj_name,OBJPROP_FONTSIZE,8);
         ObjectSetString( chart_id,obj_name,OBJPROP_FONT,"Calibri");
         ObjectSetInteger(chart_id,obj_name,OBJPROP_SELECTABLE,false);
         ObjectSetInteger(chart_id,obj_name,OBJPROP_HIDDEN,true);
         ObjectSetInteger(chart_id,obj_name,OBJPROP_BACK,false);
        }
      
      ObjectSetInteger(chart_id,obj_name,OBJPROP_XDISTANCE,x);
      ObjectSetInteger(chart_id,obj_name,OBJPROP_YDISTANCE,y+(i*step));
     }
   return true;
  }

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

Для вывода статистики о найденных паттернах в подготовленные для этого объекты, напишем функцию:

//+------------------------------------------------------------------+
//| Выводит статистику на график и дублирует в журнал                |
//+------------------------------------------------------------------+
void DisplayStatistics(const long      chart_id,         // Идентификатор графика
                       const string    dominant_signal,  // Текст доминирующего сигнала
                       const int       count_buy,        // Количество совпадений BUY
                       const int       count_sell,       // Количество совпадений SELL
                       const double    avg_buy,          // Средний процент сходства BUY
                       const double    avg_sell,         // Средний процент сходства SELL
                       const SPattern &pattern,          // Структура лучшего паттерна
                       const int       best_idx,         // Индекс лучшего совпадения
                       const int       total_patterns,   // Общее количество паттернов в базе
                       const int       x,                // Координата X текстового блока
                       const int       y,                // Координата Y первой строки
                       const int       step,             // Шаг между строками
                       const bool      use_back)         // Флаг использования подложки
  {
//--- Проверка валидности идентификатора графика
   if(chart_id<=WRONG_VALUE)
      return;

//--- Формируем единую многострочную строку статистики
   string stats_text=StringFormat("====== PATTERN STATISTICS ======\n"+
                                  "Symbol: %s, Timeframe: %s\n"+
                                  "Snapshot Time: %s\n"+
                                  "Total Patterns in Base: %d\n"+
                                  "Best Match Index: %d\n"+
                                  "Pattern Time: %s\n"+
                                  "===============================\n"+
                                  "Dominant: %s\n"+
                                  "Total Matches: %d (BUY: %d, SELL: %d)\n"+
                                  "Average Similarity BUY: %.2f%%\n"+
                                  "Average Similarity SELL: %.2f%%",
                                  _Symbol,
                                  StringSubstr(EnumToString(_Period),7),
                                  TimeToString(TimeCurrent()),
                                  total_patterns,
                                  best_idx,
                                  TimeToString(pattern.time),
                                  dominant_signal,
                                  count_buy+count_sell,
                                  count_buy,
                                  count_sell,
                                  avg_buy,
                                  avg_sell);
                                    
//--- Разбиваем строку на массив подстрок по разделителю \n
   string lines[];
   ushort sep=(ushort)'\n';
   int total_lines=StringSplit(stats_text,sep,lines);

//--- Создаем или обновляем объекты блока статистики. При ошибке - выходим 
   if(!CreateStatisticsObjects(chart_id,total_lines,x,y,step,use_back))
      return;

//--- Выводим текст в созданные объекты
   for(int i=0; i<total_lines; i++)
     {
      string name=prefix+StringFormat("_Stats_Line_%d",i);
      ObjectSetString(chart_id,name,OBJPROP_TEXT,lines[i]);
                 
      //--- Определяем цвет строки в зависимости от содержимого
      color clr=(i==0 || i==6 ? clrGray : 
                 StringFind(lines[i],"BUY") >=0 ? clrBlue : 
                 StringFind(lines[i],"SELL")>=0 ? clrDarkRed : clrDarkSlateGray);
      ObjectSetInteger(chart_id,name,OBJPROP_COLOR,clr);
     }

//--- Дублируем информацию в основной журнал и перерисовываем указанный график
   Print(stats_text);
   ChartRedraw(chart_id);
  }

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

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

//+------------------------------------------------------------------+
//| Выводит окно чарта с центровкой на историческом паттерне         |
//+------------------------------------------------------------------+
long ShowPatternWindow(string name,int x,int y,int width,int height,const SPattern &pattern)
  {
//--- Вычисляем актуальный индекс бара по времени из структуры
   int idx=iBarShift(_Symbol,_Period,pattern.time);
//--- Если бар не найден в доступной истории - выходим
   if(idx==WRONG_VALUE)
     {
      PrintFormat("%s: Bar with time %s not found in available history",__FUNCTION__,TimeToString(pattern.time));
      return WRONG_VALUE;
     }

//--- Проверяем наличие объекта окна, создаём, если его нет
   if(ObjectFind(0,name)<0)
     {
      if(!ObjectCreate(ChartID(),name,OBJ_CHART,0,0,0))
        {
         PrintFormat("%s: ObjectCreate(\"%s\",OBJ_CHART) failed",__FUNCTION__,name);
         return WRONG_VALUE;
        }
      
      //--- Устанавливаем свойства окна объекта-графика
      ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
      ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
      ObjectSetInteger(0,name,OBJPROP_XSIZE,width);
      ObjectSetInteger(0,name,OBJPROP_YSIZE,height);
     }
   
//--- Управление объектом графиком через его идентификатор
   long id=ObjectGetInteger(ChartID(),name,OBJPROP_CHART_ID);
   if(id>WRONG_VALUE)
     {
      //--- Синхронизируем символ и период графика
      if(ChartSymbol(id)!=_Symbol || ChartPeriod(id)!=_Period)
         ChartSetSymbolPeriod(id,_Symbol,_Period);

      //--- Свойства графика объекта
      ChartSetInteger(id,CHART_AUTOSCROLL,false);
      ChartSetInteger(id,CHART_SHIFT,false);
      ChartSetInteger(id,CHART_SCALE,ChartGetInteger(ChartID(),CHART_SCALE));
      
      //--- Устанавливаем графическую метку на изломе ZZ
      double price=(pattern.type==POSITION_TYPE_SELL) ? pattern.steps[0].high : pattern.steps[0].low;
      CreateMarker(id,pattern.time,price,pattern.type);
      
      //--- Получаем количество видимых баров для позиции метки излома ZZ
      int visible_bars=(int)ChartGetInteger(id,CHART_VISIBLE_BARS);
      //--- Если статистика выводится на подложку, то позиция излома ZZ находится во второй трети окна
      int center=visible_bars/(InpStatsBack ? 3 : 2);
      
      //--- Рассчитываем итоговое смещение для центровки излома ZZ относительно конца истории
      int offset=idx-center;
      if(offset<0)
         offset=0;

      //--- Смещаем график от конца истории на рассчитанное смещение
      ChartNavigate(id,CHART_END,-offset);
      
      //--- Перерисовываем график объекта
      ChartRedraw(id);
     }
//--- Возвращаем идентификатор графика объекта
   return id;
  }

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

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

//+------------------------------------------------------------------+
//| Создает или перемещает вертикальную линию начала анализа         |
//+------------------------------------------------------------------+
void CreateAnalysisLine(const datetime time,const color clr)
  {
//--- Формируем имя объекта
   string name=prefix+"_AnalysisLine";

//--- Если линии нет - создаем её
   if(ObjectFind(0,name)<0)
     {
      if(!ObjectCreate(0,name,OBJ_VLINE,0,time,0))
        {
         PrintFormat("%s: ObjectCreate(%s,OBJ_VLINE) failed",__FUNCTION__,name);
         return;
        }
      //--- Устанавливаем свойства линии
      ObjectSetInteger(0,name,OBJPROP_STYLE,STYLE_DOT);
      ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
      ObjectSetInteger(0,name,OBJPROP_RAY,true);
      ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
      ObjectSetInteger(0,name,OBJPROP_BACK,false);
     }
   
//--- Обновляем положение, цвет и тултип
   ObjectSetInteger(0,name,OBJPROP_TIME,time);
   ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
   string tooltip=StringFormat("The beginning of the search for\nZZ patterns (%s)",TimeToString(time));
   ObjectSetString(0,name,OBJPROP_TOOLTIP,tooltip);
   
//--- Перерисовываем основной график
   ChartRedraw(0);
  }

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

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

//+------------------------------------------------------------------+
//| Формирует базу паттернов из истории                              |
//+------------------------------------------------------------------+
bool CreatePatternBase(const int attempts_limit,   // Максимальное количество попыток сбора
                       const int pattern_size,     // Глубина паттерна в барах
                       const int hist_limit,       // Ограничение глубины поиска по истории
                       int      &attempts_cnt,     // Счётчик выполненных попыток
                       SPattern &base_array[])     // Массив для хранения собранных паттернов
  {
   static bool warned=false;  // было ли сообщение в журнал

//--- Если лимит попыток исчерпан - выходим
   if(attempts_cnt>=attempts_limit)
     {
      if(!warned)
        {
         PrintFormat("%s: Error. Maximum attempts (%d) reached. Check data",__FUNCTION__,attempts_limit);
         warned=true;
        }
      return false;
     }

//--- Увеличиваем счётчик и пробуем собрать базу
   attempts_cnt++;
   PrintFormat("%s: Attempt %d to collect pattern base...",__FUNCTION__,attempts_cnt);

//--- Если данные не получены - очищаем массив и ждем следующей попытки
   if(!CollectHistoryData(pattern_size,hist_limit,base_array))
     {
      ArrayFree(base_array);
      return false;
     }

//--- База успешно сформирована
   return true;
  }

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

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

//+------------------------------------------------------------------+
//| Осуществляет поиск совпадений текущего рынка с базой паттернов   |
//+------------------------------------------------------------------+
void FindMatches(const int      pattern_depth,  // Глубина паттерна в барах
                 const double   total_sim,      // Порог сходства паттерна (0-100%)
                 const SPattern&base_array[],   // База исторических паттернов
                 SMatchResult  &result,         // Структура для записи результатов
                 const int      start_idx,      // Индекс бара для снятия слепка
                 const datetime max_time,       // Время отсечки будущего
                 const double   min_candle_sim, // Сходство свечей (0-100%)
                 const double   min_pos_sim,    // Сходство относительного расположения (0-100%)
                 const int      max_grade_diff) // Максимальная разница по градациям
  {
//--- Если передан пустой массив - выходим
   int total=ArraySize(base_array);
   if(total==0)
     {
      PrintFormat("%s: Error. Empty array passed",__FUNCTION__);
      return;
     }
     
//--- Обнуляем структуру перед заполнением
   ZeroMemory(result);
   result.best_idx=WRONG_VALUE;
     
//--- Получаем текущий паттерн ("слепок" рынка) от указанного индекса
   SPattern curr_pattern;
   if(!GetCurrentPattern(curr_pattern,start_idx,pattern_depth))
      return;
      
   double max_res=0;
   double sum_buy=0, sum_sell=0;

//--- Проходим по всей базе паттернов
   for(int i=0; i<total; i++)
     {
      //--- Пропускаем паттерны, которые "еще в будущем" относительно точки анализа
      if(base_array[i].time>=max_time)
         continue;

      //--- Сравниваем текущий паттерн с историческим образцом
      double res=ComparePatterns(base_array[i],curr_pattern,min_candle_sim,min_pos_sim,max_grade_diff);
      
      //--- Если сходство ниже порога - идём далее
      if(res<total_sim)
         continue;
         
      //--- Классифицируем совпадение по типу эталона
      //--- Бычий исторический паттерн
      if(base_array[i].type==POSITION_TYPE_BUY)
        {
         sum_buy+=res;
         result.cnt_buy++;
        }
      //--- Медвежий исторический паттерн
      else if(base_array[i].type==POSITION_TYPE_SELL)
        {
         sum_sell+=res;
         result.cnt_sell++;
        }

      //--- Фиксируем лучший результат
      if(res>max_res)
        {
         max_res=res;
         result.best_idx=i;
        }
     }

//--- Рассчитываем итоговую статистику по найденным совпадениям
   int total_matches=result.cnt_buy+result.cnt_sell;
   if(total_matches>0)
     {
      result.avg_buy =(result.cnt_buy > 0 ? sum_buy /result.cnt_buy  : 0);
      result.avg_sell=(result.cnt_sell> 0 ? sum_sell/result.cnt_sell : 0);
      
      //--- Формируем текст доминирующего сигнала
      result.signal=(result.cnt_buy>result.cnt_sell) ? "PREDICT BUY" : (result.cnt_sell>result.cnt_buy) ? "PREDICT SELL" : "UNCERTAIN";
     }
  }

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

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

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

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

//+------------------------------------------------------------------+
//| Выполняет поиск совпадений и выводит результаты визуально        |
//+------------------------------------------------------------------+
void FindAndDisplayMatches(const int      pattern_depth,    // Глубина паттерна в барах
                           const double   total_sim,        // Порог сходства паттерна (0-100%)
                           const double   min_candle_sim,   // Сходство свечей (0-100%)
                           const double   min_pos_sim,      // Минимально допустимый процент сходства позиции
                           const int      max_grade_diff,   // Максимальная разница по градациям
                           const SPattern &base_array[],    // База исторических паттернов
                           SMatchResult   &match_res,       // Структура для записи результатов поиска
                           const string   wnd_name,         // Имя вложенного окна чарта
                           const int      wnd_x,            // Координата X окна паттерна
                           const int      wnd_y,            // Координата Y окна паттерна
                           const int      wnd_w,            // Ширина окна паттерна
                           const int      wnd_h,            // Высота окна паттерна
                           const int      stats_x,          // Координата X блока статистики
                           const int      stats_y,          // Координата Y первой строки
                           const int      stats_step,       // Вертикальный шаг между строками
                           const bool     use_back,         // Флаг использования подложки
                           const int      start_idx,        // Индекс бара начала поиска
                           const color    line_clr)         // Цвет линии анализа (ДОБАВЛЕНО)
  {
//--- Определяем время выбранного бара для фильтрации паттернов справа от времени бара
   ResetLastError();
   datetime max_time=iTime(_Symbol,_Period,start_idx);
   int err=GetLastError();
   if(max_time==0 && err!=0)
     {
      PrintFormat("%s: iTime(%s,%s,%d) failed. Error %d",
                  __FUNCTION__,_Symbol,StringSubstr(EnumToString(_Period),7),start_idx,err);
      return;
     }

//--- Выполняем поиск совпадений с учетом временной отсечки
   FindMatches(pattern_depth,total_sim,base_array,match_res,start_idx,max_time,min_candle_sim,min_pos_sim,max_grade_diff);

//--- Рисуем вертикальную линию начала анализа на основном графике
   CreateAnalysisLine(max_time,line_clr);

//--- Если найдено подходящее совпадение - визуализируем его
   if(match_res.best_idx>WRONG_VALUE)
     {
      //--- Создаем/обновляем вложенное окно чарта
      long id=ShowPatternWindow(prefix+wnd_name,wnd_x,wnd_y,wnd_w,wnd_h,base_array[match_res.best_idx]);
      
      //--- Если идентификатор окна получен - выводим в него блок статистики
      if(id>WRONG_VALUE)
         DisplayStatistics(id,match_res.signal,
                              match_res.cnt_buy,
                              match_res.cnt_sell,
                              match_res.avg_buy,
                              match_res.avg_sell,
                              base_array[match_res.best_idx],
                              match_res.best_idx,
                              base_array.Size(),
                              stats_x,
                              stats_y,
                              stats_step,
                              use_back);
     }
//--- Если совпадений нет - выведем в лог сообщение
   else
      PrintFormat("%s: No matches found for the configuration at %s",__FUNCTION__,TimeToString(max_time));
  }

Принцип работы функции строится на трех этапах. Сначала функция определяет временную точку отсчета — либо это текущий момент, либо бар, выбранный вручную. От этой точки снимается "цифровой слепок" рынка и запускается движок поиска FindMatches, который просматривает историческую базу на предмет совпадений.

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

В итоге во вложенном графике мы видим сам исторический паттерн, а на панели этого графика видим направление сигнала и статистические данные о количестве найденных подходящих паттернов.

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

//+------------------------------------------------------------------+
//| Обрабатывает выбор бара на графике для ручного поиска            |
//+------------------------------------------------------------------+
void ChartClickHandler(const long lparam,const double dparam,int &start_idx)
  {
//--- Проверяем, зажата ли клавиша Ctrl в момент клика
   if(TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)<0)
     {
      datetime click_time=0;
      double   click_price=0;
      int      sub_window=0;
      
      //--- Преобразуем координаты клика во время и цену
      ResetLastError();
      if(!ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,sub_window,click_time,click_price))
        {
         PrintFormat("%s: ChartXYToTimePrice() failed. Error %d",__FUNCTION__,GetLastError());
         return;
        }
      //--- Находим индекс бара по времени клика
      int bar_idx=iBarShift(_Symbol,_Period,click_time);
      if(bar_idx==WRONG_VALUE)
        {
         PrintFormat("%s: Error. iBarShift() failed for time %s",__FUNCTION__,TimeToString(click_time));
         return;
        }
      //--- Записываем новый индекс в переменную, переданную по ссылке
      start_idx=bar_idx;
      
      //--- Отобразим в журнале событие
      PrintFormat("%s: Manual mode. Analysis bar changed to: %d (%s)",
                  __FUNCTION__,start_idx,TimeToString(click_time));
      
      //--- Запускаем поиск и визуализацию от выбранного бара на графике
      FindAndDisplayMatches(ExtPatternDepth,ExtTotalSim,ExtMinCandleSim,ExtMinPosSim,ExtMaxGradeDiff,patterns,match_result,prefix+"BestMatchWindow",
                            InpWndX,InpWndY,InpWndW,InpWndH,InpStatsX,InpStatsY,InpStatsStep,InpStatsBack,start_idx,InpAnalysColor);
     }
  }  

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

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

Полностью код советника можно посмотреть в прикрепленных к статье файлах.

Для успешной компиляции советника, файл индикатора CandleCode.mq5 нужно положить в папку советника \MQL5\Experts\STOCKS_COMMODITIES\Viktor_Likhovidov_Coding_Candlestic\. Скомпилируем советник и запустим его на графике, установив в настройках небольшие значения для поиска паттернов:

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

Что ж, это выглядит интересным...

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

Давайте разберем немного настройки советника, и на что они влияют.

  1. Масштаб ZigZag (InpZZDepth). Параметр определяет размер базы паттернов.
    При значениях в пределах 12–24 советник будет запоминать только крупные развороты, игнорируя мелкий шум. Это даст меньше сигналов, но они будут иметь больший потенциал движения цены.
    При значениях до 5–8 советник начнет собирать базу на микро-откатах, что позволит ловить быстрые движения внутри дня.

  2. Глубина паттерна (InpPatternDepth). Это количество свечей, которые формируют паттерн.
    Значения 5 достаточно, чтобы описать логику разворота (например, подход к уровню, ложный пробой и подтверждение).
    Значения 8–10 можно использовать для поиска сложных формаций. Чем длиннее паттерн, тем сложнее найти его даже приблизительную копию в истории, но тем выше вероятность, что найденный аналог отработает так же.

  3. Порог сходства паттернов (InpTotalSim). Самый важный фильтр, отсеивающий не похожие на оригинал паттерны.
    Не нужно ставить 100%. Рынок никогда не повторяется один-в-один. Если выставить порог выше 90%, советник может не найти ни одного совпадения за несколько лет.
    Можно начать с 60%. Если совпадений слишком много, и они сомнительные, нужно постепенно увеличивать значение на 5%, и т.д. до получения приемлемого результата.

  4. Допуск по форме (InpMaxGradeDiff). Параметр "строгости" к внешнему виду свечей.
    Оптимальное значение 1. Позволяет найти паттерн, если, например, в истории у свечи была "средняя" тень, а сейчас она "чуть больше средней".
    При значении 0, свечи должны быть практически идентичны по градации Лиховидова.
    Значения 2, 3 определяют похожими практически сильно разные свечи.

  5. Пороги CandleCode (InpCCBBNumDevs). Чувствительность к теням.
    Для волатильных пар можно увеличить до 0.5 – 0.8. Это заставит индикатор игнорировать рыночный шум и помечать как "крупные" только действительно значимые тени.
    Для спокойных пар подходит значение 0.3. В узких коридорах даже небольшая на вид тень может быть существенной для анализа.

Практические рекомендации по настройке:

  • Установите советник на график и включите InpManualMode = true.
  • Найдите на истории несколько явных разворотов, не вызывающих сомнений.
  • Удерживайте Ctrl и кликайте по барам перед этими разворотами.
  • Если советник пишет в журнале "No matches found", значит, фильтры установлены слишком строгими, и нужно либо снизить InpTotalSim, либо увеличить InpMaxGradeDiff.
  • Если советник находит паттерны, которые визуально вообще не похожи на текущий, нужно постепенно ужесточать параметры.

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

В исходном коде советника определены, но не используются стандартные обработчики OnTimer(), OnTrade() и OnTradeTransaction(). Они оставлены для самостоятельной разработки торговых функций и обработчиков советника.



Заключение

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

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

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

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

Программы, используемые в статье:

#
 ИмяТип
Описание
 1 CandleCode.mqh Индикатор Индикатор Лиховидова CandleCode
 2 CandleCodeColorHist.mqh Индикатор Индикатор Лиховидова CandleCode в виде гистограммы
 3 ExpCodePatternFinder.mqh Советник Советник для поиска и отображения исторических паттернов, идентичных текущей ситуации
Прикрепленные файлы |
CandleCode.mq5 (7.34 KB)
Моделирование рынка (Часть 18): Первые шаги на SQL (I) Моделирование рынка (Часть 18): Первые шаги на SQL (I)
Неважно, какую программу SQL мы будем использовать: MySQL, SQL Server, SQLite, OpenSQL или другую. У всех есть что-то общее, а этот общий элемент — язык SQL. Даже если мы не собираемся использовать WorkBench, можно манипулировать или работать с базой данных непосредственно в MetaEditor или через MQL5 для выполнения действий в MetaTrader 5, но для этого вам понадобятся знания SQL. Итак, здесь мы выучим, как минимум, основы.
Моделирование рынка (Часть 17): Сокеты (XI) Моделирование рынка (Часть 17): Сокеты (XI)
Реализация той части кода, которая будет работать в MetaTrader 5, не представляет сложности. Однако есть несколько моментов, которые нужно учитывать. Это необходимо для того, чтобы вы смогли заставить систему работать. Запомните одну важную вещь: будет запущена не одна программа. В реальности нам придётся запускать три программы одновременно. Важно реализовать и построить каждую из них так, чтобы они могли взаимодействовать и общаться одна с другой, и чтобы каждая из них понимала, что пытается или хочет сделать другая.
Возможности Мастера MQL5, которые вам нужно знать (Часть 65): Использование паттернов FrAMA и индекса силы Возможности Мастера MQL5, которые вам нужно знать (Часть 65): Использование паттернов FrAMA и индекса силы
Фрактальная адаптивная скользящая средняя (FrAMA) и осциллятор индекса силы (Force Index Oscillator) — еще одна пара индикаторов, которые можно использовать совместно в советнике на языке MQL5. Эти два индикатора в некоторой степени дополняют друг друга, поскольку FrAMA — это индикатор следования за трендом, а индекс силы — это осциллятор, основанный на объеме. Как всегда, мы используем Мастер MQL5 для быстрого изучения любого потенциала этих двух инструментов.
Торговые инструменты на MQL5 (Часть 3): Создание панели сканера по нескольким таймфреймам для стратегической торговли Торговые инструменты на MQL5 (Часть 3): Создание панели сканера по нескольким таймфреймам для стратегической торговли
В этой статье мы создадим панель сканера по нескольким таймфреймам на MQL5 для отображения торговых сигналов в режиме реального времени. Мы планируем создать интерактивный грид-интерфейс, реализовать расчеты сигналов с использованием нескольких индикаторов и добавить кнопку закрытия. Статья завершается бэктестингом и стратегическими торговыми преимуществами