English Русский 中文 Español Deutsch 日本語
Padrões com exemplos (Parte I): Topo múltiplo

Padrões com exemplos (Parte I): Topo múltiplo

MetaTrader 5Sistemas de negociação | 2 setembro 2021, 10:47
1 608 0
Evgeniy Ilin
Evgeniy Ilin

Sumário


Introdução

Os padrões são um tópico bastante comum na Internet, porque são usados por muitos traders e podem ser chamados de critérios visuais para analisar a direção da precificação. Já a negociação algorítmica é uma questão aparte completamente diferente. Ela não pode ter nenhum critério visual. EAs e indicadores têm seus próprios métodos para trabalhar usando séries de preços. Existem vantagens e desvantagens em ambos os casos. O código pode não ter a amplitude de pensamento e a qualidade de análise de um ser humano, mas, em vez disso, possui vantagens igualmente valiosas: uma velocidade única e uma quantidade incomparável de processamento de dados numéricos ou lógicos por unidade de tempo. Não é fácil dizer à máquina o que fazer, é preciso prática. Com o tempo, o programador começa a entender a máquina, e a máquina começa a entendê-lo. Este ciclo será útil para iniciantes da programação aprenderem como estruturar seus pensamentos e como dividir tarefas complexas em outras mais simples.


Padrões de reversão

Para mim, pessoalmente, os padrões de reversão têm uma definição muito vaga e, o mais importante, não têm nenhuma matemática "absoluta". Na verdade, para ser honesto, qualquer padrão não contém matemática, e a única coisa que podemos oferecer a esse respeito é a estatística. As estatísticas são o único critério para a verdade, mas elas são geradas com base na negociação real. É claro que não existem fontes que possam fornecê-las com o máximo de precisão e, se pudessem, fariam isso só dentro de uma plataforma de negociação, embora percebendo que não se beneficiariam disso. Eu acho que a resposta é óbvia para todos. A única maneira de contornar essa situação é o backtesting e a visualização no testador de estratégia. É claro que a qualidade dessa abordagem é inferior, mas tem uma vantagem inegável, a velocidade e a quantidade de dados. 

Claro, os padrões de reversão por si só não são uma ferramenta suficiente para determinar uma reversão de tendência, mas em combinação com outros métodos de análise, como, por exemplo, com níveis ou análise de velas, eles podem dar o resultado desejado. Dentro deste ciclo, eles não são tão interessantes quanto outros métodos de análise extremamente interessantes, mas com base nessas formações você pode praticar razoavelmente suas habilidades de negociação algorítmica. Além do treinamento ganho graças aos resultados, você definitivamente obterá um recurso auxiliar interessante e útil, se não para negociação algorítmica, pelo menos para aliviar os olhos do trader. Indicadores proveitosos são muito apreciados.


Por que o topo múltiplo e por que é interessante?

Esse padrão se tornou bastante popular na Internet devido à sua simplicidade. Esse padrão é bastante comum em qualquer instrumento de negociação e em qualquer período gráfico, simplesmente porque não há nada de complicado nele. Além disso, se você olhar atentamente para este padrão, você pode entender que usando negociação algorítmica e os recursos da linguagem MQL5, você pode expandir o conceito deste método e tentar criar um código geral que não esteja limitado apenas por um topo duplo. Se você criar esse protótipo corretamente, poderá explorar não apenas esse padrão, mas também todos os seus híbridos e herdeiros.

O sucessor clássico do topo múltiplo é o padrão cabeça-e-ombros, amado e conhecido por todos. Mas o problema é que não há informações estruturadas sobre como negociar usando esse padrão. Na verdade, esse é o problema de tantas estratégias que se ouvem agora, porque há muitas palavras bonitas, mas não há estatísticas. Tentarei entender neste artigo, se é possível usá-las dentro da negociação algorítmica. A única maneira de coletar estatísticas sem negociar em uma conta demo ou real é usar os recursos do testador de estratégias. Não vale a pena subestimar esta ferramenta, porque sem ela você não será capaz de tirar conclusões complexas a respeito de uma estratégia em particular.


