Studying candlestick analysis techniques (part IV): Updates and additions to Pattern Analyzer

Alexander Fedosov | 15 May, 2019

Table of contents

Introduction

In earlier articles within this series we created a MetaTrader 5 app, which tested the relevance of existing candlestick patterns. A later version featured the possibility to create custom patterns based on simple candlestick types, such as short and long candles, doji, spinning top, etc. In the latest part, we developed a library for creating indicators and Expert Advisors based on candlestick patterns.

This article presents a new version of the Pattern Analyzer application. This version provides bug fixes and new features, as well as the revised user interface. Comments and suggestions from previous article were taken into account when developing the new version. The resulting application is described in this article.

Update overview

The user interface is an important part of any application: a well prepared interface structure makes the application use more efficient. We will compare the new application look with the previous one. Let's start with the Analysis tab: why did it need improvements.

Fig.1 Interface of the Analyze tab in the previous version

Point 1. Tab arrangement and dimensions.

In Fig.1, tabs marked by 1 are located in the upper part of the window. The upper right part here is empty and is not used, however the part is not enough to add more tabs. The text font is too small. These three tabs have been moved to the left part of the window: they are now arranged vertically and are more visible. Moreover, there is additional space to add more sections.

Point 2. Tables with pattern testing results.

The visual data presentation is not very efficient. Therefore, the font, the row height and the table size have been increased for better readability.

Point 3. Current timeframe selection.

The selection structure 'Timeframe -> Result' for all patterns limits the visual display of testing results. To improve this, we will develop a multi-timeframe selection option, as well as an individual selection of analyzed patterns. This will allow a more flexible customization of operation with patterns. 

Point 4. Sampling range.

The idea implemented in the previous version was to test in the range from the current data to a certain number of candlesticks in history. A more specific selection from one date to another was not available. Therefore the range selection method will be revised. Fig.2 below features the solution of all the above mentioned issues and possible improvements.

Fig. 2. Updated interface of the Analyze tab.

Here are the solutions to the above points.

The application window has become larger to display new elements. Another important new feature is that the 'Trend threshold value' parameter (Fig.3) in points has been moved from the Settings tab to both Analyze and AutoSearch tabs. The setting is individual for each of the tabs. 


Fig.3 New position of the Trend Threshold parameter

The last element which has been changed is the structure of the Results table. The occurrence column has been removed and a more relevant Timeframe parameter has been added instead. 

Fig. 4. The new structure of the results table

Now let's look at improvements in the second tab, AutoSearch, which works with generated patterns.

Point 1. Settings in different tabs.

Settings directly related to the AutoSearch section were located in Setting tab, so one needed to constantly switch between the tabs AutoSearch and Setting in order to change the settings. That is why almost all settings have been moved to the AutoSearch tab. Further improvements have been implemented in relation to the Threshold trend value, selection of current timeframes and the date range. The result of AutoSearch tab update is shown in Fig.5.

Fig.5 Updated functionality in the AutoSearch tab 

 This enables a more convenient work with patterns. The date range in this tab is also individual.


Implementation of updates

Let us consider in more detail the implementation of the above updates, as well as changes in calculations.

Structure of application windows. Main window creation method.

The CProgram::CreateGUI() method, which is responsible for the graphical interface creation has been supplemented:

//+------------------------------------------------------------------+
//| Creates the graphical interface of the program                   |
//+------------------------------------------------------------------+
bool CProgram::CreateGUI(void)
  {
//--- Creating a panel
   if(!CreateWindow("Pattern Analyzer"))
      return(false);
//--- Creating a dialog window
   if(!CreateWindowSetting1("Settings"))
      return(false);
//--- Creating a dialog window
   if(!CreateWindowSetting2("Date range settings"))
      return(false);
//--- Creating a dialog window
   if(!CreateWindowSetting3("Date range settings"))
      return(false);
//--- Complete GUI creation
   CWndEvents::CompletedGUI();
   return(true);
  }
//+-----------------------------------------------------------------

