Исследование методов свечного анализа (Часть IV): Обновление и дополнение приложения

Alexander Fedosov | 6 мая, 2019

Содержание

Введение

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

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

Обзор обновлений

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

Рис.1 Предыдущий интерфейс вкладки Анализ

Момент 1. Расположение вкладок и размеры.

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

Момент 2. Таблицы результатов тестирования паттернов.

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

Момент 3. Выбор текущего таймфрейма.

Как выяснилось, построение структуры выбора Таймфрейм —> Результаты для всех паттернов ограничивает наглядность результатов тестов, поэтому будет разработан вариант мультитаймфреймового выбора, а также индивидуальный выбор исследуемых паттернов. Это позволит более гибко настраивать работу с паттернами. 

Момент 4. Диапазон выборки.

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

Рис. 2 Обновленный интерфейс вкладки Анализ.

Итак, перечислим решения, исправляющие предыдущие варианты.

Также окно приложения необходимо было увеличить, чтобы отобразить множество новых элементов. Важным новым удобством стало то, что настройка Пороговое значение тренда(рис.3) в пунктах перенесено из вкладки Настройки во вкладки как Анализ, так и Автопоиск. При этом для каждой из этих вкладок эта настройка стала индивидуальной. 


Рис.3 Перемещение настройки Порогового значения тренда

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

Рис. 4 Новая структура таблицы результатов

Теперь рассмотрим, что потребовалось улучшать во второй вкладке Автопоиск, работающей со сгенерированными паттернами.

Момент 1. Настройки в разных вкладках.

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

Рис.5 Обновление функционала на вкладке Автопоиск 

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


Реализация обновлений

Рассмотрим более подробно как были реализованы представленные выше обновления, а также все изменения в расчетах, которые претерпели изменения.

Структура окон. Метод создания главного окна приложения.

Метод CProgram::CreateGUI(), отвечающий за создание графического интерфейса был дополнен:

//+------------------------------------------------------------------+
//| Создаёт графический интерфейс программы                          |
//+------------------------------------------------------------------+
bool CProgram::CreateGUI(void)
  {
//--- Создание панели
   if(!CreateWindow("Pattern Analyzer"))
      return(false);
//--- Создание диалогового окна
   if(!CreateWindowSetting1("Настройки"))
      return(false);
//--- Создание диалогового окна
   if(!CreateWindowSetting2("Настройки диапазона дат"))
      return(false);
//--- Создание диалогового окна
   if(!CreateWindowSetting3("Настройки диапазона дат"))
      return(false);
//--- Завершение создания GUI
   CWndEvents::CompletedGUI();
   return(true);
  }
//+-----------------------------------------------------------------

Это CreateWindowSetting2() и CreateWindowSetting3(), отвечающие за отображение нового инструмента настройки временного диапазона выборки, показанном на рис.1. Также был значительно переработан метод создания главного окна приложения CreateWindow(). Он был разделен на три блока, соответствующие элементам интерфейса каждой из вкладок — Анализ, Автопоиск, Настройки.

//+------------------------------------------------------------------+
//| Вкладка Analyze                                                  |
//+------------------------------------------------------------------+
//--- Создание кнопок набора паттернов
   if(!CreatePatternSet(m_patterns,10,10))
      return(false);
//--- Заголовок таймфреймов
   if(!CreateTFLabel(m_text_labels[1],10,100,0))
      return(false);
//--- Создание кнопок набора таймфреймов
   if(!CreateTimeframeSet(m_timeframes,10,125,0))
      return(false);
//--- Поле поиска фильтра символом 
   if(!CreateSymbolsFilter(m_symb_filter1,m_request1,10,180,0))
      return(false);
//--- Создание кнопки выбора диапазона дат
   if(!CreateDateRange(m_request3,280,180,0))
      return(false);
//--- Создаёт поле ввода порогового значения прибыли
   if(!CreateThresholdValue(m_threshold1,400,180,100,0))
      return(false);
//--- Создание таблицы символов
   if(!CreateSymbTable(m_symb_table1,10,225,0))
      return(false);
//--- Создание таблицы результатов
   if(!CreateTable1(m_table1,120,225,0))
      return(false);

В первой вкладке добавлены методы, отображающие новые элементы интерфейса. 

//+------------------------------------------------------------------+
//| Вкладка AutoSearch                                               |
//+------------------------------------------------------------------+
   if(!CreateTFLabel(m_text_labels[4],10,10,1))
      return(false);
//--- Кнопки
   if(!CreateDualButton(m_buttons[6],m_buttons[7],200,50))
      return(false);
   if(!CreateTripleButton(m_buttons[8],m_buttons[9],m_buttons[10],10,50))
      return(false);
//--- Заголовок таймфреймов
   if(!CreateTFLabel(m_text_labels[5],10,100,1))
      return(false);
//--- Создание кнопок набора таймфреймов
   if(!CreateTimeframeSet(m_timeframes1,10,125,1))
      return(false);
//--- Поля ввода
   if(!CreateSymbolsFilter(m_symb_filter2,m_request2,10,180,1))
      return(false);
//--- Создание кнопки выбора диапазона дат
   if(!CreateDateRange(m_request4,280,180,1))
      return(false);
//--- Создаёт поле ввода порогового значения прибыли
   if(!CreateThresholdValue(m_threshold2,400,180,100,1))
      return(false);
//--- Создание таблицы символов
   if(!CreateSymbTable(m_symb_table2,10,225,1))
      return(false);
