English Русский 中文 Deutsch 日本語 Português
Patrones con ejemplos (Parte I): Pico múltiple

Patrones con ejemplos (Parte I): Pico múltiple

MetaTrader 5Sistemas comerciales | 16 agosto 2021, 15:59
1 738 0
Evgeniy Ilin
Evgeniy Ilin

Contenido


Introducción

Los patrones son un tema bastante común en Internet, porque muchos tráders los usan y pueden denominarse criterios visuales para analizar la dirección de los precios posteriores. El trading de algorítmico es un tema completamente diferente. Estos criterios visuales no pueden existir para el trading algorítmico. Los asesores expertos y los indicadores tienen sus propios métodos para trabajar con una serie de precios. En ambos extremos hay tanto ventajas como desventajas. El código no puede mostrar la amplitud de pensamiento y la calidad de análisis de un humano, pero sí que tiene cualidades no menos valiosas: una velocidad incomparable y un volumen incomparable de procesamiento de datos numéricos o lógicos por unidad de tiempo. No resulta fácil decirle a la máquina qué hacer, para ello se necesita práctica. Con el tiempo, el programador comienza a comprender la máquina y la máquina comienza a entender a este. Este ciclo resultará útil para que los principiantes en programación aprendan a estructurar sus pensamientos y a dividir tareas complejas en otras más simples.


Sobre los patrones de reversión

En nuestra opinión, los patrones de reversión tienen una definición demasiado vaga y, lo que es más importante, no contienen matemáticas en su forma absoluta. En general, para ser honestos, ningún patrón tiene matemáticas: las únicas matemáticas que podemos ofrecer en este sentido son las estadísticas. Las estadísticas son el único criterio de la verdad, pero estas se basan en el comercio real. Está claro que no existen fuentes que puedan ofrecer estas estadísticas con la máxima precisión y, si pudieran, lo harían por el bien de algún tipo de investigación dentro de una plataforma comercial, entendiendo que no sacarían provecho de ello. Creo que la respuesta es obvia para todos. La única salida de esta situación es el backtesting y la visualización en el simulador de estrategias. Obviamente, este enfoque es de calidad inferior, pero tiene una ventajas innegables: la velocidad y la cantidad de los datos. 

Obviamente, los patrones de reversión por sí mismos no suponen una herramienta suficiente para determinar el viraje de la tendencia, pero, en combinación con otros métodos de análisis, como, por ejemplo, los niveles o el análisis de velas, pueden dar el resultado deseado. En el marco del presente ciclo, resultan interesantes no tanto como método de análisis extremadamente sustancial, sino como formaciones donde practicar de manera adecuada nuestras habilidades de trading algorítmica. Además del entrenamiento, en función de los resultados, podemos conseguir alguna herramienta auxiliar interesante y útil, si no para el comercio algorítmico, al menos para ahorrar esfuerzo a los ojos del tráder. Se valoran enormemente los indicadores útiles.


¿Por qué el pico múltiple y qué tiene de interesante?

Este patrón se ha vuelto bastante popular en Internet por su simplicidad. Este patrón es bastante común tanto en cualquier instrumento comercial como en cualquier periodo de gráfico, simplemente porque no tiene nada de complicado. Además, si observamos de cerca el patrón, entenderemos que utilizando el trading algorítmico y las capacidades del lenguaje MQL5, podemos incluso expandir la idea de este método e intentar crear un código general que no se limite solo a un pico doble. Si creamos correctamente este prototipo, podremos explorar no solo este patrón, sino también todos sus híbridos y herederos.

El sucesor clásico del pico múltiple es el patrón "Head and Shoulders", querido y conocido por todos. Pero, por desgracia, no existe información estructurada sobre el comercio con este patrón. En general, este es el problema de muchas estrategias que se escuchan ahora: se escriben muchas palabras bonitas, pero sin aportar estadísticas. En este artículo, vamos a intentar entender si es posible usar dichos patrones en el marco del comercio algorítmico. La única forma de recopilar estadísticas sin comerciar en una cuenta demo o real es usando las capacidades del simulador de estrategias. No debemos subestimar esta herramienta, porque sin ella no podríamos sacar conclusiones complejas respecto a una estrategia en particular.


¿Es posible ampliar el concepto de pico múltiple?

Volviendo al tema del artículo, intentaremos representar algún tipo de diagrama en el que se muestre un árbol de patrones que parta desde un pico doble. Esto es necesario para comprender cuán amplias son las posibilidades de este concepto:

Tree

Hemos decido combinar el concepto de varios patrones con el supuesto de que estos se basan aproximadamente en la misma idea. Esta idea tiene un inicio simple, consistente en encontrar un buen movimiento en cualquier dirección y determinar correctamente el supuesto lugar donde virará. Después de producirse el contacto visual con el patrón propuesto, el tráder deberá trazar correctamente algunas líneas auxiliares que lo ayuden tanto a valorar si el patrón en sí cumple con ciertos criterios, como a determinar el punto de entrada al mercado, así como a determinar correctamente el objetivo y establecer un stop loss. En este caso, el take profit se puede usar en lugar del objetivo.

La unificación del concepto de estos patrones, como podemos ver en la figura, puede basarse en que estos tengan unas reglas generales de construcción que resulten inquebrantables. Precisamente esta firmeza en la definición influye muy bien en el resultado final, y, a nuestro juicio, supone la diferencia fundamental entre un tráder algorítmico y muchos tráders manuales. La incertidumbre y la interpretación múltiple de los mismos principios no llevan a nada bueno.

Los patrones básicos aquí son:

  1. Pico doble
  2. Pico triple
  3. Cabeza y hombros

Estos patrones son muy similares en estructura y uso. Los tres patrones están diseñados para ayudar a identificar una reversión. Los tres patrones poseen una lógica similar para dibujar las líneas auxiliares. Lo ilustraremos con un ejemplo de pico doble:

Double extremum

En la figura, todas las líneas que necesitamos están numeradas e indican lo siguiente:

  1. Resistencia a la tendencia
  2. Línea auxiliar para definir un pico pesimista (algunos piensan que es un cuello, a nuestro juicio eso es incorrecto, pero podríamos equivocarnos)
  3. Línea de cuello
  4. Objetivo optimista (también es un take profit para el comercio)
  5. Nivel de stop loss máximo permitido (se establece en función del pico más lejano)
  6. Línea de pronóstico optimista (igual al movimiento de la tendencia anterior)

Se considera un objetivo pesimista relativo al punto de intersección del cuello desde el borde cercano al mercado, para ello, se toma la distancia entre "1" y "2", designada como "t", y se pospone nuevamente en la dirección de la reversión esperada. El mínimo del objetivo optimista se calcula igual, solo que la distancia entre "5" y "3" ya está pospuesta, y se designa como "s".