Será possível ampliar o conceito de topo duplo?

Voltando ao tópico do artigo, tentarei mostrar um esquema no qual representarei uma árvore de padrões que começará com um topo duplo. Isso é necessário para entender o quão amplas são as possibilidades desse conceito:

Tree

Decidi combinar o conceito de vários padrões com a suposição de que eles são baseados aproximadamente na mesma ideia. Esta última mantem um começo simples: encontrar um bom movimento em qualquer direção e determinar corretamente o lugar para onde ele deve virar. Após o contato visual com o padrão proposto, o trader deve traçar corretamente algumas linhas auxiliares, que devem ajudá-lo a avaliar tanto o padrão em si quanto o cumprimento de determinados critérios, a determinar o ponto de entrada no mercado, bem como a estabelecer corretamente o alvo e definir o stop-loss. Neste caso, o take-profit pode ser usado em vez do alvo.

A unificação do conceito desses padrões, como pode ser visto na figura, pode ser baseada no fato de que eles podem ter algumas regras gerais de construção que são de betão. É essa firmeza na definição que influencia muito bem o resultado final, e acredito que essa seja a diferença fundamental entre um trader algorítmico e muitos traders que trabalham manualmente. A incerteza e as múltiplas interpretações dos mesmos princípios não trazem nada de bom.

Os padrões básicos aqui são:

  1. Topo duplo
  2. Topo triplo
  3. Cabeça e ombros

Esses padrões são muito semelhantes em estrutura e uso. Todos os três padrões são projetados para ajudar a identificar reversões. Todos os três padrões têm uma lógica semelhante na hora de desenhar linhas auxiliares. Ilustrarei isso exemplificando com um topo duplo:

Double extremum

Na figura, todas as linhas de que precisamos estão numeradas e significam o seguinte:

  1. Resistência à tendência
  2. Linha auxiliar para definir um topo pessimista (alguém poderia pensar que é um pescoço, eu acho que não, mas posso estar errado)
  3. Linha do pescoço
  4. Alvo otimista (também é um take-profit)
  5. Máximo stop-loss permitido (definido na parte superior)
  6. Linha de previsão otimista (igual ao movimento da tendência anterior)

O alvo pessimista é considerado em relação ao ponto de intersecção da linha do pescoço a partir da borda próxima ao mercado, para isso é tomada a distância entre "1" e "2", que é indicada como "t" e é adiado mais uma vez na direção da reversão proposta. O mínimo do alvo otimista é considerado da mesma maneira, só que a distância entre "5" e "3" já está adiada, o que é indicado como "s".


Escrevamos o código para visualizar topos múltiplos

Vamos começar definindo a lógica para determinar esses padrões. Para encontrar o padrão em questão, devemos seguir a lógica barra a barra, ou seja, trabalharemos não por ticks, mas por barras. Neste caso, isso aliviará muito o terminal, descartando cálculos desnecessários. Vamos começar definindo uma classe que simboliza algum observador independente que procurará uma formação. Todas as operações necessárias para a correta detecção da formação farão parte da instância, e toda a busca será realizada internamente. Decidi que fosse assim para poder modificar o código no futuro e, também, para expandir a funcionalidade e modificar a existente.

"Mapa" da classe

Vamos começar com o que acontecerá na classe:

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

A classe representa o conjunto de operações que uma pessoa executaria se estivesse no lugar da máquina. De uma forma ou de outra, a detecção de qualquer formação pode ser dividida num conjunto de operações simples que se sucedem. Existe essa regra na matemática: se você não sabe como resolver uma equação, simplifique-a. Ela também se aplica a qualquer algoritmo. Inicialmente, a lógica de detecção não é clara, mas se você souber por onde começar a detecção, toda a tarefa será imediatamente simplificada. Sendo assim, para encontrar todo o padrão, é necessário encontrar topos ou fundos.

Identificando topos e fundos

