Estudio de técnicas de análisis de velas (Parte IV): Actualizaciones y adiciones a la aplicación

Alexander Fedosov | 6 agosto, 2019

Contenido

Introducción

En el presente ciclo de artículos, se ha desarrollado una aplicación para MetaTrader 5 capaz de poner a prueba la actualidad de los modelos de velas existentes. Posteriormente, se ha añadido la posibilidad de crear patrones propios a partir de tipos de vela simples, tales como velas cortas, largas, Doji, spining top, etc. En la última parte, se ha desarrollado una biblioteca que permite crear tanto indicadores basados en modelos de vela, como expertos comerciales.

En el presente artículo, vamos a presentar la siguiente versión de la aplicación Pattern Analyzer. En esta versión se han corregido algunas imperfecciones y se han añadido nuevas capacidades; además, se ha dado un nuevo enfoque a la comodidad y la actualidad de la interfaz actual. En este caso, además, se han tenido en cuenta las sugerencias reflejadas en los comentarios de los artículos anteriores. Podrá familiarizarse con el resultado leyendo el presente artículo.

Un vistazo general a las actualizaciones

La interfaz de usuario es una parte importante de cualquier aplicación, dado que una estructura adecuada permite trabajar con ella de forma efectiva, así como obtener cómodamente los datos analizados. Por eso, vamos a analizar la implementación del nuevo aspecto exterior en comparación con el antiguo. Vamos a echar un vistazo a la antigua versión de la pestaña Análisis, fijándonos en los puntos susceptibles de mejora.

Fig.1 Interfaz anterior de la pestaña Análisis

Punto 1. Ubicación de las pestañas y dimensiones.

En la fig.1, las pestañas marcadas con la posición 1 se encuentran en la parte superior de la aplicación; en este caso, además, la parte superior derecha se encuentra vacía y no se usa, y no dispone de espacio posible para añadir varias pestañas más. La fuente de texto es demasiado pequeña para ser leída con comodidad. Por eso, las tres pestañas han pasado a la parte superior izquierda de la aplicación, adquiriendo una orientación vertical para resultar más notorias. Asimismo, disponen de sitio suficiente para añadir varios apartados adicionales.

Punto 2. Recuadros con los resultados de las simulaciones de los patrones.

La representación visual de la información no resulta cómoda del todo. Por eso, la fuente, la altura de las líneas y el tamaño del recuadro han sido redefinidos con un mayor tamaño, para lograr así que los resultados se lean mejor.

Punto 3. Selección del marco temporal actual.

Como hemos aclarado, la construcción de la estructura de selección Marco temporal —> Resultados para todos los patrones limita la visibilidad de los resultados de las pruebas, por eso, hemos desarrollado una variante de selección de marco temporal múltiple, así como la selección individual de los patrones analizados. Esto permitirá configurar de manera más flexible el trabajo con los patrones. 

Punto 4. Intervalo de la muestra.

La idea implementada en la anterior versión consistía en simular los datos actuales hasta una cantidad determinada de velas dentro de la historia. Esto no permitía hacer la muestra más específica, concretamente, abarcando desde una fecha inicial hasta una final. Por eso, este método de muestra será sustituido por uno más avanzado. En la fig.2, más abajo, se muestra la solución de todos los puntos anteriormente descritos y las posibles mejoras.

Fig. 2 Interfaz mejorada de la pestaña Análisis.

Bien, vamos a enumerar las soluciones para corregir las variantes anteriores.

Asimismo, debemos aumentar el tamaño de la ventana de la aplicación, para representar multitud de elementos nuevos. También resulta cómodo y novedoso el traslado del ajuste del Valor umbral de la tendencia (fig.3) en puntos desde la pestaña de Ajustes a las pestañas de Análisis y Búsqueda automática. En este caso, además, el ajuste es individual para cada una de las pestañas. 

Fig.3 Traslado del ajuste del Valor umbral de la tendencia

El último elemento modificado ha sido la estructura de los recuadros de resultados. Se ha quitado la columna de Frecuencia, incluyendo en su lugar el parámetro Marco temporal, más actual y necesario. 