Escribiendo el código para visualizar el pico múltiple

Vamos a empezar determinando la lógica de razonamiento para definir estos patrones. Para encontrar el siguiente patrón, deberemos respetar la lógica barra por barra, es decir, trabajaremos no por ticks, sino por barras. En este caso, ahorraremos mucho trabajo al terminal descartando cálculos innecesarios. Comenzaremos definiendo una clase que simbolice a algún observador independiente que buscará una formación. Todas las operaciones necesarias para la correcta detección de la formación serán parte de la instancia, y toda la búsqueda se realizará de forma interna. Lo hemos decidido así para poder modificar el código en el futuro, concretamente, para ampliar la funcionalidad y modificar la existente.

Mapa de la clase:

Vamos a comenzar viendo qué habrá en la clase:

class ExtremumsPatternFamilySearcher// class simulating an independent pattern search
   {
   private:
   int BarsM;// how many bars on chart to use
   int MinimumSeriesBarsM;// the minimum number of bars in a row to detect a top
   int TopsM;// number of tops in the pattern
   int PointsPessimistM;// minimum distance in points to the nearest target
   double RelativeUnstabilityM;// maximum excess of the head size relative to the minimum shoulder
   double RelativeUnstabilityMinM;// minimum excess of the head size relative to the minimum shoulder
   double RelativeUnstabilityTimeM;// maximum excess of head and shoulders sizes
   bool bAbsolutelyHeadM;// whether a pronounced head is required
   bool bRandomExtremumsM;// random selection of extrema
     


   struct Top// top data
      {
      datetime Datetime0;// time of the candlestick closest to the market
      datetime Datetime1;// time of the next candlestick
      int Index0;// index of the candlestick closest to the market
      int Index1;// index of the next candlestick
      datetime DatetimeExtremum;// time of the top
      int IndexExtremum;// index of the top
      double Price;// price of the top
      bool bActive;// if the top is active (if not, then it does not exist)
      };
   
   struct Line// line
      {
      double Price0;// price of the candlestick closest to the market, to which the line is bound
      datetime Time0;// time of the candlestick closest to the market, to which the line is bound
      double Price1;// price of the farthest candlestick to which the line is bound
      datetime Time1;// time of the farthest candlestick to which the line is bound
      datetime TimeX;// time of the X point
      int Index1;// index of the left edge
      bool DirectionOfFormation;// direction
      double C;// free coefficient in the equation
      double K;// aspect ratio
   
      void CalculateKC()// find unknowns in the equation
         {
         if ( Time0 != Time1 ) K=double(Price0-Price1)/double(Time0-Time1);
         else K=0.0;
         C=double(Price1)-K*double(Time1);
         }
      
      double Price(datetime T)// function of line depending on time
         {
         return K*T+C;
         }
      };
   
   public:   
   
   ExtremumsPatternFamilySearcher(int BarsI,int MinimumSeriesBarsI,int TopsI,int PointsPessimistI, double RelativeUnstabilityI,
   double RelativeUnstabilityMinI,double RelativeUnstabilityTimeI,bool bAbsolutelyHeadI,bool bRandomExtremumsI)// parametric constructor
      {
      BarsM=BarsI;
      MinimumSeriesBarsM=MinimumSeriesBarsI;
      TopsM=TopsI;
      PointsPessimistM=PointsPessimistI;
      RelativeUnstabilityM=RelativeUnstabilityI;
      RelativeUnstabilityMinM=RelativeUnstabilityMinI;
      RelativeUnstabilityTimeM=RelativeUnstabilityTimeI;
      bAbsolutelyHeadM=bAbsolutelyHeadI;
      bRandomExtremumsM=bRandomExtremumsI;
      bPatternFinded=bFindPattern();
      }
      
   int FormationDirection;// direction of the formation (multiple top or bottom, or non at all) ( -1,1,0 )      
   bool bPatternFinded;// if the pattern was found during formation
   Top TopsUp[];// required upper extrema
   Top TopsDown[];// required lower extrema
   Top TopsUpAll[];// all upper extrema
   Top TopsDownAll[];// all lower extrema
   int RandomIndexUp[];// array for the random selection of the tops index
   int RandomIndexDown[];// array for the random selection of the bottoms index
   Top StartTop;// where the formation starts (top farthest from the market)
   Top EndTop;// where the formation ends (top closest to the market)
   Line Neck;// neck
   Top FarestTop;// top farthest from the neck (will be used to determine the head or the formation size) or the same as the head
   Line OptimistLine;// line of optimistic forecast
   Line PessimistLine;// line of pessimistic forecast
   Line BorderLine;// line at the edge of the pattern
   Line ParallelLine;// line parallel to the trend resistance
   
      
   private:
   void SetTopsSize();// setting sizes for arrays with tops
   bool SearchFirstUps();// search for tops
   bool SearchFirstDowns();// search for bottoms
   void CalculateMaximum(Top &T,int Index0,int Index1);// calculate the maximum price between two bars
   void CalculateMinimum(Top &T,int Index0,int Index1);// calculate the minimum price between two bars
   bool PrepareExtremums();// prepare extrema
   bool IsExtremumsAbsolutely();// control the priority of tops
   void DirectionOfFormation();// determine the direction of the formation
   void FindNeckUp(Top &TStart,Top &TEnd);// find neck for the bullish pattern
   void FindNeckDown(Top &TStart,Top &TEnd);// find neck for the bearish pattern
   void SearchFarestTop();// find top farthest from the neck
   bool bBalancedExtremums();// initial balancing of extrema (so that they do not differ much)
   bool bBalancedExtremumsHead();// if a pattern has more than 2 tops, we can check for a pronounced head
   bool bBalancedExtremumsTime();// require that the extrema be not very far in time relative to the minimum distance
   bool bBalancedHead();// balance the head (in other words, require that it be neither the first nor the last one on the list of tops, if there are more than three of them)
   bool CorrectNeckUpLeft();// adjust the neck so as to find the intersection of price and neck (this creates prerequisites for the previous trend) 
   bool CorrectNeckDownLeft();// similarly for the bottom
   int CorrectNeckUpRight();// adjust the neck so as to find the intersection of price and neck on the right or at the current price position, which is the same (to determine the entry point)
   int CorrectNeckDownRight();// similarly for the bottom
   void SearchLineOptimist();// calculate the optimistic forecast line
   bool bWasTrend();// determine whether a trend preceded the pattern definition (in this case the optimistic target line is considered as the trend beginning)
   void SearchLineBorder();// determine trend resistance or support (usually a sloping line)
   void CalculateParallel();// determine a line parallel to support or resistance (crosses the neck at the pattern low or high)
   bool bCalculatePessimistic();// calculate the line of the pessimistic target
   bool bFindPattern();// perform all the above actions
   int iFindEnter();// find intersection with the neck
   public:
   void CleanAll();// clean up objects
   void DrawPoints();// draw points
   void DrawNeck();// draw the neck
   void DrawLineBorder();// line at the border
   void DrawParallel();// line parallel to the border
   void DrawOptimist();// line of optimistic forecast
   void DrawPessimist();// line of pessimistic forecast
   };