Sem eles, todo o significado do padrão se perde, embora sua presença seja uma condição necessária para ter um padrão, não é suficiente. Os topos podem ser definidos de maneiras diferentes. Mas o principal é a existência de uma meia onda pronunciada, que é determinada por dois movimentos opostos pronunciados, neste caso, várias barras seguidas numa direção. Para ver isso, precisamos determinar qual é o número mínimo de barras numa direção que sinaliza a presença de movimento. Para isso, deve existir uma variável 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;
   }

Os fundos são definidos de maneira espelhada:

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

Neste caso, fugi da lógica dos fractais e fiz uma própria para determinar topos e fundos, não acho que seja melhor ou pior que os próprios fractais, mas pelo menos não preciso usar nenhuma funcionalidade externa, e não preciso arrastar para trás funções desnecessárias integradas da linguagem. Certamente, tais funções são boas, mas, neste caso, são redundantes. Esta função define todos os topos e fundos em que trabalharemos no futuro. Se você visualizar o que está acontecendo nesta função, será assim:

Searching for tops & bottoms

Primeiro, procuramos o movimento 1, depois dele, o movimento 2, e, no número 3, a definição do topo ou fundo. Para "3", a lógica é movida para duas funções separadas, assim:

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

Claro, colocamos tudo isso mais tarde num contêiner pré-preparado. A lógica é que todas as estruturas usadas dentro da classe fornecem uma adição gradual de dados. Na saída, ao passar por todas as etapas da pesquisa e verificações, obtemos todos os dados com os quais podemos exibir esse padrão no gráfico. Claro, a lógica para encontrar topos e fundos pode ser completamente diferente, mas minha tarefa é apenas mostrar uma lógica de detecção simples para coisas complexas.

Selecionando os topos em que trabalharemos

Os topos e fundos que encontramos são apenas intermediários. Depois de os termos encontrado, é necessário selecionar aqueles topos que consideramos mais adequados para o papel dos ombros. Não podemos determinar isso com segurança, uma vez que nosso código não tem visão de máquina, e o uso de técnicas complexas provavelmente não beneficiará o desempenho. Por enquanto, vamos selecionar os topos mais próximos do 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;   
   }

Esta lógica, que apresentada no gráfico de nosso instrumento, será equivalente à variante na caixa roxa, mas vou desenhar mais algumas variações de todas as possíveis:

Choose tops & bottoms

Neste caso, temos uma lógica de escolha simples. Nossas variantes são numeradas "0" e "1" porque estão mais próximas do mercado. Certamente, aqui tudo é mostrado para um topo duplo, mas não é difícil imaginar essa mesma lógica para um topo triplo ou múltiplo, apenas que o número dos topos selecionados será um pouco maior.

Esta função será expandida no futuro para poder selecionar aleatoriamente os topos, como desenhei em azul na figura, para simular múltiplas instâncias dos buscadores de formações. Graças a isso, seremos capazes de encontrar de forma mais eficiente e frequente todas as formações no modo automático.

Determinando a direção da formação

Uma vez que identificamos os altos e baixos, devemos determinar se determinada formação pode ocorrer num determinado ponto do mercado, assumindo que ela deverá ter uma direção. Nesta fase, achei que deveria ser dada prioridade à direção cujo tipo de extremo é mais próximo do mercado. Com base em dada lógica, será selecionada a variante da figura numerada “0”, pois o mais próximo do mercado é o fundo e não o topo, claro, se levarmos em conta que a situação do mercado é exatamente a mesma como em nossa figura. Isso é feito de forma muito simples no código:

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

As ações ulteriores exigirão uma direção clara. A direção é equivalente ao tipo de padrão:

  1. Topo múltiplo
  2. Fundo múltiplo

Estas regras também funcionarão para a formação cabeça-e-ombros e para todos os outros híbridos quando se trata deles. A classe foi concebida como comum para todos os padrões desta família e, em parte, essa generalidade já está funcionando.

Filtros para descartar padrões inválidos

Agora vamos mais longe. Como sabemos que temos uma direção e uma das formas de selecionar topos e fundos, devemos prever que, para um topo múltiplo, os topos que estão entre os selecionados sejam menores que o mais baixo (de entre os selecionados). E para um fundo múltiplo deve ser maior que o mais alto dos escolhidos. Isso é necessário para que, no caso de uma seleção aleatória de topos, todos os selecionados sejam claramente distinguidos. Caso contrário, esta verificação não será necessária:

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