CreateWindowSetting2() and CreateWindowSetting3() are responsible for the display of the new date range selection tool shown in Fig.1. The main app window creation method CreateWindow() has also been redesigned. It has been divided into three blocks corresponding to the UI elements of each of the tabs: Analyze, AutoSearch and Setting.

//+------------------------------------------------------------------+
//| The Analyze tab                                                  |
//+------------------------------------------------------------------+
//--- Create buttons of the pattern set
   if(!CreatePatternSet(m_patterns,10,10))
      return(false);
//--- Timeframe headers
   if(!CreateTFLabel(m_text_labels[1],10,100,0))
      return(false);
//--- Create buttons of the timeframe set
   if(!CreateTimeframeSet(m_timeframes,10,125,0))
      return(false);
//--- Symbol filter search window 
   if(!CreateSymbolsFilter(m_symb_filter1,m_request1,10,180,0))
      return(false);
//--- Create a button for date range selection
   if(!CreateDateRange(m_request3,280,180,0))
      return(false);
//--- Create an entry field for the threshold profit value
   if(!CreateThresholdValue(m_threshold1,400,180,100,0))
      return(false);
//--- Create a table of symbols
   if(!CreateSymbTable(m_symb_table1,10,225,0))
      return(false);
//--- Create a table of results
   if(!CreateTable1(m_table1,120,225,0))
      return(false);

The methods for displaying new interface elements have been added in the first tab. 

//+------------------------------------------------------------------+
//| The AutoSearch tab                                               |
//+------------------------------------------------------------------+
   if(!CreateTFLabel(m_text_labels[4],10,10,1))
      return(false);
//--- Buttons
   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);
//--- Timeframe headers
   if(!CreateTFLabel(m_text_labels[5],10,100,1))
      return(false);
//--- Create buttons of the timeframe set
   if(!CreateTimeframeSet(m_timeframes1,10,125,1))
      return(false);
//--- Edit fields
   if(!CreateSymbolsFilter(m_symb_filter2,m_request2,10,180,1))
      return(false);
//--- Create a button for date range selection
   if(!CreateDateRange(m_request4,280,180,1))
      return(false);
//--- Create an entry field for the threshold profit value
   if(!CreateThresholdValue(m_threshold2,400,180,100,1))
      return(false);
//--- Create a table of symbols
   if(!CreateSymbTable(m_symb_table2,10,225,1))
      return(false);
//--- Create a table of results
   if(!CreateTable2(m_table2,120,225,1))
      return(false);

Also the following methods have been moved to the second AutoSearch tab from the Setting tab (Fig.5): methods responsible for the display of elements related to the selection of generated pattern sizes CreateTripleButton() and the switchable option Repeat/No repeat with the CreateDualButton() method. New methods have been added: methods responsible for the timeframe header and their selection.

//+------------------------------------------------------------------+
//| The Settings tab                                                 |
//+------------------------------------------------------------------+
//--- Creating candlestick settings
   if(!CreateCandle(m_pictures[0],m_buttons[0],m_candle_names[0],"Long",10,10,"Images\\EasyAndFastGUI\\Candles\\long.bmp"))
      return(false);
   if(!CreateCandle(m_pictures[1],m_buttons[1],m_candle_names[1],"Short",104,10,"Images\\EasyAndFastGUI\\Candles\\short.bmp"))
      return(false);
   if(!CreateCandle(m_pictures[2],m_buttons[2],m_candle_names[2],"Spinning top",198,10,"Images\\EasyAndFastGUI\\Candles\\spin.bmp"))
      return(false);
   if(!CreateCandle(m_pictures[3],m_buttons[3],m_candle_names[3],"Doji",292,10,"Images\\EasyAndFastGUI\\Candles\\doji.bmp"))
      return(false);
   if(!CreateCandle(m_pictures[4],m_buttons[4],m_candle_names[4],"Marubozu",386,10,"Images\\EasyAndFastGUI\\Candles\\maribozu.bmp"))
      return(false);
   if(!CreateCandle(m_pictures[5],m_buttons[5],m_candle_names[5],"Hammer",480,10,"Images\\EasyAndFastGUI\\Candles\\hammer.bmp"))
      return(false);