La clase representa las transacciones secuenciales que realizaría una persona si estuviera en el lugar de la máquina. De una forma u otra, la detección de cualquier formación se puede descomponer en un conjunto de operaciones simples y sucesivas. Hay una regla en matemáticas: si no sabes cómo resolver una ecuación, simplifícala. Esta regla se aplica no solo a las matemáticas, sino también a cualquier otro algoritmo. De inicio, la lógica de detección resulta incomprensible, pero si sabemos dónde comenzar la detección, toda la tarea se simplificará de inmediato. En este caso, para encontrar el patrón completo, deberemos encontrar o picos o valles, aunque, en realidad, deberíamos encontrar ambos.

Definición de picos y valles:

Sin picos y valles, se pierde todo el significado del patrón, ya que la presencia de picos y valles es una condición necesaria para la presencia de un patrón, aunque no suficiente. Los picos se pueden definir de diferentes formas. Lo principal es la presencia de una media onda pronunciada, y la media onda está determinada por dos movimientos opuestos pronunciados, en este caso, varias barras seguidas en una dirección. Para hacer esto, deberemos determinar cuál es el número mínimo de barras en una dirección que indique la presencia de movimiento. Para ello, tendremos que establecer una variable de entrada. 

bool ExtremumsPatternFamilySearcher::SearchFirstUps()// find tops
   {
   int NumUp=0;// the number of found tops
   int NumDown=0;// the number of found bottoms
   bool bDown=false;// an auxiliary boolean which shows if a segment of bearish candlesticks has been found
   bool bUp=false;// an auxiliary boolean which shows if a segment of bullish candlesticks has been found
   bool bNextUp=true;// can we move on to searching for the next top
   bool bNextDown=true;// can we move on to searching for the next bottom
   
   for(int i=0;i<ArraySize(TopsUp);i++)// before search, set all necessary tops to an inactive state
      {
      TopsUp[i].bActive=false;
      }
   for(int i=0;i<ArraySize(TopsUpAll);i++)// before search, set all tops to an inactive state
      {
      if (!TopsUpAll[i].bActive) break;
      TopsUpAll[i].bActive=false;
      }
               
   
   for(int i=0;i<BarsM;i++)
      {
      if ( i+MinimumSeriesBarsM-1 < BarsM )// if remaining bars are enough to determine the extremum and we can start searching for the next top
         {
         if ( bNextUp )// if it is allowed to search for the next top
            {
            bDown=true;
            for(int j=i;j<i+MinimumSeriesBarsM;j++)// determine the first extrema for upper tops
               {
               if ( Open[j]-Close[j] < 0 )// if at least one of the selected candlesticks was upward
                  {
                  bDown=false;
                  break;
                  }
               }
            if ( bDown )
               {
               TopsUpAll[NumUp].Datetime0=Time[i+MinimumSeriesBarsM-1];
               TopsUpAll[NumUp].Index0=i+MinimumSeriesBarsM-1;
               bNextUp=false;
               }
            }        
         }

      if ( MinimumSeriesBarsM+i < BarsM && bDown )// if the remaining bars are enough to determine the second half of the extremum and the previous half has been found
         {
         bUp=true;                  
         for(int j=i;j<MinimumSeriesBarsM+i;j++)//determine further candlesticks in the opposite direction
            {
            if ( Open[j]-Close[j] > 0 )//if at least one of the selected candlesticks was downward
               {
               bUp=false;
               break;
               }
            }
         if ( bUp )
            {
            TopsUpAll[NumUp].Datetime1=Time[i];
            TopsUpAll[NumUp].Index1=i;
            TopsUpAll[NumUp].bActive=true;
            bNextUp=false;
            }   
         } 
      // after that, register the found formation as a top, if it is a top
      if ( bDown && bUp )
         {
         CalculateMaximum(TopsUpAll[NumUp],TopsUpAll[NumUp].Index0,TopsUpAll[NumUp].Index1);// calculate extremum between two bars
         bNextUp=true;
         bDown=false;
         bUp=false;
         NumUp++;
         }
      }
   if ( NumUp >= TopsM ) return true;// if the required number of tops have been found
   else return false;
   }

Los valles se determinan al revés:

bool ExtremumsPatternFamilySearcher::SearchFirstDowns()// find bottoms
   {
   int NumUp=0;
   int NumDown=0;
   bool bDown=false;// an auxiliary boolean which shows if a segment of bearish candlesticks has been found
   bool bUp=false;// an auxiliary boolean which shows if a segment of bullish candlesticks has been found
   bool bNextUp=true;// can we move on to searching for the next top
   bool bNextDown=true;// can we move on to searching for the next bottom

   for(int i=0;i<ArraySize(TopsDown);i++)// before search, set all necessary bottoms to an inactive state
      {
      TopsDown[i].bActive=false;
      }
   for(int i=0;i<ArraySize(TopsDownAll);i++)// before search, set all bottoms to an inactive state
      {
      if (!TopsDownAll[i].bActive) break;
      TopsDownAll[i].bActive=false;
      }

   for(int i=0;i<BarsM;i++)
      {
      if ( i+MinimumSeriesBarsM-1 < BarsM )// if remaining bars are enough to determine the extremum and we can start searching for the next top
         {
         if ( bNextDown )// if it is allowed to search for the next bottom
            {
            bUp=true;               
            for(int j=i;j<i+MinimumSeriesBarsM;j++)// determine the first extrema for upper tops
               {
               if ( Open[j]-Close[j] > 0 )//if at least one of the selected candlesticks was downward
                  {
                  bUp=false;
                  break;
                  }
               }
            if ( bUp )
               {
               TopsDownAll[NumDown].Datetime0=Time[i+MinimumSeriesBarsM-1];
               TopsDownAll[NumDown].Index0=i+MinimumSeriesBarsM-1;
               bNextDown=false;
               }
            }        
         }

      if ( MinimumSeriesBarsM+i < BarsM && bUp )// if the remaining bars are enough to determine the second half of the extremum and the previous half has been found
         {   
         bDown=true;                              
         for(int j=i;j<MinimumSeriesBarsM+i;j++)//determine further candlesticks in the opposite direction
            {
            if ( Open[j]-Close[j] < 0 )// if at least one of the selected candlesticks was upward
               {
               bDown=false;
               break;
               }
            }
         if ( bDown )
            {
            TopsDownAll[NumDown].Datetime1=Time[i];
            TopsDownAll[NumDown].Index1=i;
            TopsDownAll[NumDown].bActive=true;
            bNextDown=false;              
            }
         } 
      // after that, register the found formation as a bottom, if it is a bottom
      if ( bDown && bUp )
         {
         CalculateMinimum(TopsDownAll[NumDown],TopsDownAll[NumDown].Index0,TopsDownAll[NumDown].Index1);// calculate extremum between two bars
         bNextDown=true;
         bDown=false;
         bUp=false;            
         NumDown++;
         }
      }
      
   if ( NumDown == TopsM ) return true;//if the required number of bottoms have been found
   else return false;
   }