Fig. 4 Nueva estructura del recuadro de resultados

Ahora vamos a analizar qué hemos necesitado mejorar en la segunda pestaña Búsqueda automática, que trabaja con los patrones generados.

Punto 1. Configuración en pestañas distintas.

Los ajustes relacionados directamente con el apartado de Búsqueda automática se encontraban en la pestaña Ajustes, y para modificarlos era necesario alternar constantemente entre Búsqueda automática y Ajustes. Por eso, casi todos los ajustes han sido trasladados a la pestaña Búsqueda automática. Asimismo, hemos añadido mejoras relacionadas con el Valor umbral de la tendencia, la selección de los marcos temporales actuales y el intervalo de fechas. El resultado de las actualizaciones de la Búsqueda automática se muestran en la fig.5.

Fig.5 Actualización de la funcionalidad en la pestaña Búsqueda automática 

 Ahora, el trabajo con los patrones generados es aún más cómodo. Además, merece la pena destacar que el Intervalo de fechas aquí es igualmente individual.


Implementando las actualizaciones

Vamos a ver con mayor detalle cómo han sido implementadas las actualizaciones mostradas más arriba, así como los cambios realizados en los cálculos.

Estructura de las ventanas. Método de creación de la ventana principal de la aplicación.

El método CProgram::CreateGUI(), encargado de crear la interfaz gráfica, ha sido complementado:

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

Estamos hablando de CreateWindowSetting2()CreateWindowSetting3(), encargados de representar la nueva herramienta de ajuste del intervalo temporal de la muestra, mostrada en la fig.1. Asimismo, se ha desarrollado significativamente el método de creación de la ventana principal de la aplicación CreateWindow(). Se ha dividido en tres bloques que se corresponden con los elementos de la interfaz de cada una de las pestañas: Análisis, Búsqueda automática, Ajustes.

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

En la primera pestaña, se han añadido los métodos que representan los nuevos elementos de la interfaz. 

A la segunda pestaña de Búsqueda automática de la pestaña Ajustes (fig.5) se han trasladado los métodos responsables de la representación de los elementos de selección de las dimensiones del patrón generado CreateTripleButton() y la opción conmutable Repetir/No repetir, con el método CreateDualButton(). Asimismo, se han añadido los métodos encargados del encabezado del marco temporal y sus conjuntos.

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

El apartado Ajustes ahora tiene menos elementos. En él han permanecido los ajustes individuales de las velas, los ajustes de los coeficientes para calcular la probabilidad y la efectividad, la selección del idioma de la interfaz y la selección de las velas para generar los patrones en la pestaña de Búsqueda automática.A continuación, vamos a ver los métodos con mayor detalle. 

El método de creación de la selección de patrones CreatePatternSet(). supone un conjunto de botones conmutables para la selección de los patrones existentes para el análisis.

Fig.6 Principio de funcionamiento de la selección de patrones para el análisis

La implementación se muestra más abajo:

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

Debemos prestar atención a que el botón que selecciona/desmarca todos los patrones "All Patterns" es el último botón. Para procesar su pulsación, se usa un código adicional en la sección de procesamiento de los eventos de pulsación de botones:

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

El método de selección de marcos temporales CreateTimeframeSet() es muy semejante al anterior. También dispone de un conjunto de botones conmutables que sirven para seleccionar los marcos temporales analizados.

Fig.7 Principio de funcionamiento de la selección de marcos temporales para el análisis

La implementación se muestra en el siguiente listado:

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

También dispone de un botón para seleccionar/desmarcar todos los marcos temporales, y se procesa en la sección de pulsación de botones:

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

El próximo elemento nuevo es el botón de Intervalo de fechas. Forma parte de una nueva herramienta compleja para el ajuste del intervalo de la muestra. Se implementa con el método CreateDateRange().

Fig.8 Principio de funcionamiento de la selección del intervalo de fechas para el análisis

Su implementación se muestra más abajo:

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

En el procesador del evento de pulsación del botón también existe un código encargado de representar la ventana diálogo con el intervalo de fechas:

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