//--- Text labels
   if(!CreateTextLabel(m_text_labels[0],10,140))
      return(false);
   if(!CreateTextLabel(m_text_labels[3],300,140))
      return(false);
//--- Edit fields
   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);
//--- List views
   if(!CreateListView(300,180))
      return(false);
//---
   if(!CreateCheckBox(m_checkbox1,300+8,160,"All candlesticks"))
      return(false);
//--- Status Bar
   if(!CreateStatusBar(1,26))
      return(false);

The Setting section now contains less elements. It contains individual candlestick settings, settings of ratios for the calculation of probability and efficiency coefficients, interface language selection and candlestick selection for pattern generation in the AutoSearch tab. Next, consider the new methods in more detail. 

Method for creating pattern selection CreatePatternSet(). It is a set of switchable buttons for selecting existing patterns for analysis.

Fig.6 The principle of pattern selector for analysis

The implementation is presented below:

//+------------------------------------------------------------------+
//| Creates a set of pattern buttons                                 |
//+------------------------------------------------------------------+
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);
  }
//+------------------------------------------------------------------+
//| Creates a button for selecting a pattern for analysis            |
//+------------------------------------------------------------------+
#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)
  {
//--- Save the pointer to the main control
   button.MainPointer(m_tabs1);
//--- Attach to tab
   m_tabs1.AddToElementsArray(0,button);
//--- Properties
   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");
//--- Create a control
   if(!button.CreateButton(candlename,x_gap,y_gap))
      return(false);
//--- Add the element pointer to the data base
   CWndContainer::AddToElementsArray(0,button);
   return(true);
  }

Pay attention to the last button 'All Patterns' which selects/unselects all patterns. The button press is processed by additional code in the button press event handing section:

   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- Select and unselect all pattern buttons
      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);
        }
...
}

The current timeframe selection method CreateTimeframeSet() is very similar to the previous one. It also has a set of switchable buttons which select timeframes for analysis.

Fig.7 The principle of timeframe selector for analysis

The implementation is presented in the below code:

//+------------------------------------------------------------------+
//| Creates a set of timeframe buttons                               |
//+------------------------------------------------------------------+
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)
  {
//--- Save the pointer to the main control
   button.MainPointer(m_tabs1);
//--- Attach to tab
   m_tabs1.AddToElementsArray(tab,button);
//--- Properties
   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);
//--- Create a control
   if(!button.CreateButton(candlename,x_gap,y_gap))
      return(false);
//--- Add the element pointer to the data base
   CWndContainer::AddToElementsArray(0,button);
   return(true);
  }

It also has a button for selecting/unselecting all timeframes and is processed in the button-press handling section:

//--- Select and unselect all pattern buttons
      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);
        }

The next new item is the Date Range button. It is part of the new composite sample range setting tool. It is implemented using the CreateDateRange() method.

Fig.8 The principle of date range selector for analysis

The implementation is presented below:

//+------------------------------------------------------------------+
//| Creates a button to show the date range selection window         |
//+------------------------------------------------------------------+
bool CProgram::CreateDateRange(CButton &button,const int x_gap,const int y_gap,const int tab)
  {
//--- Save the pointer to the main control
   button.MainPointer(m_tabs1);
//--- Attach to tab
   m_tabs1.AddToElementsArray(tab,button);
//--- Properties
   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);
//--- Create a control
   if(!button.CreateButton("",x_gap,y_gap))
      return(false);
//--- Add the element pointer to the data base
   CWndContainer::AddToElementsArray(0,button);
   return(true);
  }

The button press event handler also includes code which is responsible for the display of the dialog box with the date range:

      //---
      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);
        }

There is no need to describe new elements which have been added in the tab, since they are similar to the implementation of the Analyze tab elements, with the exception of coordinate parameters. Therefore, let's consider methods responsible for the display of other new windows. 

Structure of application windows. A method for creating the application dialog box.