En este caso, nos hemos apartado de la lógica de los fractales, creando nuestra propia lógica para determinar picos y valles; no nos parece mejor ni peor que los propios fractales, pero al menos no necesitaremos utilizar ninguna funcionalidad externa, ni arrastrar tras nosotros funciones del lenguaje integradas e innecesarias, de las cuales podemos prescindir en algunos casos. Obviamente, estas funciones son buenas, pero en este caso resultan redundantes. Esta función define todos los picos y valles con los que trabajaremos en el futuro. Si visualizamos lo que está sucediendo en esta función, tendrá el aspecto siguiente:

Searching for tops & bottoms

Primero, buscamos el movimiento 1, y luego el movimiento 2; el número 3 ya supone la definición de la parte superior o inferior. Para "3", hemos sacado la lógica a dos funciones separadas que tienen este aspecto:

void ExtremumsPatternFamilySearcher::CalculateMaximum(Top &T,int Index0,int Index1)// if 2 intermediate points are found, find High between them
   {
   double MaxValue=High[Index0];
   datetime MaxTime=Time[Index0];
   int MaxIndex=Index0;
   for(int i=Index0;i<=Index1;i++)
      {
      if ( High[i] >  MaxValue )
         {
         MaxValue=High[i];
         MaxTime=Time[i];
         MaxIndex=i;
         }
      }
   T.DatetimeExtremum=MaxTime;
   T.IndexExtremum=MaxIndex;
   T.Price=MaxValue;
   }
   
void ExtremumsPatternFamilySearcher::CalculateMinimum(Top &T,int Index0,int Index1)//if 2 intermediate points are found, find Low between them
   {
   double MinValue=Low[Index0];
   datetime MinTime=Time[Index0];
   int MinIndex=Index0;
   for(int i=Index0;i<=Index1;i++)
      {
      if ( Low[i] <  MinValue ) 
         {
         MinValue=Low[i];
         MinTime=Time[i];
         MinIndex=i;
         }
      } 
   T.DatetimeExtremum=MinTime;
   T.IndexExtremum=MinIndex;
   T.Price=MinValue;      
   }

Por supuesto, pondremos todo esto más adelante en un contenedor preparado de antemano. La lógica consiste en que todas las estructuras usadas dentro de la clase ofrezcan una adición gradual de datos. En la salida, tras pasar por todas las etapas de búsqueda y superar las verificaciones, obtendremos todos los datos que necesitamos, que nos servirán para mostrar gráficamente este patrón en el gráfico. Obviamente, la lógica para encontrar picos y valles puede ser completamente diferente, pero nuestra tarea consiste solo en mostrar una lógica simple de detección para cosas complejas.

Seleccionamos los picos con los que vamos a trabajar:

Los picos y valles que encontramos son solo intermedios. Una vez que los hayamos encontrado, deberemos seleccionar aquellos picos que consideremos más adecuados en el rol de hombros. No podemos determinar esto de manera fiable, ya que nuestro código no dispone de visión por computadora y es poco probable que el uso de técnicas tan complejas beneficie el rendimiento. Por ahora, seleccionaremos los picos más cercanos al mercado:

bool ExtremumsPatternFamilySearcher::PrepareExtremums()// assign the tops with which we will work
   {
   int Quantity;// an auxiliary counter for random tops
   int PrevIndex;// an auxiliary index for maintaining the order of indexes (increment only)
   
   for(int i=0;i<TopsM;i++)// simply select the tops that are closest to the market
      {
      TopsUp[i]=TopsUpAll[i];
      TopsDown[i]=TopsDownAll[i];
      }
   return true;   
   }

Visualmente, esta lógica en el gráfico de nuestro instrumento será equivalente a la opción en el marco púrpura, pero dibujaremos otras opciones posibles:

Choose tops & bottoms

En este caso, tenemos la lógica de elección más sencilla. Nuestras opciones están numeradas con las cifras "0" y "1" porque son las más cercanas al mercado. Aquí, por supuesto, todo se muestra para un pico doble, pero no resulta difícil imaginar que tendremos la misma lógica para un pico triple o superior, solo que el número de picos seleccionados será un poco mayor.

Esta función se ampliará en el futuro para poder seleccionar aleatoriamente los picos, como hemos dibujado en azul en la figura, para simular múltiples instancias de los buscadores de formaciones. Gracias a ello, podremos encontrar de forma más eficaz y frecuente todas las formaciones en el modo automático.

Determinando la dirección de la formación:

Tras identificar los picos y los valles, deberemos determinar si una formación puede tener lugar en un punto dado del mercado, ya que entonces deberá tener una dirección. En esta etapa, hemos pensado que debería concederse prioridad a la dirección cuyo tipo de extremo esté más cerca del mercado. Usando como base esta lógica, seleccionaremos la opción con el número "0", porque lo más cercano al mercado es el valle, y no el pico: claro, esto considerando que la situación en el mercado sea exactamente la misma que en nuestra figura. En el código, esto se hace de forma muy simple:

void ExtremumsPatternFamilySearcher::DirectionOfFormation()// determine whether it is a double top (1) or double bottom (-1) (only if all tops and bottoms are found - if not found, then 0)
   {
   if ( TopsDown[0].DatetimeExtremum > TopsUp[0].DatetimeExtremum && TopsDown[ArraySize(TopsDown)-1].bActive )
      {
      StartTop=TopsDown[ArraySize(TopsDown)-1];
      EndTop=TopsDown[0];    
      FormationDirection=-1;
      }
   else if ( TopsDown[0].DatetimeExtremum < TopsUp[0].DatetimeExtremum && TopsUp[ArraySize(TopsUp)-1].bActive )
      {
      StartTop=TopsUp[ArraySize(TopsUp)-1];
      EndTop=TopsUp[0]; 
      FormationDirection=1;  
      }
   else FormationDirection=0;   
   }