//--- Создание таблицы результатов
   if(!CreateTable2(m_table2,120,225,1))
      return(false);

Во вторую  вкладку Автопоиск из вкладки Настройки(рис.5) перенесены методы, отвечающие за отображение элементов выбора размерности генерируемого паттерна CreateTripleButton() и переключаемой опции с повтором и без, метод CreateDualButton(). Также добавлены методы, отвечающие за заголовок таймфрейма и их набор.

//+------------------------------------------------------------------+
//| Вкладка Settings                                                 |
//+------------------------------------------------------------------+
//--- Создание настроек свечей
   if(!CreateCandle(m_pictures[0],m_buttons[0],m_candle_names[0],"Длинная",10,10,"Images\\EasyAndFastGUI\\Candles\\long.bmp"))
      return(false);
   if(!CreateCandle(m_pictures[1],m_buttons[1],m_candle_names[1],"Короткая",104,10,"Images\\EasyAndFastGUI\\Candles\\short.bmp"))
      return(false);
   if(!CreateCandle(m_pictures[2],m_buttons[2],m_candle_names[2],"Волчок",198,10,"Images\\EasyAndFastGUI\\Candles\\spin.bmp"))
      return(false);
   if(!CreateCandle(m_pictures[3],m_buttons[3],m_candle_names[3],"Доджи",292,10,"Images\\EasyAndFastGUI\\Candles\\doji.bmp"))
      return(false);
   if(!CreateCandle(m_pictures[4],m_buttons[4],m_candle_names[4],"Марибозу",386,10,"Images\\EasyAndFastGUI\\Candles\\maribozu.bmp"))
      return(false);
   if(!CreateCandle(m_pictures[5],m_buttons[5],m_candle_names[5],"Молот",480,10,"Images\\EasyAndFastGUI\\Candles\\hammer.bmp"))
      return(false);
//--- Текстовые метки
   if(!CreateTextLabel(m_text_labels[0],10,140))
      return(false);
   if(!CreateTextLabel(m_text_labels[3],300,140))
      return(false);
//--- Поля ввода
   if(!CreateCoef(m_coef1,10,180,"K1",1))
      return(false);
   if(!CreateCoef(m_coef2,100,180,"K2",0.5))
      return(false);
   if(!CreateCoef(m_coef3,200,180,"K3",0.25))
      return(false);
   if(!CreateLanguageSetting(m_lang_setting,10,240,2))
      return(false);
//--- Списки
   if(!CreateListView(300,180))
      return(false);
//---
   if(!CreateCheckBox(m_checkbox1,300+8,160,"Все свечи"))
      return(false);
//--- Статусная строка
   if(!CreateStatusBar(1,26))
      return(false);

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

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

Рис.6 Принцип работы выбора паттернов для анализа

Реализация представлена ниже:

//+------------------------------------------------------------------+
//| Создает набор кнопок паттернов                                   |
//+------------------------------------------------------------------+
bool CProgram::CreatePatternSet(CButton &button[],int x_gap,int y_gap)
  {
   ArrayResize(button,15);
   string pattern_names[15]=
     {
      "Hummer",
      "Invert Hummer",
      "Handing Man",
      "Shooting Star",
      "Engulfing Bull",
      "Engulfing Bear",
      "Harami Cross Bull",
      "Harami Cross Bear",
      "Harami Bull",
      "Harami Bear",
      "Doji Star Bull",
      "Doji Star Bear",
      "Piercing Line",
      "Dark Cloud Cover",
      "All Patterns"
     };
   int k1=x_gap,k2=x_gap,k3=x_gap;
   for(int i=0;i<=14;i++)
     {
      if(i<5)
        {
         CreatePatternButton(button[i],pattern_names[i],k1,y_gap);
         k1+=150;
        }
      else if(i>=5 && i<10)
        {
         CreatePatternButton(button[i],pattern_names[i],k2,y_gap+30);
         k2+=150;
        }
      else if(i>=10 && i<14)
        {
         CreatePatternButton(button[i],pattern_names[i],k3,y_gap+60);
         k3+=150;
        }
      else if(i==14)
        {
         CreatePatternButton(button[i],pattern_names[i],k3,y_gap+60);
        }
     }
   return(true);
  }
//+------------------------------------------------------------------+
//| Создает кнопку выбора исследуемого паттерна                      |
//+------------------------------------------------------------------+
#resource "\\Images\\EasyAndFastGUI\\Candles\\passive.bmp"
#resource "\\Images\\EasyAndFastGUI\\Candles\\pressed.bmp"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::CreatePatternButton(CButton &button,const string candlename,const int x_gap,const int y_gap)
  {
//--- Сохраним указатель на главный элемент
   button.MainPointer(m_tabs1);
//--- Закрепить за вкладкой
   m_tabs1.AddToElementsArray(0,button);
//--- Свойства
   button.XSize(120);
   button.YSize(20);
   button.Font("Trebuchet");
   button.FontSize(9);
   button.LabelColor(clrWhite);
   button.LabelColorHover(clrWhite);
   button.LabelColorPressed(clrWhite);
   button.IsCenterText(true);
   button.TwoState(true);
   button.IconFile("Images\\EasyAndFastGUI\\Candles\\passive.bmp");
   button.IconFilePressed("Images\\EasyAndFastGUI\\Candles\\pressed.bmp");
//--- Создадим элемент управления
   if(!button.CreateButton(candlename,x_gap,y_gap))
      return(false);
//--- Добавим указатель на элемент в базу
   CWndContainer::AddToElementsArray(0,button);
   return(true);
  }