The methods which display dialog boxes for the Analyze and AutoSearch tabs are similar and thus we will consider one of them.

//+------------------------------------------------------------------+
//| Creates a date range selection dialog in the Analyze tab         |
//+------------------------------------------------------------------+
bool CProgram::CreateWindowSetting2(const string caption_text)
  {
//--- Add the pointer to the window array
   CWndContainer::AddWindow(m_window[2]);
//--- Coordinates
   int x=m_request3.X();
   int y=m_request3.Y()+m_request3.YSize();
//--- Properties
   m_window[2].XSize(372);
   m_window[2].YSize(300);
   m_window[2].WindowType(W_DIALOG);

//--- Create the form
   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,"Time",2))
      return(false);
   if(!CreateTimeEdit(m_time_edit2,m_window[2],200,200,"Time",2))
      return(false);
//---
   return(true);
  }


Calculation part. Redesigned candlestick and pattern finder methods.

Due to serious changes in the user interface structure, as well as due to addition of new elements and deletion of some old ones, the calculation methods have also changed. Two calculation methods are available in the current application: the first one is used for existing patterns and the second method is used for generated patterns. 

Calculation is launched after a click on one of the available trading instruments in the Symbols table. This rule applies to both tabs, Analyze and AutoSearch. One of the two methods is called depending on the tab.

//+------------------------------------------------------------------+
//| Symbol change in the Analyze tab                                 |
//+------------------------------------------------------------------+
bool CProgram::ChangeSymbol1(const long id)
  {
//--- Check the element ID
   if(id!=m_symb_table1.Id())
      return(false);
//--- Exit if the line is not selected
   if(m_symb_table1.SelectedItem()==WRONG_VALUE)
     {
      //--- Show the full symbol description in the status bar
      m_status_bar.SetValue(0,"No symbol selected for analysis");
      m_status_bar.GetItemPointer(0).Update(true);
      return(false);
     }
//--- Get a symbol
   string symbol=m_symb_table1.GetValue(0,m_symb_table1.SelectedItem());
//--- Show the full symbol description in the status bar
   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);
  }
//+------------------------------------------------------------------+
//| Symbol change in the AutoSearch tab                              |
//+------------------------------------------------------------------+
bool CProgram::ChangeSymbol2(const long id)
  {
//--- Check the element ID
   if(id!=m_symb_table2.Id())
      return(false);
//--- Exit if the line is not selected
   if(m_symb_table2.SelectedItem()==WRONG_VALUE)
     {
      //--- Show the full symbol description in the status bar
      m_status_bar.SetValue(0,"No symbol selected for analysis");
      m_status_bar.GetItemPointer(0).Update(true);
      return(false);
     }
//--- Get a symbol
   string symbol=m_symb_table2.GetValue(0,m_symb_table2.SelectedItem());
//--- Show the full symbol description in the status bar
   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);
  }

The GetPattertType() method with two different argument types is called at the end of each method. This is the key method in search for patterns and in handling of obtained results. Now let's consider each of the method in detail.

The first method type is used to search for existing patterns.

   bool              GetPatternType(const string symbol);

The method has quite a lengthy implementation, therefore an example for only one pattern will be provided here.

//+------------------------------------------------------------------+
//| Pattern recognition                                              |
//+------------------------------------------------------------------+
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[];
//--- Receive data for selected timeframes
   GetTimeframes(m_timeframes,m_cur_timeframes1);
   int total=ArraySize(m_cur_timeframes1);
//--- Check at least one selected timerame
   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;
//--- Delete all rows
   m_table1.DeleteAllRows();
//--- Get the date range
   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");
//--- Check specified dates
   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);
     }
//--- Hammer, bullish model
   if(m_patterns[0].IsPressed())
     {
      ArrayResize(m_hummer_total,total);
      ArrayResize(hummer_coef,total);
      ZeroMemory(m_hummer_total);
      ZeroMemory(hummer_coef);
      ZeroMemory(cand1);
      count++;
      //--- Calculation by timeframes
      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);             // Current candlestick
            if(cand1.trend==DOWN &&                                        // Checking the trend direction
               cand1.type==CAND_HAMMER)                                    // Checking the "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);
      //--- Update the table
      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);
  }