Otras acciones requerirán una dirección clara. La dirección es equivalente al tipo del patrón:

  1. Pico múltiple
  2. Valle múltiple

Estas reglas también funcionarán para la formación Head and Shoulders y todos los demás híbridos cuando se trata de ellos. La clase ha sido diseñada como clase común para todos los patrones de esta familia, y en parte esta generalidad ya está funcionando.

Filtros para descartar patrones no válidos:

A continuación, iremos un poco más lejos. Sabiendo que tenemos una dirección y una de las formas de selección de picos o valles, deberemos prever que para un pico múltiple, esos picos que se encuentran entre los seleccionados serán más bajos que el más bajo de los seleccionados. Y para un valle múltiple, deberá ser más alto que el más alto de los elegidos. Esto será necesario para que, en caso de seleccionar los picos aleatoriamente, todos los picos seleccionados se distingan claramente. De lo contrario, esta verificación no será necesaria:

bool ExtremumsPatternFamilySearcher::IsExtremumsAbsolutely()// require the selected extrema to be the most extreme ones
   {
   if ( bRandomExtremumsM )// check only if we have a random selection of tops (in other case the check should be considered completed)
      {
      if ( FormationDirection == 1 )
         {
         int StartIndex=RandomIndexUp[0];
         int EndIndex=RandomIndexUp[ArraySize(RandomIndexUp)-1];
         for(int i=StartIndex+1;i<EndIndex;i++)// check all tops between the selected ones
            {
            for(int j=0;j<ArraySize(TopsUp);j++)
               {
               if ( TopsUpAll[i].Price >= TopsUp[j].Price )
                  {
                  for(int k=0;k<ArraySize(RandomIndexUp);k++)
                     {
                     if ( i != RandomIndexUp[k] ) return false;
                     }
                  }
               }
            }
         return true;
         }
      else if ( FormationDirection == -1 )
         {
         int StartIndex=RandomIndexDown[0];
         int EndIndex=RandomIndexDown[ArraySize(RandomIndexDown)-1];
         for(int i=StartIndex+1;i<EndIndex;i++)// check all tops between the selected ones
            {
            for(int j=0;j<ArraySize(TopsDown);j++)
               {
               if ( TopsDownAll[i].Price <= TopsDown[j].Price )
                  {
                  for(int k=0;k<ArraySize(RandomIndexDown);k++)
                     {
                     if ( i != RandomIndexDown[k] ) return false;
                     }
                  }
               }
            }
         return true;      
         }
      else return false;      
      }
   else
      {
      return true;
      }
   }

Si representamos visualmente la versión correcta e incorrecta de la selección aleatoria de picos, que es lo que hace la última función de predicado, todo tendrá el aspecto que sigue:

Control de picos no considerados


Y, obviamente, todos estos criterios serán totalmente inversos en patrones tanto alcistas como bajistas. En la figura, tomamos como ejemplo un patrón alcista; el segundo caso, a nuestro parecer, cualquiera lo puede imaginar por sí mismo.

Una vez hayamos completado los procedimientos preparatorios, podremos comenzar a buscar el cuello. Diferentes tráders construyen sus cuellos de maneras distintas. Hemos resaltado de forma condicional varios tipos de construcción:

  1. Visualmente, con inclinación (no basado en sombras)
  2. Visualmente, en horizontal (no basado en sombras)
  3. Punto más alto o más bajo, inclinado (basado en las sombras)
  4. Punto más alto o más bajo, en horizontal (basado en las sombras)

Por razones de seguridad, y también para aumentar las posibilidades de obtener beneficios, nos inclinamos por la opción 4. Esta elección se basa en las siguientes consideraciones:

  • Es posible encontrar con mayor claridad el comienzo de un movimiento de inversión
  • Es más fácil de implementar como código
  • Determinación inequívoca del ángulo de inclinación (horizontal)

Quizá esto no resulte del todo correcto desde el punto de vista de la construcción, pero no hemos encontrado reglas claras. Desde el punto de vista del comercio algorítmico, esto no resulta crítico, y si encontramos al menos algo racional en este patrón, entonces el simulador o la visualización definitivamente nos mostrarán algo. Además, deberemos pensar en el refuerzo de los indicadores comerciales, y esta es una historia completamente diferente.

Hemos creado dos funciones mutuamente inversas para los patrones alcista y bajista, que definen todos los parámetros necesarios del cuello:

void ExtremumsPatternFamilySearcher::FindNeckUp(Top &TStart,Top &TEnd)// find the neck line based on the two extreme tops (for the classic multiple top)
   {
   double PriceMin=Low[TStart.IndexExtremum];
   datetime TimeMin=Time[TStart.IndexExtremum];
   for(int i=TStart.IndexExtremum;i>=TEnd.IndexExtremum;i--)// define the lowest point
      {
      if ( Low[i] < PriceMin )
         {
         PriceMin=Low[i];
         TimeMin=Time[i];
         }
      }
   // define the parameters of the anchor point and all parameters of the line equation
   Neck.Price0=PriceMin;
   Neck.TimeX=TimeMin;
   Neck.Time0=Time[0];
   Neck.Price1=PriceMin;
   Neck.Time1=TStart.DatetimeExtremum;
   Neck.DirectionOfFormation=true;
   Neck.CalculateKC();
   }
   
void ExtremumsPatternFamilySearcher::FindNeckDown(Top &TStart,Top &TEnd)// find the neck line based on two extreme bottoms (for the classic multiple bottom)
   {
   double PriceMax=High[TStart.IndexExtremum];
   datetime TimeMax=Time[TStart.IndexExtremum];
   for(int i=TStart.IndexExtremum;i>=TEnd.IndexExtremum;i--)// define the lowest point
      {
      if ( High[i] > PriceMax )
         {
         PriceMax=High[i];
         TimeMax=Time[i];         
         }
      }
   // define the parameters of the anchor point and all parameters of the line equation
   Neck.Price0=PriceMax;
   Neck.TimeX=TimeMax;
   Neck.Time0=Time[0];
   Neck.Price1=PriceMax;
   Neck.Time1=TStart.DatetimeExtremum;
   Neck.DirectionOfFormation=false;
   Neck.CalculateKC();
   }