No hay necesidad de describir los nuevos elementos añadidos en la pestaña, puesto que son idénticos a la implementación de la pestaña Análisis, con la excepción de parámetros tales como las coordenadas. Por eso, vamos a pasar al análisis de los métodos encargados de la representación de otras nuevas ventanas, aparte de la principal. 

Estructura de las ventanas. Método de creación de la ventana de diálogo de la aplicación.

Los métodos de representación de las ventanas de diálogo para las pestañas Análisis y Búsqueda automática son semejantes entre sí, por eso, vamos a analizar uno de ellos, y el segundo será similar.

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


Sección de cálculos. Métodos redefinidos de búsqueda de velas y patrones.

Debido a los cambios considerables sucedidos en la estructura de la interfaz de usuario, así como a la adición de nuevos elementos y a la exclusión de otros antiguos, hemos tenido que cambiar el método de cálculo. En la aplicación actual existen dos métodos de cálculo: el primero se encarga de los patrones existentes, y el segundo, de los generados. 

Los cálculos se inician al pulsar sobre uno de los instrumentos comerciales disponibles en el recuadro de símbolos: esta regla se aplica de la misma forma para ambas pestañas, tanto para Análisis, como para Búsqueda automática. En este caso, además, tiene lugar la llamada de uno de los métodos, dependiendo de la pestaña.

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

Podemos ver por el listado, que al final de cada uno de los métodos se llama el método GetPattertType(), que tiene dos tipos distintos de argumentos. Este método es clave a la hora de buscar patrones y procesar los resultados obtenidos. Por eso, vamos a analizar cada uno de los tipos con más detalle.

El primer tipo del método se usa al buscar los patrones existentes.

   bool              GetPatternType(const string symbol);

La implementación de este método es bastante larga, por eso, vamos mostrar un ejemplo para un solo patrón, el lector podrá estudiar los demás por su propia cuenta.

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

El algoritmo no es complicado, y funciona de la forma siguiente:

A continuación, analizamos los métodos que han existido en anteriores versiones y que han sido modificados en la versión actualizada, además de un nuevo método para calcular los datos obtenidos y mostrar estos en el recuadro de resultados:

El método de búsqueda de patrones GetPatternType() tiene dos implementaciones distintas, mientras que esos tres métodos son universales. Vamos a analizar su implementación con mayor detalle:

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

El algoritmo de este método es el siguiente: obtenemos la fecha inicial del intervalo de la muestra, dependiendo de los patrones que investigamos, existentes o generados. Dado que este método se usa en el ciclo de cálculo en el intervalo de fechas, modificamos la fecha inicial, desplazándola del pasado hacia el futuro en una vela en el marco temporal establecido. A continuación, copiamos los datos necesarios para calcular los tipos simples de vela. Si los datos son insuficientes, se lo comunicamos al usuario. 

¡Nota importante! Debemos monitorear la disponibilidad de datos históricos en el terminal MetaTrader 5, de lo contrario, la aplicación podría funcionar de forma incorrecta. 

Si no hay datos suficientes, se comprueba si la vela actual pertenece a un cierto tipo de vela simple.

El método GetCategory(), como ya sabemos por los artículos anteriores, comprueba en la historia el comportamiento del precio después de la aparición del patrón.

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

En el algoritmo de este método, al igual que en el anterior, solo ha cambiado el método de obtención de la información sobre las velas analizadas. Esto se relaciona directamente con el cambio de la herramienta de selección del intervalo temporal.

El último método común para ambos GetPatternType() comprende la obtención, el cálculo y la muestra de los datos en el recuadro de resultados. 

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

El método recibe en sus argumentos toda la información necesaria para el cálculo. Su algoritmo es muy sencillo Solo hay dos aspectos que debemos mencionar aquí. En las anteriores versiones de la aplicación, el número exacto de líneas en el recuadro se conoce inicialmente. Por ejemplo, al procesar los datos de los patrones existentes, el recuadro de resultados siempre ha tenido el mismo número de líneas, que es igual al número de patrones determinados, es decir, 14. Ahora, resulta imposible saber qué número de patrones o marcos temporales de trabajo va a seleccionar el usuario. Por eso mismo, hemos introducido un sencillo contador de líneas, m_total_row. Así, la llamada del método AddRow() añade una línea al recuadro de resultados según dos rasgos: el patrón y el marco temporal.