The operation algorithm is performed as follows:

Next, we consider the methods which existed in earlier versions and were changed in the updated version, as well as one new method for the calculation of obtained data and their output in the results table:

The GetPatternType() method for pattern search has two different implementations, while these three methods are universal. Let's consider them in detail:

//+------------------------------------------------------------------+
//| Candlestick type recognition                                     |
//+------------------------------------------------------------------+
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);
   //--- Get the start date from the range depending on the type of patterns
   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");
//--- Shift date 
   start+=PeriodSeconds(timeframe)*shift;
   int copied=CopyRates(symbol,timeframe,start,aver_period+1,rt);
   if(copied<6)
     {
      Print(start,": Not enough data for calculation — ",GetLastError());
     }
//--- Get details of the previous candlestick
   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;
//--- Determine the trend direction
   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;
//--- Determine if it is a bullish or a bearish candlestick
   res.bull=res.open<res.close;
//--- Get the absolute size of candlestick body
   res.bodysize=MathAbs(res.open-res.close);
//--- Get sizes of shadows
   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;
//--- Calculate average body size of previous candlesticks
   double sum=0;
   for(int i=1; i<=aver_period; i++)
      sum+=MathAbs(rt[i].open-rt[i].close);
   sum/=aver_period;

//--- Determine the candlestick type   
   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;
//--- marubozu
   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);
  }

The method algorithm is as follows: get the initial date from the selection range depending on the patterns you want to analyze, existing or generated ones. Since this method is used in a calculation cycle in the date range, change the initial date by shifting it from past towards the future by one candlestick of the given timeframe. Then copy data required for the calculation of simple candlestick types. If this data is not enough, display an appropriate message to the user. 

Important note! It is necessary to monitor the availability of historical data in the MetaTrader 5 terminal, otherwise the application may not work correctly. 

If data is enough, a check is performed of whether the current candlestick belongs to a simple candlestick type.

As is known from previous articles, the GetCategory() method checks the price behavior after pattern occurrence using historic data.

//+------------------------------------------------------------------+
//| Determine profit categories                                      |
//+------------------------------------------------------------------+
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);
//--- Get details of the previous candlestick
   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);

//--- Check if it is the 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++;
     }

//--- Check if it is the 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);
  }

In this method algorithm only the method for obtaining data about analyzed candlesticks has changed. This is directly related to the new time range selection tool.

The last common method for both GetPatternType() is getting, calculating and displaying data in the results table. 

//+------------------------------------------------------------------+
//| Get, calculate and display data in the results table             |
//+------------------------------------------------------------------+
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++;
  }
//+------------------------------------------------------------------+

The method receives all data for calculation in its arguments. Data handling algorithm is quite simple. Two moments should be mentioned here. In previous application versions, the exact number of rows was known in advance. For example, when processing existing pattern data, the results table always had the same number of rows equal to the number of preset patterns, i.e. 14. Now, the user can choose any number of patterns or working timeframes and thus the number of rows is not known. Therefore a simple row counter m_total_row was added. The call of the AddRow() method adds a row to the results table based on two signs: pattern and timeframe.

The second point concerns the AutoSearch tab. In previous versions, the finite number of rows was equal to the number of combinations of generated patterns. The previous algorithm is not suitable now for the same reason: the number of timeframes is unknown. Therefore, the entire array of generated combinations should be written again for each of the selected timeframes.

Let's consider the second variant of the GetPatternType() method.

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