Se representarmos a versão correta e incorreta da seleção aleatória de topos, que é o que a última função-predicado faz, então tudo ficará assim:

Controle de topos não considerados


Certamente, todos esses critérios são absolutamente espelhados para os padrões de alta e de baixa. Na figura, é tomado como exemplo um padrão de alta, e, eu acho que todos podem imaginar facilmente o segundo caso.

Após a conclusão dos procedimentos preparatórios, podemos começar a procurar o pescoço. Diferentes traders constroem seus pescoços de maneiras diferentes. Eu destaquei provisionalmente vários tipos de construções:

  1. Inclinadas (não baseadas em sombras)
  2. Horizontais (não baseadas em sombras)
  3. Baseadas no ponto mais alto ou mais baixo, com inclinação (baseadas em sombras)
  4. Baseadas no ponto mais alto ou mais baixo, horizontalmente (baseadas em sombras)

Por razões de segurança e aumento das chances de lucro, acredito que devemos escolher a variante 4. Esta escolha deve-se às seguintes considerações:

  • Uma localização mais clara do início do movimento de reversão
  • Uma implementação no código mais fácil
  • Determinação inequívoca do ângulo de inclinação (horizontal)

Talvez isso não seja totalmente correto do ponto de vista da construção, mas não encontrei regras claras. Do ponto de vista da negociação algorítmica, isso não é crítico, sendo que se encontrarmos pelo menos algo racional nesse padrão, o testador ou a visualização definitivamente nos mostrarão algo. Além disso, será necessário pensar no fortalecimento dos indicadores de negociação, o que será uma história completamente diferente.

Criei duas funções espelhadas para os padrões de alta e de baixa, padrões esses que definem todos os parâmetros necessários do pescoço:

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 o pescoço de maneira correta e simples, é melhor seguir as mesmas regras de construção dele para todos os padrões da família selecionada. Por um lado, isso nos poupará de detalhes desnecessários, que em nosso caso não darão nada. A fim de construir um pescoço para um topo múltiplo de qualquer complexidade, é melhor usar os dois topos extremos do padrão. Os índices desses topos serão os índices entre os quais buscaremos o menor ou o maior preço no segmento de mercado selecionado. O pescoço será uma linha horizontal regular. Os primeiros pontos de ancoragem devem estar exatamente neste nível, e é melhor considerar o tempo de ancoragem exatamente igual ao tempo dos topos ou fundos extremos (dependendo de qual padrão estamos considerando). É assim que se verá isso:

Pescoço

A janela de pesquisa do mínimo ou máximo está localizada exatamente entre o primeiro e o último topo. Esta regra funciona para qualquer padrão desta família, com qualquer número de topos e fundos.

Para determinar um alvo otimista, devemos primeiro determinar o tamanho do padrão. O tamanho do padrão é a dimensão vertical da cabeça ao pescoço, em pontos. Para fazer isso, precisamos encontrar o topo mais distante do pescoço, que será a borda do padrão:

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 os topos se revelem muito diferentes entre si, é necessário realizar uma verificação adicional. Somente se essa verificação for aprovada, poderemos prosseguir para as próximas etapas. Na realidade, deve haver duas verificações, uma para o tamanho vertical dos extremos e a outra para o horizontal (tempo). Se os topo estiverem muito dispersos no tempo, essa variante também não será apropriada. É assim que fica a verificação do tamanho 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 o tamanho vertical correto dos topos, precisamos de dois deles. O primeiro é o mais distante do pescoço e o segundo é o mais próximo, respectivamente. Se esses tamanhos forem muito diferentes, essa formação pode acabar sendo falsa e é melhor não arriscar e marcá-la como inválida. Da mesma forma que com o predicado anterior, tudo isso pode ser acompanhado por um gráfico apropriada de como se pode e não se pode fazer:

Controle de dimensões verticais