El segundo aspecto tiene que ver con la pestaña Búsqueda automática. Antes, el número final de líneas era igual al número de combinaciones de los patrones generados. Ahora, el algoritno anterior ya no es adecuado, precisamente por el mismo motivo: no sabemos cuántos marcos temporales vamos a tener. Por eso, deberemos registrar de nuevo la matriz de combinaciones generadas para cada uno de los marcos temporales seleccionados.

Vamos a echar un vistazo ahora a la segunda variante del método GetPatternType().

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

Aquí, aparte del símbolo actual, el segundo es el enlace a la matriz de línea con las combinaciones de los patrones generados. 

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

El algoritmo de esta versión del método se basa en la necesidad de comprender en qué secuencia se da el cálculo a partir de los datos introducidos. Vamnos a omitir la obtención de los marcos temporales y el intervalo de fechas por parte del usuario, ya que hemos visto este proceso un poco más arriba. A continuación, el algoritmo verifica el tamaño de los patrones que están siendo puestos a prueba. Vamos a analizar un patrón de tres velas. Después de declarar las estructuras para guardar los datos de precio y de resetear la estructura ratings utilizada, entramos en el primer ciclo de la iteración de los marcos temporales, y obtenemos el número de datos copiados para cada uno de ellos. Esto permite determinar posteriormente en qué intervalo tendrá lugar la búsqueda de los patrones establecidos. Después del ciclo del marco temporal, entramos en el ciclo de cálculo para cada patrón en el marco temporal establecido. Y a continuación, para cada uno de los patrones, iteramos por las velas definidas en el intervalo de fechas especificado.

Para que resulte más comprensible, vamos a mostrar un ejemplo de cálculo y el orden de muestra de la información en el recuadro de resultados.

Fig.9  Ejemplo de cálculo y orden de muestra de los resultados en el recuadro

Como podemos ver por la fig.9, la simulación se ha realizado con la pareja de divisas EURUSD, con el patrón de 1 vela en los marcos temporales М15, М30, Н1, Н2. Se han seleccionado dos velas simples para la simulación, con los índices 1 y 2. En el recuadro de resultados se puede observar la implementación del algoritmo descrito más arriba. En concreto: primero se toma el marco temporal de 15 minutos, y luego se investigan en el mismo y por orden todos los patrones generados; acto seguido, se hace lo propio con el marco temporal de 30 minutos, y así sucesivamente.

Conclusión

Al final del artículo se adjunta un fichero con todos los archivos enumerados, clasificados por carpetas. Por eso, para que funcione correctamente, basta con colocar la carpeta  MQL5 en la carpeta raíz del terminal. Para encontrar la carpeta raíz del terminal en la que se encuentra la carpeta MQL5 , debemos pulsar en MetaTarder 5 la combinación de teclas  Ctrl+Shift+D o utilizar el menú contextual como se muestra en la fig.10, más abajo.

Fig.10 Abriendo la carpeta MQL5 en la carpeta raíz del terminal MetaTrader 5.

Programas usados en el artículo:

#
 Nombre
Tipo
Descripción
1
PatternAnalyzer.mq5 Interfaz gráfica
 Panel de instrumentos para analizar el modelo de velas.
2 MainWindow.mqh Biblioteca  Biblioteca para la construcción de la interfaz gráfica
3 Program.mqh Biblioteca  Biblioteca de métodos para la creación de los elementos de la interfaz y los cálculos

Artículos anteriores de esta serie:

Estudio de técnicas de análisis de velas (Parte I): Comprobando los patrones existentes.
Estudio de técnicas de análisis de velas (Parte II): Búsqueda automática de patrones nuevos.
Estudio de técnicas de análisis de velas (Parte III): Biblioteca para el trabajo con patrones.