Here, in addition to the current symbol, the second parameter is the link to the string array of generated patterns. 

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
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;
//--- Delete all rows
   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);
      //--- Calculation by timeframes
      for(int i=0;i<total;i++)
        {
         MqlRates rt[];
         ZeroMemory(rt);
         ZeroMemory(ratings);
         int copied=CopyRates(symbol,m_cur_timeframes2[i],start,end,rt);
         //--- Calculation by patterns
         for(int j=0;j<total_patterns;j++)
           {
            //--- Calculation by a date range         
            for(int k=0;k<copied;k++)
              {
               //--- Get the current candlestick type
               GetCandleType(symbol,cur_cand,m_cur_timeframes2[i],k);                 // current candlestick
               //---
               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);
      //--- Calculation by timeframes
      for(int i=0;i<total;i++)
        {
         MqlRates rt[];
         ZeroMemory(rt);
         ZeroMemory(ratings);
         int copied=CopyRates(symbol,m_cur_timeframes2[i],start,end,rt);
         //--- Calculation by patterns
         for(int j=0;j<total_patterns;j++)
           {
            //--- Calculation by a date range         
            for(int k=0;k<copied;k++)
              {
               //--- Get the current candlestick type
               GetCandleType(symbol,prev_cand,m_cur_timeframes2[i],k+1);               // previous candlestick
               GetCandleType(symbol,cur_cand,m_cur_timeframes2[i],k);                  // current candlestick
               //---
               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);
      //--- Calculation by timeframes
      for(int i=0;i<total;i++)
        {
         MqlRates rt[];
         ZeroMemory(ratings);
         int copied=CopyRates(symbol,m_cur_timeframes2[i],start,end,rt);
         //--- Calculation by patterns
         for(int j=0;j<total_patterns;j++)
           {
            //--- Calculation by a date range         
            for(int k=0;k<copied;k++)
              {
               //--- Get the current candlestick type
               GetCandleType(symbol,prev_cand2,m_cur_timeframes2[i],k+2);                                  // previous candlestick
               GetCandleType(symbol,prev_cand,m_cur_timeframes2[i],k+1);                                   // previous candlestick
               GetCandleType(symbol,cur_cand,m_cur_timeframes2[i],k);                                      // current candlestick
               //---
               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);
//--- Update the table
   m_table2.Update(true);
   m_table2.GetScrollVPointer().Update(true);
   return(true);
  }

It is important to understand the sequence of calculations within this version algorithm, based on input data. We will not consider receiving of timeframes and date ranges here, as this was discussed earlier. Then the algorithm checks the size of patterns currently being tested. Consider a three-candlestick pattern. After declaring a structure for storing price data and after nulling the used 'ratings' structure, the algorithm loops for the first time through timeframes and gets the amount of copied data for each of them. This enables the determining of the range, in which the specified patterns will be further searched. After the timeframe cycle, enter into the calculation cycle for each pattern on the specified timeframe. Then, for each of the patterns, loop through candlesticks defined in the specified date range.

For a better understanding, view the calculation example and the order of information display in the results table.

Fig.9. Calculation example and order of result output in the table

As can be seen in Fig.9, the test was performed using the EURUSD currency pair, with the 1-candlestick pattern, on the M15, M30, H1 and H2 timeframes. Two simple candlesticks were selected for testing: with indexes 1 and 2. The implementation of the above described algorithm can be observed in the results table. It is performed as follows: first all generated patterns are analyzed one by one on the 15-minute timeframe, then on the 30-minute timeframe, and so on.

Conclusion

The archive attached below contains all described files arranged into appropriate folders. For their proper operation, you only need to save the MQL5 folder into the terminal folder. To open the terminal root directory, in which the MQL5 folder is located, press the Ctrl+Shift+D key combination in the MetaTrader 5 terminal or use the context menu as shown in Fig 10 below.

Fig.10 Opening the MQL5 folder in the MetaTrader 5 terminal root.

Programs used in the article

#
 Name
Type
Description
1
PatternAnalyzer.mq5 Graphical interface
 Toolbar for analyzing candlestick patterns
2 MainWindow.mqh Code Base  GUI library
3 Program.mqh Code Base  Library of methods for creating UI and calculation elements

Previous articles in this series:

Studying candlestick analysis techniques (Part I): Checking existing patterns
Studying candlestick analysis techniques (Part II): Auto search for new patterns
Studying candlestick analysis techniques (Part III): Library for pattern operations