Visualmente, isso é fácil de determinar, mas o código precisa de algum tipo de indicador quantitativo. É fácil adivinhar que, neste caso, basta:

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

Eu acredito que o indicador é eficaz o suficiente para eliminar um número suficientemente grande de falsos padrões, no final, mesmo o melhor código não será capaz de detectar essas coisas de forma mais eficiente do que nosso olho, mas a negociação algorítmica assume esse fato desde o início, a única coisa que podemos fazer é trazer a lógica o mais próximo possível da realidade, mas você definitivamente precisa saber quando parar.

A verificação horizontal será semelhante, com a única diferença de que consideramos os índices das barras como os tamanhos (você pode usar o tempo, isso não é 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 verificação, você pode pegar um indicador semelhante e, da mesma forma, representar tudo isso graficamente:

Controle de dimensão horizontal

Nesse caso, os critérios quantitativos serão os mesmos, apenas com a dimensão do índice ou tempo, e não com pontos. Além disso, o número com o qual estamos comparando provavelmente deve ser exibido separadamente, o que dará espaço para ajuste flexível:

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

O linha do pescoço deve necessariamente cruzar o preço à esquerda, porque isso significaria que uma tendência poderia tê-lo precedido:

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

Da mesma forma, existem duas funções espelhadas para os padrões de alta e de baixa. Abaixo está uma ilustração desse predicado e do seguinte:

Controle de interseção esquerda e direita

As caixas azuis marcam os segmentos de mercado onde exercemos esse controle. Ambos os segmentos estão atrás do padrão, à esquerda e à direita dos picos extremos. 

Restam apenas duas verificações:

  1. Precisamos exatamente do padrão que cruza o pescoço no momento atual (ou seja, na vela zero)
  2. É necessário que o padrão seja precedido por um movimento maior ou igual ao tamanho do próprio padrão

O primeiro ponto é necessário para negociação algorítmica. Eu não acho que vale a pena detectar formações apenas para olhar para elas, embora esta função exista. Precisamos detectar e encontrar exatamente o ponto a partir do qual podemos negociar para abrir imediatamente uma posição, sabendo que já estamos no ponto de entrada. O segundo ponto é uma das condições necessárias, pois sem um bom movimento prévio, o próprio padrão é inútil.

O cruzamento com velas zero (verificação do cruzamento à direita) é considerado assim:

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

Da mesma forma, existem duas funções espelhadas para ambos os casos do padrão. A única coisa a ter em mente aqui é que o cruzamento à direita não é considerado válido se o preço vai além do padrão e depois volta, isso é apresentado aqui e é mostrado na ilustração anterior.

Resta apenas determinar como encontrar a tendência anterior. Até agora, para esses fins, utilizo a linha de previsão otimista. Se há um pedaço de mercado entre o pescoço e a linha da previsão otimista, então esse é o movimento desejado, também é importante que ele não seja muito prolongado, caso contrário, definitivamente não é um movimento:

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, o trabalho do último predicado também pode ser representado graficamente:

Movimento anterior

Neste ponto, acho que vale a pena terminar a revisão do código neste artigo e passar para as avaliações visuais. Acho que os principais aspectos desse método foram suficientemente destacados aqui. Aspectos adicionais serão abordados no próximo artigo da série.

Vejamos o resultado no visualizador do testador de estratégias do MetaTrader 5:

Eu sempre uso o desenho de linha porque é rápido, simples e acessível. Na Ajuda do MQL5, você pode encontrar exemplos de como usar qualquer objeto gráfico, incluindo o de linha. Não vou dar o código para desenho, é redundante aqui, mas vocês podem ver seu resultado de trabalho. Claro, tudo isso pode ser feito melhor e mais colorido, mas afinal temos apenas um protótipo, e nesses casos também recomendo que você use uma expressão muito comum entre os matemáticos - "necessário e suficiente":

topo triplo no visualizador do testador de estratégias do MetaTrader 5

Neste caso, mostrei um exemplo com topo triplo. Achei que esse exemplo seria mais interessante. A busca por topos duplos é semelhante, só que nas configurações você precisa definir o número de topos que devem estar no padrão. Embora o código não encontre os dados de formação com frequência, isto é apenas uma demonstração, podendo ser facilmente modificado, o que farei no futuro.


Ideias para o futuro

Mais para frente, iremos considerar o que não foi dito neste artigo e melhorar a qualidade da pesquisa para todas as formações, bem como refinar a classe para encontrar a formação cabeça-e-ombros. Tentaremos também encontrar possíveis híbridos de todas essas formações, um dos quais pode ser o "N"-ésimo topo e vários "ombros". Também gostaria de dizer que o ciclo não se limitará a esta família particular de padrões e que vale a pena esperar por novos materiais interessantes e úteis. Entre outras coisas, existem diferentes abordagens para encontrar formações. Esta série de artigos foi concebida precisamente com o objetivo de claramente e usar exemplos para mostrar o máximo de padrões possível, destacando assim todas as maneiras possíveis de quebrar um problema complexo em mais simples. O ciclo conterá:

  1. Outros padrões interessantes
  2. Outros métodos de detecção de formações de outro tipo
  3. Negociação com base no histórico e coleta de estatísticas para diferentes instrumentos e timeframes
  4. Existem muitos padrões e eu não sei todos (por isso posso potencialmente considerar o seu padrão)
  5. Os níveis também serão tratados (já que os níveis são sempre usados para detectar reversões)


    Conclusão

    Tentei tornar o material o mais fácil e compreensível possível para todos. Espero que encontrem algo útil depois de ler este artigo. A conclusão deste artigo em particular, ao que me parece, foi que, como pode ser visto pelos gráficos do visualizador do testador de estratégia, um código simples é capaz de encontrar as formações mais complexas e não é necessário usar redes neurais ou escrever/usar algoritmos de visão de máquina complexos. A linguagem MQL5 tem funcionalidade suficiente para implementar até mesmo os algoritmos mais complexos. A amplitude de possibilidades é limitada apenas pela imaginação e afinco. 

    Traduzido do russo pela MetaQuotes Ltd.
    Artigo original: https://www.mql5.com/ru/articles/9394

    Arquivos anexados |
    Prototype.zip (309.42 KB)
    Gráficos na Biblioteca DoEasy (Parte 78): princípios de animação dentro da biblioteca Corte de imagens Gráficos na Biblioteca DoEasy (Parte 78): princípios de animação dentro da biblioteca Corte de imagens
    Neste artigo, definiremos os princípios de animação que usaremos em algumas partes da biblioteca, desenvolveremos uma classe para copiar uma parte de uma imagem e colá-la no local especificado do objeto-forma, preservando e restaurando aquela parte do fundo da forma sobre a qual a imagem será colocada.
    Gráficos na biblioteca DoEasy (Parte 77): classe do objeto Sombra Gráficos na biblioteca DoEasy (Parte 77): classe do objeto Sombra
    Neste artigo, criaremos uma classe separada para o objeto de sombra (um herdeiro do objeto de elemento gráfico) e também adicionaremos a capacidade de ocupar o fundo do objeto com um preenchimento gradiente.
    Gráficos na biblioteca DoEasy (Parte 79): classe para o objeto quadro-de-animação e seus objetos herdeiros Gráficos na biblioteca DoEasy (Parte 79): classe para o objeto quadro-de-animação e seus objetos herdeiros
    Neste artigo, desenvolveremos uma classe para um quadro de animação e seus herdeiros. A classe permitirá desenhar formas, bem como salvar e restaurar o plano de fundo.
    Gráficos na biblioteca DoEasy (Parte 76): objeto forma e temas de cores predefinidos Gráficos na biblioteca DoEasy (Parte 76): objeto forma e temas de cores predefinidos
    Neste artigo, descreveremos o conceito de criação de temas de GUI na biblioteca, criaremos um objeto forma que será descendente de um objeto da classe do elemento gráfico, prepararemos dados para criar as sombras dos objetos gráficos da biblioteca e para continuar desenvolvendo a funcionalidade no futuro.