Para construir el cuello de forma correcta y sencilla, deberemos seguir las reglas de construcción aplicadas al cuello para todos los patrones de la familia seleccionada. Por un lado, esto nos ahorrará detalles innecesarios, que en nuestro caso no darán nada. Para construir el cuello para un pico múltiple de cualquier complejidad, será mejor usar los dos picos extremos del patrón. Los índices de estos picos serán los índices entre los que buscaremos el precio más bajo o más alto en el segmento de mercado seleccionado. El cuello será una línea horizontal normal. Los primeros puntos de anclaje deberán encontrarse exactamente en este nivel, y será mejor tomar un tiempo de anclaje exactamente igual a la hora de los picos o valles extremos (dependiendo del patrón que estemos considerando). Este será el aspecto de la imagen:

Cuello

La ventana para buscar el mínimo o el máximo se encontrará exactamente entre el primer y el último pico. Esta regla funciona para cualquier patrón de esta familia, con cualquier número de picos y valles.

Para definir un objetivo optimista, primero deberemos determinar el tamaño del patrón. El tamaño del patrón supone la dimensión vertical de la cabeza al cuello, en puntos. Para hacer esto, deberemos encontrar el pico más alejado del cuello, que será el borde del patrón:

void ExtremumsPatternFamilySearcher::SearchFarestTop()// define the farthest top
   {
   double MaxTranslation;// temporary variable to determine the highest top
   if ( FormationDirection == 1 )// if we deal with a multiple top
      {
      MaxTranslation=TopsUp[0].Price-Neck.Price0;// temporary variable to determine the highest top
      FarestTop=TopsUp[0];
      for(int i=1;i<ArraySize(TopsUp);i++)
         {
         if ( TopsUp[i].Price-Neck.Price0 > MaxTranslation ) 
            {
            MaxTranslation=TopsUp[i].Price-Neck.Price0;
            FarestTop=TopsUp[i];
            }
         }      
      }
   if ( FormationDirection == -1 )// if we deal with a multiple bottom
      {
      MaxTranslation=Neck.Price0-TopsDown[0].Price;// temporary variable to determine the lowest bottom
      FarestTop=TopsDown[0];      
      for(int i=1;i<ArraySize(TopsDown);i++)
         {
         if ( Neck.Price0-TopsDown[i].Price > MaxTranslation ) 
            {
            MaxTranslation=Neck.Price0-TopsDown[0].Price;
            FarestTop=TopsDown[i];
            }
         }      
      }
   }

Para evitar que los picos resulten demasiado diferentes entre sí, deberemos realizar una comprobación adicional, y solo si se supera esta verificación, podremos continuar con los siguientes pasos. En concreto, deberemos hacer dos comprobaciones, una para el tamaño vertical de los extremos y la otra para el horizontal (tiempo). Si los picos están demasiado dispersos en el tiempo, esta opción tampoco nos convendrá. Este será el aspecto de la comprobación del tamaño vertical:

bool ExtremumsPatternFamilySearcher::bBalancedExtremums()// balance the tops
   {
   double Lowest;// the lowest top for the multiple top
   double Highest;// the highest bottom for the multiple bottom
   double AbsMin;// distance from the neck to the nearest top
   if ( FormationDirection == 1 )// for the multiple top
      {
      Lowest=TopsUp[0].Price;
      for(int i=1;i<ArraySize(TopsUp);i++)// find the lowest top
         {
         if ( TopsUp[i].Price < Lowest ) Lowest=TopsUp[i].Price;
         }
      AbsMin=Lowest-Neck.Price0;// determine distance from the lowest top to the neck
      if ( AbsMin == 0.0 ) return false;
      if ( ((FarestTop.Price - Neck.Price0)-AbsMin)/AbsMin >= RelativeUnstabilityM ) return false;// if the head is too much bigger than the lowest leverage
      }
   else if ( FormationDirection == -1 )// for the multiple bottom
      {
      Highest=TopsDown[0].Price;
      for(int i=1;i<ArraySize(TopsDown);i++)// find the highest top
         {
         if ( TopsDown[i].Price > Highest ) Highest=TopsDown[i].Price;
         }
      AbsMin=Neck.Price0-Highest;// determine distance from the highest top to the neck
      if ( AbsMin == 0.0 ) return false;
      if ( ((Neck.Price0-FarestTop.Price)-AbsMin)/AbsMin >= RelativeUnstabilityM ) return false;// if the head is too much bigger than the lowest leverage
      }
   else return false;
   return true;   
   }

Para determinar el tamaño vertical correcto de los picos, necesitaremos dos picos. El primero será el más alejado del cuello y el segundo será el más cercano, respectivamente. Si estos tamaños difieren mucho, esta formación podría resultar falsa, y será mejor no arriesgarse y marcarla como no válida. Al igual que sucede con el predicado anterior, todo esto puede ir acompañado de una ilustración gráfica adecuada sobre cómo se puede hacer y cómo no:

Control del tamaño vertical

Obviamente, resulta más fácil de definir visualmente, pero el código necesita algún tipo de indicador cuantitativo. Es fácil adivinar que, en este caso, bastará con:

  • K = (Max - Min)/Min
  • K <= RelativeUnstabilityM

Parece que el indicador es lo suficientemente efectivo como para eliminar una ingente cantidad de patrones falsos; al final, ni siquiera el mejor código podrá detectar estas cosas de forma más eficiente que nuestro ojo, pero el comercio algorítmico asume este hecho desde el principio. Lo único que podemos hacer es aproximar todo lo que podamos la lógica a la realidad, pero definitivamente necesitaremos saber cuándo detenernos.

La comprobación horizontal tendrá un aspecto similar, con la única salvedad de que tomaremos como tamaños los índices de barra (podemos usar el tiempo como factor, esto no es fundamental):

bool ExtremumsPatternFamilySearcher::bBalancedExtremumsTime()// balance the sizes of shoulders and head along the horizontal axis
   {
   double Lowest;// minimum distance between the tops
   double Highest;// maximum distance between the tops
   if ( FormationDirection == 1 )// for the multiple top
      {
      Lowest=TopsUp[1].IndexExtremum-TopsUp[0].IndexExtremum;
      Highest=TopsUp[1].IndexExtremum-TopsUp[0].IndexExtremum;
      for(int i=1;i<ArraySize(TopsUp)-1;i++)// find the lowest top
         {
         if ( TopsUp[i+1].IndexExtremum-TopsUp[i].IndexExtremum < Lowest ) Lowest=TopsUp[i+1].IndexExtremum-TopsUp[i].IndexExtremum;
         if ( TopsUp[i+1].IndexExtremum-TopsUp[i].IndexExtremum > Highest ) Highest=TopsUp[i+1].IndexExtremum-TopsUp[i].IndexExtremum;
         }
      if ( double(Highest-Lowest)/double(Lowest) > RelativeUnstabilityTimeM ) return false;// if the width of one of the waves differs much
      }
   else if ( FormationDirection == -1 )// for the multiple bottom
      {   
      Lowest=TopsDown[1].IndexExtremum-TopsDown[0].IndexExtremum;
      Highest=TopsDown[1].IndexExtremum-TopsDown[0].IndexExtremum;
      for(int i=1;i<ArraySize(TopsDown)-1;i++)// find the lowest top
         {
         if ( TopsDown[i+1].IndexExtremum-TopsDown[i].IndexExtremum < Lowest ) Lowest=TopsDown[i+1].IndexExtremum-TopsDown[i].IndexExtremum;
         if ( TopsDown[i+1].IndexExtremum-TopsDown[i].IndexExtremum > Highest ) Highest=TopsDown[i+1].IndexExtremum-TopsDown[i].IndexExtremum;
         }
      if ( double(Highest-Lowest)/double(Lowest) > RelativeUnstabilityTimeM ) return false;// if the width of one of the waves differs much 
      }
   else return false;
   return true;
   }