При этом обращаю внимание на то, что последней кнопкой является кнопка выбора/снятия выбора всех паттернов All Patterns. Для обработки ее нажатия используется дополнительный код в секции обработки событий нажатия кнопок:

   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- Выделение и снятие всех кнопок паттернов
      if(lparam==m_patterns[14].Id())
        {
         if(m_patterns[14].IsPressed())
           {
            for(int i=0;i<14;i++)
               m_patterns[i].IsPressed(true);
           }
         else if(!m_patterns[14].IsPressed())
           {
            for(int i=0;i<14;i++)
               m_patterns[i].IsPressed(false);
           }
         for(int i=0;i<14;i++)
            m_patterns[i].Update(true);
        }
...
}

Метод выбора текущих таймфреймов CreateTimeframeSet() очень схож с предыдущим. Тоже имеет набор переключаемых кнопок, служащих выбором исследуемых таймфреймов.

Рис.7 Принцип работы выбора таймфреймов для анализа

Реализация представлена в листинге ниже:

//+------------------------------------------------------------------+
//| Создает набор кнопок таймфреймов                                 |
//+------------------------------------------------------------------+
bool CProgram::CreateTimeframeSet(CButton &button[],int x_gap,int y_gap,const int tab)
  {
   ArrayResize(button,22);
   string timeframe_names[22]=
     {"M1","M2","M3","M4","M5","M6","M10","M12","M15","M20","M30","H1","H2","H3","H4","H6","H8","H12","D1","W1","MN","ALL"};
   int k1=x_gap,k2=x_gap;
   for(int i=0;i<22;i++)
     {
      CreateTimeframeButton(button[i],timeframe_names[i],k1,y_gap,tab);
      k1+=33;
     }
   return(true);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::CreateTimeframeButton(CButton &button,const string candlename,const int x_gap,const int y_gap,const int tab)
  {
//--- Сохраним указатель на главный элемент
   button.MainPointer(m_tabs1);
//--- Закрепить за вкладкой
   m_tabs1.AddToElementsArray(tab,button);
//--- Свойства
   button.XSize(30);
   button.YSize(30);
   button.Font("Trebuchet");
   button.FontSize(10);
   button.LabelColor(clrWhite);
   button.LabelColorHover(clrWhite);
   button.LabelColorPressed(clrWhite);
   button.BackColor(C'200,200,200');
   button.BackColorHover(C'200,200,200');
   button.BackColorPressed(C'50,180,75');
   button.BorderColor(C'200,200,200');
   button.BorderColorHover(C'200,200,200');
   button.BorderColorPressed(C'50,180,75');
   button.IsCenterText(true);
   button.TwoState(true);
//--- Создадим элемент управления
   if(!button.CreateButton(candlename,x_gap,y_gap))
      return(false);
//--- Добавим указатель на элемент в базу
   CWndContainer::AddToElementsArray(0,button);
   return(true);
  }

Он также имеет кнопку выбора/снятия выбора всех таймфреймов и обрабатывается в секции нажатия кнопок:

//--- Выделение и снятие всех кнопок таймфреймов
      if(lparam==m_timeframes[21].Id())
        {
         if(m_timeframes[21].IsPressed())
           {
            for(int i=0;i<21;i++)
               m_timeframes[i].IsPressed(true);
           }
         else if(!m_timeframes[21].IsPressed())
           {
            for(int i=0;i<21;i++)
               m_timeframes[i].IsPressed(false);
           }
         for(int i=0;i<21;i++)
            m_timeframes[i].Update(true);
        }

Следующим новым элементом является кнопка Диапазон дат. Она является частью составного нового инструмента настройки диапазона выборки. За нее отвечает метод CreateDateRange().

Рис.8 Принцип работы выбора диапазона дат для анализа

Её реализация представлена ниже:

//+------------------------------------------------------------------+
//| Создаёт кнопку для отображения окна выборки диапазона дат        |
//+------------------------------------------------------------------+
bool CProgram::CreateDateRange(CButton &button,const int x_gap,const int y_gap,const int tab)
  {
//--- Сохранить указатель на главный элемент
   button.MainPointer(m_tabs1);
//--- Закрепить за вкладкой
   m_tabs1.AddToElementsArray(tab,button);
//--- Свойства
   button.XSize(100);
   button.YSize(25);
   button.Font("Trebuchet");
   button.FontSize(10);
   button.IsHighlighted(false);
   button.IsCenterText(true);
   button.BorderColor(C'0,100,255');
   button.BackColor(clrAliceBlue);
//--- Создадим элемент управления
   if(!button.CreateButton("",x_gap,y_gap))
      return(false);
//--- Добавим указатель на элемент в базу
   CWndContainer::AddToElementsArray(0,button);
   return(true);
  }

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

      //---
      if(lparam==m_request3.Id())
        {
         int x=m_request3.X();
         int y=m_request3.Y()+m_request3.YSize();
         m_window[2].X(x);
         m_window[2].Y(y);
         m_window[2].OpenWindow();
         val=(m_lang_index==0)?"Настройки диапазона дат":"Date Range Settings";
         m_window[2].LabelText(val);
        }

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

Структура окон. Метод создания диалогового окна приложения.

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

//+------------------------------------------------------------------+
//| Создаёт диалоговой окно выбора диапазона дат во вкладке Анализ   |
//+------------------------------------------------------------------+
bool CProgram::CreateWindowSetting2(const string caption_text)
  {
//--- Добавим указатель окна в массив окон
   CWndContainer::AddWindow(m_window[2]);
//--- Координаты
   int x=m_request3.X();
   int y=m_request3.Y()+m_request3.YSize();
//--- Свойства
   m_window[2].XSize(372);
   m_window[2].YSize(300);
   m_window[2].WindowType(W_DIALOG);

//--- Создание формы
   if(!m_window[2].CreateWindow(m_chart_id,m_subwin,caption_text,x,y))
      return(false);
//---
   if(!CreateCalendar(m_calendar1,m_window[2],10,25,D'01.01.2018',2))
      return(false);
   if(!CreateCalendar(m_calendar2,m_window[2],201,25,m_calendar2.Today(),2))
      return(false);
//---
   if(!CreateTimeEdit(m_time_edit1,m_window[2],10,200,"Время",2))
      return(false);
   if(!CreateTimeEdit(m_time_edit2,m_window[2],200,200,"Время",2))
      return(false);
//---
   return(true);
  }


Расчетная часть. Переработанные методы поиска свечей и паттернов.

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

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

//+------------------------------------------------------------------+
//| Изменение символа во вкладке Анализ                              |
//+------------------------------------------------------------------+
bool CProgram::ChangeSymbol1(const long id)
  {
//--- Проверка идентификатора элемента
   if(id!=m_symb_table1.Id())
      return(false);
//--- Выйти, если строка не выделена
   if(m_symb_table1.SelectedItem()==WRONG_VALUE)
     {
      //--- Показать полное описание символа в статусной строке
      m_status_bar.SetValue(0,"Не выбран символ для анализа");
      m_status_bar.GetItemPointer(0).Update(true);
      return(false);
     }
//--- Получим символ
   string symbol=m_symb_table1.GetValue(0,m_symb_table1.SelectedItem());
//--- Показать полное описание символа в статусной строке
   string val=(m_lang_index==0)?"Выбранный символ: ":"Selected symbol: ";
   m_status_bar.SetValue(0,val+::SymbolInfoString(symbol,SYMBOL_DESCRIPTION));
   m_status_bar.GetItemPointer(0).Update(true);
//---
   GetPatternType(symbol);
   return(true);
  }
//+------------------------------------------------------------------+
//| Изменение символа во вкладке Автопоиск                           |
//+------------------------------------------------------------------+
bool CProgram::ChangeSymbol2(const long id)
  {
//--- Проверка идентификатора элемента
   if(id!=m_symb_table2.Id())
      return(false);
//--- Выйти, если строка не выделена
   if(m_symb_table2.SelectedItem()==WRONG_VALUE)
     {
      //--- Показать полное описание символа в статусной строке
      m_status_bar.SetValue(0,"Не выбран символ для анализа");
      m_status_bar.GetItemPointer(0).Update(true);
      return(false);
     }
//--- Получим символ
   string symbol=m_symb_table2.GetValue(0,m_symb_table2.SelectedItem());
//--- Показать полное описание символа в статусной строке
   string val=(m_lang_index==0)?"Выбранный символ: ":"Selected symbol: ";
   m_status_bar.SetValue(0,val+::SymbolInfoString(symbol,SYMBOL_DESCRIPTION));
   m_status_bar.GetItemPointer(0).Update(true);
//---
   if(!GetCandleCombitation())
     {
      if(m_lang_index==0)
         MessageBox("Число выбранных свечей меньше размера исследуемого паттерна!","Ошибка",MB_OK);
      else if(m_lang_index==1)
         MessageBox("The number of selected candles is less than the size of the studied pattern!","Error",MB_OK);
      return(false);
     }
//---
   GetPatternType(symbol,m_total_combination);
   return(true);
  }

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

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

   bool              GetPatternType(const string symbol);

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

//+------------------------------------------------------------------+
//| Распознавание паттернов                                          |
//+------------------------------------------------------------------+
bool CProgram::GetPatternType(const string symbol)
  {
   CANDLE_STRUCTURE cand1,cand2;
//---
   RATING_SET hummer_coef[];
   RATING_SET invert_hummer_coef[];
   RATING_SET handing_man_coef[];
   RATING_SET shooting_star_coef[];
   RATING_SET engulfing_bull_coef[];
   RATING_SET engulfing_bear_coef[];
   RATING_SET harami_cross_bull_coef[];
   RATING_SET harami_cross_bear_coef[];
   RATING_SET harami_bull_coef[];
   RATING_SET harami_bear_coef[];
   RATING_SET doji_star_bull_coef[];
   RATING_SET doji_star_bear_coef[];
   RATING_SET piercing_line_coef[];
   RATING_SET dark_cloud_cover_coef[];
//--- Получение данных по выбранным таймфреймам
   GetTimeframes(m_timeframes,m_cur_timeframes1);
   int total=ArraySize(m_cur_timeframes1);
//--- Проверка на хотя бы один выбранный таймфрейм
   if(total<1)
     {
      if(m_lang_index==0)
         MessageBox("Вы не выбрали рабочий таймфрейм!","Ошибка",MB_OK);
      else if(m_lang_index==1)
         MessageBox("You have not selected a working timeframe!","Error",MB_OK);
      return(false);
     }
   int count=0;
   m_total_row=0;
   m_table_number=1;
//--- Удалить все строки
   m_table1.DeleteAllRows();
//--- Получение диапазона дат
   datetime start=StringToTime(TimeToString(m_calendar1.SelectedDate(),TIME_DATE)+" "+(string)m_time_edit1.GetHours()+":"+(string)m_time_edit1.GetMinutes()+":00");
   datetime end=StringToTime(TimeToString(m_calendar2.SelectedDate(),TIME_DATE)+" "+(string)m_time_edit2.GetHours()+":"+(string)m_time_edit2.GetMinutes()+":00");
//--- Проверка правильности установленных дат
   if(start>end || end>TimeCurrent())
     {
      if(m_lang_index==0)
         MessageBox("Неправильно выбран диапазон дат!","Ошибка",MB_OK);
      else if(m_lang_index==1)
         MessageBox("Incorrect date range selected!","Error",MB_OK);
      return(false);
     }
//--- Молот бычья модель
   if(m_patterns[0].IsPressed())
     {
      ArrayResize(m_hummer_total,total);
      ArrayResize(hummer_coef,total);
      ZeroMemory(m_hummer_total);
      ZeroMemory(hummer_coef);
      ZeroMemory(cand1);
      count++;
      //--- Расчет по таймфреймам
      for(int j=0;j<total;j++)
        {
         MqlRates rt[];
         ZeroMemory(rt);
         int copied=CopyRates(symbol,m_cur_timeframes1[j],start,end,rt);
         for(int i=0;i<copied;i++)
           {
            GetCandleType(symbol,cand1,m_cur_timeframes1[j],i);             // текущая свеча
            if(cand1.trend==DOWN &&                                        // проверяем направление тренда
               cand1.type==CAND_HAMMER)                                    // проверка "молот"
              {
               m_hummer_total[j]++;
               GetCategory(symbol,i+3,hummer_coef[j],m_cur_timeframes1[j],m_threshold_value1);
              }
           }
         AddRow(m_table1,"Hammer",hummer_coef[j],m_hummer_total[j],m_cur_timeframes1[j]);
        }
     }
...
//---
   if(count>0)
     {
      //---
      m_table1.DeleteRow(m_total_row);
      //--- Обновить таблицу
      m_table1.Update(true);
      m_table1.GetScrollVPointer().Update(true);
     }
   else
     {
      if(m_lang_index==0)
         MessageBox("Вы не выбрали паттерн!","Ошибка",MB_OK);
      else if(m_lang_index==1)
         MessageBox("You have not chosen a pattern!","Error",MB_OK);
     }
   return(true);
  }

Алгоритм работы несложен и работает следующем образом:

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

Если метод поиска паттернов GetPatternType() имеет две различные реализации, то три этих метода универсальны. Рассмотрим их реализацию подробнее:

//+------------------------------------------------------------------+
//| Распознавание типа свечи                                         |
//+------------------------------------------------------------------+
bool CProgram::GetCandleType(const string symbol,CANDLE_STRUCTURE &res,ENUM_TIMEFRAMES timeframe,const int shift)
  {
   MqlRates rt[];
   int aver_period=5;
   double aver=0.0;
   datetime start=TimeCurrent();
   SymbolSelect(symbol,true);
   //--- Получение начальной даты из диапазона выборки в зависимости от типа паттернов
   if(m_table_number==1)
      start=StringToTime(TimeToString(m_calendar1.SelectedDate(),TIME_DATE)+" "+(string)m_time_edit1.GetHours()+":"+(string)m_time_edit1.GetMinutes()+":00");
   else if(m_table_number==2)
      start=StringToTime(TimeToString(m_calendar3.SelectedDate(),TIME_DATE)+" "+(string)m_time_edit3.GetHours()+":"+(string)m_time_edit3.GetMinutes()+":00");
//--- Смещение даты 
   start+=PeriodSeconds(timeframe)*shift;
   int copied=CopyRates(symbol,timeframe,start,aver_period+1,rt);
   if(copied<6)
     {
      Print(start,": Недостаточно данных для расчета — ",GetLastError());
     }
//--- Получаем данные предыдущих свечей
   if(copied<aver_period)
      return(false);
//---
   res.open=rt[aver_period].open;
   res.high=rt[aver_period].high;
   res.low=rt[aver_period].low;
   res.close=rt[aver_period].close;
//--- Определяем направление тренда
   for(int i=0;i<aver_period;i++)
      aver+=rt[i].close;

   aver/=aver_period;

   if(aver<res.close)
      res.trend=UPPER;
   if(aver>res.close)
      res.trend=DOWN;
   if(aver==res.close)
      res.trend=FLAT;
//--- Определяем бычья свеча или медвежья
   res.bull=res.open<res.close;
//--- Получаем абсолютную величину тела свечи
   res.bodysize=MathAbs(res.open-res.close);
//--- Получаем размеры теней
   double shade_low=res.close-res.low;
   double shade_high=res.high-res.open;
   if(res.bull)
     {
      shade_low=res.open-res.low;
      shade_high=res.high-res.close;
     }
   double HL=res.high-res.low;
//--- Вычисляем средний размер тела предыдущих свечей
   double sum=0;
   for(int i=1; i<=aver_period; i++)
      sum+=MathAbs(rt[i].open-rt[i].close);
   sum/=aver_period;

//--- Определяем тип свечи   
   res.type=CAND_NONE;
//--- long 
   if(res.bodysize>sum*m_long_coef && res.bull)
      res.type=CAND_LONG_BULL;
//--- sort 
   if(res.bodysize<sum*m_short_coef && res.bull)
      res.type=CAND_SHORT_BULL;
//--- long bear
   if(res.bodysize>sum*m_long_coef && !res.bull)
      res.type=CAND_LONG_BEAR;
//--- sort bear
   if(res.bodysize<sum*m_short_coef && !res.bull)
      res.type=CAND_SHORT_BEAR;
//--- doji
   if(res.bodysize<HL*m_doji_coef)
      res.type=CAND_DOJI;
//--- maribozu
   if((shade_low<res.bodysize*m_maribozu_coef && shade_high<res.bodysize*m_maribozu_coef) && res.bodysize>0)
      res.type=CAND_MARIBOZU;
//--- hammer
   if(shade_low>res.bodysize*m_hummer_coef2 && shade_high<res.bodysize*m_hummer_coef1)
      res.type=CAND_HAMMER;
//--- invert hammer
   if(shade_low<res.bodysize*m_hummer_coef1 && shade_high>res.bodysize*m_hummer_coef2)
      res.type=CAND_INVERT_HAMMER;
//--- spinning top
   if((res.type==CAND_SHORT_BULL || res.type==CAND_SHORT_BEAR) && shade_low>res.bodysize*m_spin_coef && shade_high>res.bodysize*m_spin_coef)
      res.type=CAND_SPIN_TOP;
//---
   ArrayFree(rt);
   return(true);
  }

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

Важное отступление! Необходимо следить за доступностью исторических данных в терминале MetaTrader 5, иначе приложение может работать неверно. 

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

Метод GetCategory(), как мы знаем из предыдущих статей, проверяет на истории, как ведет себя цена после появившегося паттерна.

//+------------------------------------------------------------------+
//| Определение категорий прибыли                                    |
//+------------------------------------------------------------------+
bool CProgram::GetCategory(const string symbol,const int shift,RATING_SET &rate,ENUM_TIMEFRAMES timeframe,int threshold)
  {
   MqlRates rt[];
   datetime start=TimeCurrent();
   if(m_table_number==1)
      start=StringToTime(TimeToString(m_calendar1.SelectedDate(),TIME_DATE)+" "+(string)m_time_edit1.GetHours()+":"+(string)m_time_edit1.GetMinutes()+":00");
   else if(m_table_number==2)
      start=StringToTime(TimeToString(m_calendar3.SelectedDate(),TIME_DATE)+" "+(string)m_time_edit3.GetHours()+":"+(string)m_time_edit3.GetMinutes()+":00");
   start+=PeriodSeconds(timeframe)*shift;
   int copied=CopyRates(symbol,timeframe,start,4,rt);
//--- Получаем данные предыдущих свечей
   if(copied<4)
     {
      return(false);
     }
   double high1,high2,high3,low1,low2,low3,close0,point;
   close0=rt[0].close;
   high1=rt[1].high;
   high2=rt[2].high;
   high3=rt[3].high;
   low1=rt[1].low;
   low2=rt[2].low;
   low3=rt[3].low;
   if(!SymbolInfoDouble(symbol,SYMBOL_POINT,point))
      return(false);

//--- Проверка на Uptrend
   if((int)((high1-close0)/point)>=threshold)
     {
      rate.a_uptrend++;
     }
   else if((int)((high2-close0)/point)>=threshold)
     {
      rate.b_uptrend++;
     }
   else if((int)((high3-close0)/point)>=threshold)
     {
      rate.c_uptrend++;
     }

//--- Проверка на Downtrend
   if((int)((close0-low1)/point)>=threshold)
     {
      rate.a_dntrend++;
     }
   else if((int)((close0-low2)/point)>=threshold)
     {
      rate.b_dntrend++;
     }
   else if((int)((close0-low3)/point)>=threshold)
     {
      rate.c_dntrend++;
     }
   return(true);
  }

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

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

//+------------------------------------------------------------------+
//| Получение,расчет и вывод информации в таблицу результатов        |
//+------------------------------------------------------------------+
void CProgram::AddRow(CTable &table,string pattern_name,RATING_SET &rate,int found,ENUM_TIMEFRAMES timeframe)
  {
   int row=m_total_row;
   int total_patterns=ArraySize(m_total_combination);
   double p1,p2,k1,k2;
   int sum1=0,sum2=0;
   sum1=rate.a_uptrend+rate.b_uptrend+rate.c_uptrend;
   sum2=rate.a_dntrend+rate.b_dntrend+rate.c_dntrend;
//---
   p1=(found>0)?NormalizeDouble((double)sum1/found*100,2):0;
   p2=(found>0)?NormalizeDouble((double)sum2/found*100,2):0;
   k1=(found>0)?NormalizeDouble((m_k1*rate.a_uptrend+m_k2*rate.b_uptrend+m_k3*rate.c_uptrend)/found,3):0;
   k2=(found>0)?NormalizeDouble((m_k1*rate.a_dntrend+m_k2*rate.b_dntrend+m_k3*rate.c_dntrend)/found,3):0;

//---
   table.AddRow(row);
   if(m_table_number==1)
      table.SetValue(0,row,pattern_name);
   else if(m_table_number==2)
     {
      if(row<total_patterns)
         table.SetValue(0,row,m_total_combination[row]);
      else if(row>=total_patterns)
        {
         int i=row-int(total_patterns*MathFloor(double(row)/total_patterns));
         table.SetValue(0,row,m_total_combination[i]);
        }
     }
   table.SetValue(1,row,(string)found);
   table.SetValue(2,row,TimeframeToString(timeframe));
   table.SetValue(3,row,(string)p1,2);
   table.SetValue(4,row,(string)p2,2);
   table.SetValue(5,row,(string)k1,2);
   table.SetValue(6,row,(string)k2,2);
   ZeroMemory(rate);
   m_total_row++;
  }
//+------------------------------------------------------------------+

Метод в своих аргументах получает все данные для расчета и алгоритм их обработки достаточно просто. Примечательны здесь лишь два момента. В предыдущих версиях приложения было изначально известно точное количество строк в таблице результатов. Например, при обработке данных существующих паттернов в таблице результатов всегда было одно и то же количество строк, равных число предустановленных паттернов, равное 14. Теперь же неизвестно,  какое число паттернов или рабочих таймфреймов выберет пользователь. Поэтому был введен простой счетчик строк m_total_row. И при вызове метода AddRow() добавляется строка в таблицу результатов по двум признакам: паттерн и таймфрейм.

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

Рассмотрим теперь второй вариант метода GetPatternType().

bool              GetPatternType(const string symbol,string &total_combination[]);

Здесь, помимо текущего символа, вторым является ссылка на строковый массив комбинаций сгенерированных паттернов. 

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::GetPatternType(const string symbol,string &total_combination[])
  {
   CANDLE_STRUCTURE cand1[],cand2[],cand3[],cur_cand,prev_cand,prev_cand2;
   RATING_SET ratings;
   int total_patterns,m_pattern_total[];
   string elements[];
//---
   total_patterns=ArraySize(total_combination);
   ArrayResize(cand1,total_patterns);
   ArrayResize(cand2,total_patterns);
   ArrayResize(cand3,total_patterns);
   ArrayResize(m_pattern_total,total_patterns);
   ArrayResize(elements,m_pattern_size);
//---
   for(int i=0;i<total_patterns;i++)
     {
      StringReplace(total_combination[i],"[","");
      StringReplace(total_combination[i],"]","");
      if(m_pattern_size>1)
        {
         ushort sep=StringGetCharacter(",",0);
         StringSplit(total_combination[i],sep,elements);
        }
      m_pattern_total[i]=0;
      if(m_pattern_size==1)
         IndexToPatternType(cand1[i],(int)total_combination[i]);
      else if(m_pattern_size==2)
        {
         IndexToPatternType(cand1[i],(int)elements[0]);
         IndexToPatternType(cand2[i],(int)elements[1]);
        }
      else if(m_pattern_size==3)
        {
         IndexToPatternType(cand1[i],(int)elements[0]);
         IndexToPatternType(cand2[i],(int)elements[1]);
         IndexToPatternType(cand3[i],(int)elements[2]);
        }
     }
//---
   GetTimeframes(m_timeframes1,m_cur_timeframes2);
   int total=ArraySize(m_cur_timeframes2);
   if(total<1)
     {
      if(m_lang_index==0)
         MessageBox("Вы не выбрали рабочий таймфрейм!","Ошибка",MB_OK);
      else if(m_lang_index==1)
         MessageBox("You have not selected a working timeframe!","Error",MB_OK);
      return(false);
     }
   m_total_row=0;
   m_table_number=2;
//--- Удалить все строки
   m_table2.DeleteAllRows();
//---
   datetime start=StringToTime(TimeToString(m_calendar3.SelectedDate(),TIME_DATE)+" "+(string)m_time_edit3.GetHours()+":"+(string)m_time_edit3.GetMinutes()+":00");
   datetime end=StringToTime(TimeToString(m_calendar4.SelectedDate(),TIME_DATE)+" "+(string)m_time_edit4.GetHours()+":"+(string)m_time_edit4.GetMinutes()+":00");
//---
   if(start>end || end>TimeCurrent())
     {
      if(m_lang_index==0)
         MessageBox("Неправильно выбран диапазон дат!","Ошибка",MB_OK);
      else if(m_lang_index==1)
         MessageBox("Incorrect date range selected!","Error",MB_OK);
      return(false);
     }
//---
   if(m_pattern_size==1)
     {
      ZeroMemory(cur_cand);
      //--- Расчет по таймфреймам
      for(int i=0;i<total;i++)
        {
         MqlRates rt[];
         ZeroMemory(rt);
         ZeroMemory(ratings);
         int copied=CopyRates(symbol,m_cur_timeframes2[i],start,end,rt);
         //--- Расчет по паттернам
         for(int j=0;j<total_patterns;j++)
           {
            //--- Расчет по диапазону дат         
            for(int k=0;k<copied;k++)
              {
               //--- Получаем тип текущей свечи
               GetCandleType(symbol,cur_cand,m_cur_timeframes2[i],k);                 // текущая свеча
               //---
               if(cur_cand.type==cand1[j].type && cur_cand.bull==cand1[j].bull)
                 {
                  m_pattern_total[j]++;
                  GetCategory(symbol,k+3,ratings,m_cur_timeframes2[i],m_threshold_value2);
                 }
              }
            AddRow(m_table2,"",ratings,m_pattern_total[j],m_cur_timeframes2[i]);
            m_pattern_total[j]=0;
           }
        }
     }
   else if(m_pattern_size==2)
     {
      ZeroMemory(cur_cand);
      ZeroMemory(prev_cand);
      //--- Расчет по таймфреймам
      for(int i=0;i<total;i++)
        {
         MqlRates rt[];
         ZeroMemory(rt);
         ZeroMemory(ratings);
         int copied=CopyRates(symbol,m_cur_timeframes2[i],start,end,rt);
         //--- Расчет по паттернам
         for(int j=0;j<total_patterns;j++)
           {
            //--- Расчет по диапазону дат         
            for(int k=0;k<copied;k++)
              {
               //--- Получаем тип текущей свечи
               GetCandleType(symbol,prev_cand,m_cur_timeframes2[i],k+1);               // предыдущая свеча
               GetCandleType(symbol,cur_cand,m_cur_timeframes2[i],k);                  // текущая свеча
               //---
               if(cur_cand.type==cand1[j].type && cur_cand.bull==cand1[j].bull && 
                  prev_cand.type==cand2[j].type && prev_cand.bull==cand2[j].bull)
                 {
                  m_pattern_total[j]++;
                  GetCategory(symbol,k+4,ratings,m_cur_timeframes2[i],m_threshold_value2);
                 }
              }
            AddRow(m_table2,"",ratings,m_pattern_total[j],m_cur_timeframes2[i]);
            m_pattern_total[j]=0;
           }
        }
     }
   else if(m_pattern_size==3)
     {
      ZeroMemory(cur_cand);
      ZeroMemory(prev_cand);
      ZeroMemory(prev_cand2);
      //--- Расчет по таймфреймам
      for(int i=0;i<total;i++)
        {
         MqlRates rt[];
         ZeroMemory(ratings);
         int copied=CopyRates(symbol,m_cur_timeframes2[i],start,end,rt);
         //--- Расчет по паттернам
         for(int j=0;j<total_patterns;j++)
           {
            //--- Расчет по диапазону дат         
            for(int k=0;k<copied;k++)
              {
               //--- Получаем тип текущей свечи
               GetCandleType(symbol,prev_cand2,m_cur_timeframes2[i],k+2);                                  // предыдущая свеча
               GetCandleType(symbol,prev_cand,m_cur_timeframes2[i],k+1);                                   // предыдущая свеча
               GetCandleType(symbol,cur_cand,m_cur_timeframes2[i],k);                                      // текущая свеча
               //---
               if(cur_cand.type==cand1[j].type && cur_cand.bull==cand1[j].bull && 
                  prev_cand.type==cand2[j].type && prev_cand.bull==cand2[j].bull && 
                  prev_cand2.type==cand3[j].type && prev_cand2.bull==cand3[j].bull)
                 {
                  m_pattern_total[j]++;
                  GetCategory(symbol,k+5,ratings,m_cur_timeframes2[i],m_threshold_value2);
                 }
              }

            AddRow(m_table2,"",ratings,m_pattern_total[j],m_cur_timeframes2[i]);
            m_pattern_total[j]=0;
           }
        }
     }
//---
   m_table2.DeleteRow(m_total_row);
//--- Обновить таблицу
   m_table2.Update(true);
   m_table2.GetScrollVPointer().Update(true);
   return(true);
  }

Алгоритм данной версии метода заключается в том, что необходимо осознать в какой последовательности идет расчет, исходя из вводных данных. Получение выбранных пользователем таймфреймов и диапазона дат опустим, так как это рассматривалось чуть выше. Далее алгоритм проверяет какой размерности паттерны сейчас тестируются. Рассмотрим паттерн размерностью три. После объявления структуры для хранения ценовых данных и обнуления используемой структуры ratings мы входим в первый цикл перебора таймфреймов и для каждого из них получаем количество скопированных данных. Это необходимо, чтобы далее определить в каком диапазоне будет поиск заданных паттернов. После цикла таймфрейма входим в цикл расчета для каждого паттерна на заданном таймфрейме. И далее для каждого из паттернов проходим цикл из числа свечей, определенных в заданном диапазоне дат.

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

Рис.9  Пример расчета и порядок вывода результатов в таблицу

Как видно на рис.9, тест проводился на валютной паре EURUSD с размерностью паттерна 1 на таймфреймах М15, М30, Н1, Н2. Простые свечи для тестирования были выбраны две — с индексами 1 и 2. И в таблице результатов наблюдается реализация алгоритма, описанного чуть выше. А именно: сначала берется 15-минутный таймфрейм и на нем исследуются все сгенерированные по порядку паттерны, затем на 30-минутном таймфрейме и так далее.

Заключение

В конце статьи приложен архив со всеми перечисленными файлами, отсортированными по папкам. Поэтому для корректной работы достаточно положить папку  MQL5 в корень терминала. Для того, чтобы найти корень терминала, в котором находится папка MQL5 нужно в MetaTarder 5 нажать комбинацию клавиш  Ctrl+Shift+D или воспользоваться контекстным меню, как показано на рис.10 ниже.

Рис.10 Поиск папки MQL5 в корне терминала MetaTrader 5.

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

#
 Имя
Тип
Описание
1
PatternAnalyzer.mq5 Графический интерфейс
 Панель инструментов для анализа свечной модели.
2 MainWindow.mqh Библиотека  Библиотека построения графического интерфейса
3 Program.mqh Библиотека  Библиотека методов создания элементов интерфейса и расчетной части

Предыдущие статьи этого цикла:

Исследование методов свечного анализа (Часть I): Проверка существующих паттернов.
Исследование методов свечного анализа (Часть II): Автопоиск новых паттернов.
Исследование методов свечного анализа (Часть III): Библиотека работы с паттернами.