Para la comprobación, podemos tomar un indicador similar y representar todo esto gráficamente de la misma forma visual:

Control del tamaño horizontal

En este caso, el criterio cuantitativo será el mismo, solo que con la dimensión del índice o el tiempo, y no con puntos; el número con el que estamos comparando probablemente debería mostrarse por separado, lo cual nos dará margen para realizar un ajuste flexible:

  • K = (Max - Min)/Min
  • K <= RelativeUnstabilityTimeM

El cuello necesariamente debe cruzarse con el precio de la izquierda, porque esto significará que una tendencia podría haber precedido a esto:

bool ExtremumsPatternFamilySearcher::CorrectNeckUpLeft()// next the neck line must be corrected so that it finds an intersection with the price on the left
   {
   bool bCrossNeck=false;// indicates if the neck was crossed
   if ( Neck.DirectionOfFormation )// if the neck is found for a double top
      {
      for(int i=StartTop.Index1;i<BarsM;i++)// define the intersection point
         {
         if ( High[i] >= FarestTop.Price )// if the movement goes beyond the formation, then the formation is fake
            {
            return false;
            }         
         if ( Close[i] < Neck.Price0 && Open[i] < Neck.Price0 && High[i] < Neck.Price0 && Low[i] < Neck.Price0   )
            {
            Neck.Time1=Time[i];
            Neck.Index1=i;
            return true;
            }
         }
      }
   return false;
   }
   
bool ExtremumsPatternFamilySearcher::CorrectNeckDownLeft()// next the neck line must be corrected so that it finds an intersection with the price on the left
   {
   bool bCrossNeck=false;// indicates if the neck was crossed
   if ( !Neck.DirectionOfFormation )// if the neck is found for a double bottom
      {
      for(int i=StartTop.Index1;i<BarsM;i++)// define the intersection point
         {
         if ( Low[i] <= FarestTop.Price )//  if the movement goes beyond the formation, then the formation is fake
            {
            return false;
            }         
         if ( Close[i] > Neck.Price0 && Open[i] > Neck.Price0 && High[i] > Neck.Price0 && Low[i] > Neck.Price0 )
            {
            Neck.Time1=Time[i];
            Neck.Index1=i;
            return true;
            }
         }
      }
   return false;
   }

Asimismo, hay dos funciones inversas para los patrones alcista y bajista. A continuación, mostraremos una ilustración gráfica de este predicado y el siguiente:

Control de la intersección a derecha e izquierda

Hemos destacado con marcos azules los segmentos del mercado donde ejercemos este control. Ambos segmentos se encuentran más allá del patrón, a la izquierda y a la derecha de los picos extremos. 

Solo quedan dos comprobaciones:

  1. Necesitamos un patrón tal que cruce la línea del cuello en el momento actual (es decir, en la vela cero)
  2. Es necesario que el patrón esté precedido por un movimiento mayor o igual al tamaño del patrón en sí.

El primer punto es necesario para el trading algorítmico. En nuestra opinión, no merece la pena detectar formaciones para simplemente mirarlas, aunque se haya previsto esta función. Necesitamos tanto la detección como la búsqueda exactamente en el punto desde el que podemos comerciar, para abrir una posición directamente conociendo que ya estamos en el punto de entrada. El segundo punto es una de las condiciones necesarias, porque sin un buen movimiento previo, el patrón en sí resultará inútil.

La intersección con la vela cero (comprobación de la intersección a la derecha) se calcula de la forma que sigue:

int ExtremumsPatternFamilySearcher::CorrectNeckUpRight()// next the neck line must be corrected so that it finds an intersection with the price on the right
   bool bCrossNeck=false;// indicates if the neck was crossed
   if ( Neck.DirectionOfFormation )// if the neck is found for a double top
      {
      for(int i=EndTop.IndexExtremum;i>1;i--)// define the intersection point
         {
         if ( High[i] > FarestTop.Price || Low[i] < Neck.Price0 )// if the movement goes beyond the formation, then the formation is fake
            {
            return -1;
            }         
         }
      }
      
   if ( Close[0] <= Neck.Price0 )
      {
      Neck.Time0=Time[0];
      return 1;
      }      
   return 0;
   }

int ExtremumsPatternFamilySearcher::CorrectNeckDownRight()// next the neck line must be corrected so that it finds an intersection with the price on the right
   {
   bool bCrossNeck=false;// indicates if the neck was crossed
   if ( !Neck.DirectionOfFormation )// if the neck is found for a double bottom
      {
      for(int i=EndTop.IndexExtremum;i>1;i--)// define the intersection point
         {
         if ( Low[i] < FarestTop.Price || High[i] > Neck.Price0  )// if the movement goes beyond the formation, then the formation is fake
            {
            return -1;
            }         
         }
      }
      
   if ( Close[0] >= Neck.Price0 )
      {
      Neck.Time0=Time[0];
      return 1;
      }   
      
   return 0;
   }
<

Asimismo, hay dos funciones inversas para ambos casos del patrón. Lo único a considerar aquí es que la intersección a la derecha no se considerará válida si el precio ha superado el patrón y luego ha regresado, esto se contempla aquí, y se muestra en la ilustración gráfica anterior.

Solo queda determinar cómo encontrar la tendencia anterior. Hasta ahora, para ello, usábamos la línea de pronóstico optimista. Si hay una parte del mercado entre el cuello y la línea de pronóstico optimista, entonces este será el movimiento deseado; también es importante que este movimiento no se prolongue demasiado en el tiempo, de lo contrario, definitivamente no será el movimiento:

bool ExtremumsPatternFamilySearcher::bWasTrend()// did we find the movement preceding the formation (also move here the anchor point tho the intersection)
   {
   bool bCrossOptimist=false;// denotes if the neck is crossed
   if ( FormationDirection == 1 )// if the optimistic forecast is at the double top
      {
      for(int i=Neck.Index1;i<BarsM;i++)// define the intersection point
         {
         if ( High[i] > Neck.Price0 )// if the movement goes beyond the neck, then the formation is fake
            {
            return false;
            }         
         if ( Low[i] < OptimistLine.Price0 )
            {
            OptimistLine.Time1=Time[i];
            return true;
            }
         }
      }
   else if ( FormationDirection == -1 )// if the optimistic forecast is at the double bottom
      {
      for(int i=Neck.Index1;i<BarsM;i++)// define the intersection point
         {
         if ( Low[i] < Neck.Price0 )//  if the movement goes beyond the nec, then the formation is fake
            {
            return false;
            }         
         if ( High[i] > OptimistLine.Price0 )
            {
            OptimistLine.Time1=Time[i];
            return true;
            }
         }      
      }
   return false;
   }

Visualmente, el funcionamiento del último predicado también se puede representar gráficamente:

Movimiento anterior

En este punto, merece la pena terminar el análisis del código en este artículo y pasar a las valoraciones visuales. A nuestro juicio, los aspectos principales de este método se han destacado suficientemente en este artículo. Trataremos los aspectos adicionales en el próximo artículo de la serie.

Veamos el resultado en el visualizador del simulador de estrategias de MetaTrader 5:

Siempre utilizamos el dibujado lineal en el gráfico: es rápido, simple y accesible. En la guía de ayuda de MQL5, podemos encontrar ejemplos del uso de cualquier objeto gráfico, incluida una línea. No vamos a mostrar el código para el dibujado (aquí sería redundante), pero podemos ver el resultado de su trabajo. Obviamente, todo esto se puede hacer mejor y más llamativo, pero, al fin y al cabo, solo tenemos un prototipo, y en estos casos también recomendamos usar una expresión muy común entre los matemáticos "necesario y suficiente":

pico triple en el visulaizador del simulador de MetaTrader 5

En este caso, hemos mostrado un ejemplo con pico triple. Consideramos que un ejemplo así sería más interesante. La búsqueda de picos dobles es similar: la única diferencia es que debemos establecer en los ajustes el número de picos que debe haber en el patrón. Con frecuencia, el código no encuentra los datos de formación, pero esto es solo una demostración, y todo se puede modificar fácilmente, cosa que haremos en el futuro.


Ideas para el futuro

En el futuro, analizaremos lo que no hemos mencionado en este artículo, y mejoraremos la calidad de la búsqueda de todas las formaciones; asimismo, mejoraremos la clase para que pueda encontrar la formación "cabeza y hombros". También intentaremos encontrar posibles híbridos de todas estas formaciones, una de las cuales puede ser el "N"-ésimo pico y los "hombros" múltiples. También querríamos decir que el ciclo no se limitará a esta familia particular de patrones: merece la pena esperar al nuevo material, será interesante y útil. Entre otras cosas, existen diferentes enfoques para encontrar varias formaciones; esta serie de artículos se ha concebido precisamente para mostrar todo ello con claridad y usar ejemplos que muestren tantos patrones como sea posible, destacando así todas las formas posibles de descomposición de un problema complejo en otros más simples. El ciclo contendrá:

  1. Otros patrones interesantes
  2. Otros métodos para detectar un tipo diferente de formación
  3. Comercio con la historia y recopilación estadística para diferentes instrumentos y marcos temporales
  4. Hay algunos patrones que no conocemos (por lo que, potencialmente, podemos anilizar un patrón del lector)
  5. Los niveles también se iluminarán (ya que los niveles se usan en todas partes para detectar reversiones)


    Conclusión

    Hemos tratado de hacer que el material sea lo más fácil y comprensible posible para todos. Esperamos que el lector encuentre algo útil para sí mismo al leer este artículo. La conclusión de este artículo en particular, a nuestro parecer, es que (como podemos ver en los gráficos del visualizador del simulador de estrategias) un código simple es capaz de encontrar las formaciones más complejas, y no resulta necesario en absoluto usar redes neuronales o escribir/utilizar algunos algoritmos complejos de visión por computadora. El lenguaje MQL5 dispone de funcionalidad suficiente para implementar incluso los algoritmos más complejos. La amplitud de las posibilidades está limitada solo por nuestra imaginación y empeño. 

    Traducción del ruso hecha por MetaQuotes Ltd.
    Artículo original: https://www.mql5.com/ru/articles/9394

    Archivos adjuntos |
    Prototype.zip (309.42 KB)
    Gráficos en la biblioteca DoEasy (Parte 78): Fundamentos de animación en la biblioteca. Cortando las imágenes Gráficos en la biblioteca DoEasy (Parte 78): Fundamentos de animación en la biblioteca. Cortando las imágenes
    En el artículo, definiremos los principios de animación que se usarán en algunas partes de la biblioteca, y también desarrollaremos una clase para copiar una parte de una imagen y pegarla en un lugar específico del objeto de formulario, con la posibilidad de guardar y restaurar la parte del fondo del formulario sobre la que se superpondrá la imagen.
    Gráficos en la biblioteca DoEasy (Parte 77): Clase de objeto Sombra Gráficos en la biblioteca DoEasy (Parte 77): Clase de objeto Sombra
    En el presente artículo, vamos a crear la clase para el objeto de sombra, que es heredero del objeto de elemento gráfico. Asimismo, añadiremos la posibilidad de rellenar el fondo del objeto con relleno en gradiente.
    Cómo ser un mejor programador (parte 01): 5 cosas que evitar para convertirse en un programador exitoso de MQL5 Cómo ser un mejor programador (parte 01): 5 cosas que evitar para convertirse en un programador exitoso de MQL5
    Hay muchos malos hábitos que impiden a los programadores principiantes e incluso avanzados sacar el cien por cien de rendimiento a su carrera de codificación. En este artículo, discutiremos y abordaremos dichos hábitos. El presente material es una lectura obligada para todos aquellos que quieran convertirse en desarrolladores exitosos en MQL5.
    Gráficos en la biblioteca DoEasy (Parte 76): Objeto de formulario y temas de color predeterminados Gráficos en la biblioteca DoEasy (Parte 76): Objeto de formulario y temas de color predeterminados
    En este artículo, describiremos la construcción de diferentes temas de diseño de la GUI en la biblioteca. Asimismo, crearemos el objeto "formulario", que es sucesor del objeto de clase del elemento gráfico, y también prepararemos los datos para crear las sombras de los objetos gráficos de la biblioteca y desarrollar posteriormente la funcionalidad.