English Русский 中文 Español Deutsch 日本語
preview
Conjunto de ferramentas para marcação manual de gráficos e negociação (Parte III). Otimização e novas ferramentas

Conjunto de ferramentas para marcação manual de gráficos e negociação (Parte III). Otimização e novas ferramentas

MetaTrader 5Exemplos | 20 janeiro 2022, 09:06
821 0
Oleh Fedorov
Oleh Fedorov

Introdução

Nos artigos anteriores (1, 2) descrevi a biblioteca Shortcuts e exemplifiquei como usá-la na forma de EA. De certa forma, ela é como um organismo vivo: criatura que é dada à luz, tem contato com o meio onde deverá viver. O ambiente muda constantemente e estabelece certas regras. Uma delas diz: "desenvolva-se" 🙂. E o organismo tem de se desenvolver para estar em correspondência... Este artigo apresenta alguns resultados de tal processo.

Lembre-se que a biblioteca consiste em 5 arquivos.

Shortcuts.mqh é o arquivo mais importante. Ele não só armazena a lógica de processamento de teclas pressionadas, senão que também é o único que é diretamente incorporado ao Expert Advisor ou ao indicador. Desse modo, ele inclui os outros arquivos e efetua sua inicialização.

No arquivo GlobalVariables.mqh foram colocadas todas as variáveis globais. Elas são, basicamente, configurações que permitem definir as cores, espessura da linha, fator de prolongação de linhas retas, etc.

O arquivo Mouse.mqh contém a descrição da classe para movimentação do mouse. Ele armazena as coordenadas atuais do cursor (tanto em pixels quanto em "preço-tempo") e o número da barra atual.

O arquivo Utilites.mqh contém funções auxiliares. Ele permite calcular os extremos das barras, os pontos de intersecção de linhas e outras coisas úteis que não estão diretamente relacionadas ao desenho, mas que o definem completamente.

O arquivo Graphics.mqh trata, em essência, do desenho, e, para isso, usa os dados dos outros arquivos. Além disso, é o arquivo Shortcuts.mqh que chama suas funções, principalmente.

Nem sempre sou rigoroso com o escopo das funções. Às vezes, os cálculos podem ser realizados dentro das funções de desenho. Por enquanto, estou à vontade com desenvolver e dar manutenção desse jeito. Talvez um dia corrija estas imperfeições.

Este projeto ilustra como o código da biblioteca pode ser usado num indicador.


Otimizando a velocidade da biblioteca

Neste caso, as alterações são mínimas.

Respeito a projetos com versões iniciais da biblioteca, por que escolhi um EA e não um indicador? A resposta é simples: cada EA funciona em seu próprio thread de execução (e, idealmente, não afeta outros EAs), dessa forma, o terminal é menos lento se for necessário manipular atalhos de teclado em vários gráficos.

O objetivo de um EA é negociar, porém este produto de software não faz isso. Além disso, quando há um indicador no gráfico, é muito mais fácil colocar outro EA. Assim, em algum momento foi decidido que o indicador deveria existir, e a questão da velocidade veio à tona. Isto último é especialmente importante se o usuário tiver muitas janelas abertas, p.e. se usuário tiver 40 guias abertas (longe do limite), será difícil processar as verificações de teclas pressionadas se elas ocorrerem em todos os gráficos simultaneamente.

E foi aí que uma ideia brilhante iluminou minha cabeça: por que verificar todos os gráficos? Se apenas a janela ativa estiver em funcionamento, as verificações só deverão ocorrer nessa janela.

O código é muito simples.

/* Shortcuts.mqh */

void CShortcuts::OnChartEvent(
  const int id,
  const long &lparam,
  const double &dparam,
  const string &sparam
)
 {
 //...

  if(ChartGetInteger(0,CHART_BRING_TO_TOP)==false)
   {
    return;
   }
 
 //...

No início da função é necessário verificar se o gráfico em questão está ativo, ou seja, se ele está em primeiro plano. Nós não precisamos fazer nada se este não for o caso.

Eu não medi o desempenho real ou o ganho de velocidade como porcentagem. Mas de acordo com o feedback dos usuários que baixaram a biblioteca e que realmente usam muitas guias, o aplicativo se tornou mais rápido, mesmo na forma de indicador. Isto é exatamente o que precisamos.

Neste ponto, devemos lembrar o objetivo do aplicativo.

Em primeiro lugar, ele é projetado para executar funções ocasionais (portanto, não é chamado a cada tick) e, por isso, consome recursos por um tempo muito limitado.

Em segundo lugar, os gráficos em si são uma fonte de problemas de velocidade se seu computador não for suficientemente potente. Quanto mais objetos no gráfico, mais difícil é processá-los. No entanto, como a biblioteca é, de facto, gráfica, eu tenho que aceitar o custo de desenhar e controlar cuidadosamente o que está sendo desenhado.

Em terceiro lugar, a função que mais consome recursos do programa é a de busca de extremos. Porém, não sei como implementá-la mais rapidamente. Por isso, considero que a atual implementação é ótima, por enquanto. De qualquer forma, esta função não é chamada com tanta frequência, apenas quando são traçadas linhas e outras formas úteis, e para já seu desempenho não otimizado pode ser ignorado .

Todas as outras funções são chamadas com muito menos frequência e funcionam rápido o suficiente para as tarefas deste aplicativo, e podemos não falar sobre elas.


Refatoração do código: a interconexão

A versão do código apresentada nos artigos anteriores assume que o aplicativo é monolítico e que suas partes não poderão ser utilizadas separadamente. É por isso que o código utilizava as configurações globais diretamente, enquanto alguns utilitários dependiam da classe do mouse.

Devido a isto, eu poderia escrever o código mais rapidamente, mas do ponto de vista da reutilização, era inconveniente. Se eu precisar anexar o arquivo de utilitário existente a um novo projeto que não utilize o mouse ou gráficos, ainda tenho que anexar o arquivo de configurações globais e a classe do mouse.

Isto é errado e desconfortável.

Por isso resolvi fazer uma pequena modificação no código. É por isso que decidi modificar ligeiramente o código. Todas as variáveis globais ainda são utilizadas. Não podemos removê-las, pois elas são as configurações.

Adicionei campos privados contendo cópias destas variáveis às classes principais. Para armazenar estes valores, é necessário adicionar funções especiais "públicas". Eles também são necessários para a leitura dos valores.

Ficam mais ou menos assim:

private:
  /* Fields */
  //---
  static int          m_TrendLengthCoefficient;

public:
  /* Methods */
  //---
  static int          TrendLengthCoefficient(void) {return m_TrendLengthCoefficient;}
  //---
  static void         TrendLengthCoefficient(int _coefficient) {m_TrendLengthCoefficient=_coefficient;}

O processo parece longo e entediante, considerando o número de configurações existentes.

Mas a vantagem é grande. Em primeiro lugar, a classe se torna independente de arquivos externos. Se alguém quiser usar a classe, pode usar apenas as variáveis necessárias e definir seus valores conforme a necessidade.

Em segundo lugar, tais variáveis podem ser alteradas no momento da execução. Por exemplo, alguém quer escrever uma função que constrói um leque de linhas a partir de um único ponto. Cada linha é duas vezes mais longa que a anterior, e elas divergem em ângulos diferentes. Como fazer isso? Usando a implementação atual da classe CUtilites: antes de cada desenho, definimos o parâmetro descrito para o exemplo, TrendLengthCoefficient, colocando os pontos iniciais nas mesmas coordenadas, enquanto os pontos finais devem ser colocados em algum círculo de raio arbitrário.

Em terceiro lugar, os dados dentro da classe podem ser agrupados de qualquer forma conveniente. Podemos criar estruturas ou mesmo classes completas que armazenam separadamente, digamos, dados relativos aos retângulos, dados relativos às linhas diagonais e dados sobre os níveis. Do ponto de vista do usuário final, a interface (a maneira de acessar os dados) não foi alterada.

Em quarto lugar, os dados não devem necessariamente ser armazenados na RAM. Às vezes, as variáveis podem ser armazenadas nas variáveis globais do terminal, ou geralmente armazenadas em arquivos ou mesmo em um banco de dados. Alguns parâmetros podem ser calculados de imediato com base em outros parâmetros. Com esta disposição "correta" do acesso aos dados, como mostrado no exemplo acima, o usuário final pode reutilizar o código sem se preocupar com a implementação direta das estruturas de dados. De qualquer forma, é muito conveniente, apesar do esforço adicional necessário para escrever código excedente, chamar funções desnecessárias e ter que inicializar as variáveis necessárias cada vez que uma instância é criada.

Por isso, reescrevi tudo o que pude na versão atual da biblioteca para corresponder ao novo estilo, para que o arquivo de utilitários possa agora ser usado "como está" em qualquer projeto.

A classe do mouse originalmente continha todas as funções relacionadas, de modo que não havia nada a ser corrigido nela. A classe de desenho é inútil sem os utilitários. Entretanto, mudei todas as configurações externas nela para a nova forma de campos.

Assim, aqui está o que temos. O mouse e os utilitários são classes bastante independentes que podem ser usadas por conta própria ou em combinações. A classe de desenho usa ambas, mas é independente de outros arquivos externos, exceto o gerenciador que deve inicializar a classe. A classe que contém atalhos de teclado é a classe de gerenciamento, ou seja, o gerenciador que faz o código inteiro funcionar conforme necessário. Assim, a interconexão das classes é agora muito mais fraca, o que resulta nos benefícios acima mencionados.


Ferramenta fios

Na versão anterior da biblioteca, quando se desenhava uma linha de tendência, um fio cruzado era desenhado no final da linha para marcar o tempo e os níveis de preço. Para criá-lo, eu usava duas linhas simples, vertical e horizontal. Entretanto, para mostrá-lo num ponto arbitrário do gráfico, tinha que pressionar duas teclas, H e I. Às vezes era conveniente... mas, na verdade, queremos menos esforço... Por isso, acrescentei a ferramenta fio.

A ferramenta funciona de uma maneira comum. Se movermos o cursor para o local desejado e pressionarmos X, será gerado o fio. Abaixo está o código da função.


/* Graphics.mqh */

//+------------------------------------------------------------------+
//| Draws a crosshair at specified coordinates. If the coordinates   |
//|   are not set, the mouse pointer coordinates are used.           |
//+------------------------------------------------------------------+
//| Parameters:                                                      |
//|   datetime _time - crosshair time                                |
//|   double _price - price level                                    |
//+------------------------------------------------------------------+
void CGraphics::DrawCross(datetime _time=-1,double _price=-1)
 {
  datetime time;
  double price;
//---
  if(_time==-1)
   {
    time=CMouse::Time();
   }
  else
   {
    time=_time;
   }

  if(_price==-1)
   {
    price=CMouse::Price();
   }
  else
   {
    price=NormalizeDouble(_price,Digits());
   }
  DrawSimple(OBJ_HLINE,time,price);
  DrawSimple(OBJ_VLINE,time,price);
  
 }

Para aqueles familiarizados com o código da versão anterior, não há nada de especial desta vez. Em primeiro lugar, estabelecemos as coordenadas. Se estas coordenadas forem passadas usando os parâmetros, então são usados exatamente estes valores. Se os parâmetros padrão forem definidos, então usaremos as coordenadas do ponteiro do mouse.

Além disso, a linha é desenhada usando a função que foi descrita no segundo artigo.

Para que esta função funcione exatamente da maneira descrita, ela deve ser chamada a partir do arquivo Shortcuts.mqh quando ocorre um determinado evento - tecla X pressionada.

/* GlobalVariables.mqh */
  
  // ...
  
  input string   Cross_Key="X";                       // Crosshair where the mouse was clicked
  
  // ...
  /* Shortcuts.mqh */
  
  void CShortcuts::OnChartEvent( /* ... */ )
    switch(id)
     {
       case CHARTEVENT_KEYDOWN:
       
       // ... 
       
       //--- Draw a crosshair
       if(CUtilites::GetCurrentOperationChar(Cross_Key) == lparam)
        {
         m_graphics.DrawCross();
        }
     }

Ferramenta: linha de tendência atravessando extremos arbitrários

A opção de criar uma linha de tendência que atravessa extremos com um certo número de barras à esquerda e à direita é prática. No entanto, às vezes pode ser conveniente traçar uma linha que cruza extremos arbitrários. Isto pode ser feito usando o comando Q.

O gif abaixo mostra um exemplo de como esta função funciona.

Exemplo de desenho de linha que atravessa extremos arbitrários

Como meu aplicativo de captura de tela tem algumas características específicas, eu tive que clicar no gráfico sempre antes de desenhar. Em condições reais, basta ativar o gráfico, e em seguida é possível desenhar tantas linhas quantas forem necessárias.

A linha é desenhada em duas etapas. No primeiro passo, você deve pressionarQ. Isto ativa o modo de desenho de linha arbitrário e marca o primeiro ponto, ficando assim claro que o comando foi executado.

Se não quisermos utilizar este ponto extremo (onde está o marcador), podemos pressionar Q mais uma vez - isto irá mudar o modo e cancelará o desenho. (Algum dia, provavelmente mudarei este comportamento ajustando a tecla Esc para cancelar, embora eu pessoalmente me sinta confortável na forma como funciona agora).

Se o primeiro ponto estiver correto, o próximo ponto será selecionado clicando perto do segundo extremo. Se isto resultar, o marcador não será mais necessário. Ele será apagado e a linha de tendência será desenhada.

Como os parâmetros da linha "arbitrária" não dependem do modo T, pode-se configurar, digamos, que T desenha uma linha em negrito de 4 px de largura, quatro vezes maior que o espaçamento entre os extremos, enquanto Q desenha uma linha fina que é duas vezes maior que o espaçamento.

Como de costume, o código é dividido em vários arquivos.

Vamos começar do final, do processamento do evento CHARTEVENT_KEYDOWN:

/* Shortcuts.mqh */

void CShortcuts::OnChartEvent(
  const int id,
  const long &lparam,
  const double &dparam,
  const string &sparam
)
 {
   //...
   
   switch(id)
   {
   
   //...
   
     case CHARTEVENT_KEYDOWN:
      if(CUtilites::GetCurrentOperationChar(Free_Line_Key) == lparam)
       {
        m_graphics.ToggleFreeLineMode();
        if(m_graphics.IsFreeLineMode()){
          m_graphics.DrawFreeLine(CMouse::Bar(),CMouse::Above());
        }
       } 
    
    //...

Caso o programa determine que a tecla Q é pressionada (a letra é armazenada na variável externa Free_Line_Key), ela muda o modo de desenho. Se após a comutação do modo se verificar que o modo está ligado, será gerado um comando para executar a função de desenho de linha.

O clique é processado no evento

/* Shortcuts.mqh */

        //...
        
    case CHARTEVENT_CLICK:
        ChartClick_Handler();
      break;
      
      //...
      
}

//+------------------------------------------------------------------+
//| Processing a click on a free chart field                         |
//+------------------------------------------------------------------+
void CShortcuts::ChartClick_Handler()
 {
  
//---
  if(m_graphics.IsFreeLineMode()){
    m_graphics.DrawFreeLine(
      CMouse::Bar(),CMouse::Above()
    );
  }
  
 }

Mais uma vez, observe que quando a tecla é pressionada, o modo de desenho muda imediatamente, mesmo antes de qualquer desenho começar (o nome da minha função começa com Toggle). Este estado permanece até ser trocado novamente usando as teclas ou depois que a linha é desenhada. Ao clicar, o programa primeiro verifica se há algo a ser desenhado. Se houver, ele desenha e muda para o modo neutro.

A função ChartClick_Handler é implementada separadamente, pois pretendo adicionar mais modos que requerem cliques no gráfico. Por exemplo, o modo de exclusão de objetos complexos como fios cruzados ou níveis verticais que foram descritos no artigo anterior, às vezes pode exigir um clique no gráfico para cancelar o menu. Até agora, parece que uma implementação separada das funções de clique simplificará o desenvolvimento futuro. Mas todos estes recursos serão implementados mais tarde.

Por enquanto, vamos continuar a considerar como funciona o desenho.

/* Graphics.mqh */


//+------------------------------------------------------------------+
//|  Draws a line by arbitrary specified extrema. In the current     |
//|    implementation, the first extremum is set by a hot key        |
//|    (Q by default), the second is set by clicking near the        |
//|    required top                                                  |
//+------------------------------------------------------------------+
//|  Parameters:                                                     |
//|    int _bar - bar to start search at                             |
//|    bool _isUp - top or bottom?                                   |
//|    int _fractalSizeRight - number of bars to the right of extr   |
//|    int _fractalSizeLeft -  number of bars to the left of extremum|
//+------------------------------------------------------------------+
void CGraphics::DrawFreeLine(
  int _bar,
  bool _isUp,
  int _fractalSizeRight=1,
  int _fractalSizeLeft=1
)
 {
//--- Variables
  double    selectedPrice,countedPrice,trendPrice1,trendPrice2;
  datetime  selectedTime,countedTime,trendTime1,trendTime2;
  int       selectedBar,countedBar;
  int       bar1,bar2;

  string trendName="",trendDescription="p2;";
  int fractalForFirstSearch = MathMax(_fractalSizeRight,_fractalSizeLeft)* 2;

//--- Search for a bar that meets the extremum criteria
  selectedBar = CUtilites::GetNearesExtremumSearchAround(
    _bar,
    _isUp,
    _fractalSizeLeft,
    _fractalSizeRight
  );

//--- Building the starting marker
  if(0==m_Clicks_Count)
   {
    m_Clicks_Count=1;
    if(_isUp)
     {
      m_First_Point_Price=iHigh(NULL,PERIOD_CURRENT,selectedBar);
     }
    else
     {
      m_First_Point_Price=iLow(NULL,PERIOD_CURRENT,selectedBar);
     }
    m_First_Point_Time=iTime(NULL,PERIOD_CURRENT,selectedBar);
    //---
    m_First_Point_Time=CUtilites::DeepPointSearch(
                         m_First_Point_Time,
                         _isUp,
                         ENUM_TIMEFRAMES(Period())
                       );
    //---
    DrawFirstPointMarker(_isUp);
   
   }
//--- Processing a click on the chart
  else
   {
    ObjectDelete(0,m_First_Point_Marker_Name);
    if(_isUp)
     {
      countedPrice=iHigh(NULL,PERIOD_CURRENT,selectedBar);
     }
    else
     {
      countedPrice=iLow(NULL,PERIOD_CURRENT,selectedBar);
     }
    countedTime=iTime(NULL,PERIOD_CURRENT,selectedBar);
    //--- Move a point in time on smaller timeframes
    countedTime=CUtilites::DeepPointSearch(countedTime,_isUp,ENUM_TIMEFRAMES(Period()));

    //--- The line is always drawn from left to right. 
    //--- If it is not convenient, you can comment this part
    //---   up to the next comment
    if(countedTime<m_First_Point_Time)
     {
      trendTime1=countedTime;
      trendPrice1=countedPrice;
      trendTime2=m_First_Point_Time;
      trendPrice2=m_First_Point_Price;
     }
    else
     {
      trendTime2=countedTime;
      trendPrice2=countedPrice;
      trendTime1=m_First_Point_Time;
      trendPrice1=m_First_Point_Price;
     }
    //--- Set the description for future correction
    trendDescription+=TimeToString(trendTime2)+";"+DoubleToString(trendPrice2,Digits());

    //selectedPrice=CUtilites::EquationDirect(
    //                trendTime1,trendPrice1,trendTime2,trendPrice2,selectedTime
    //              );
    trendName=CUtilites::GetCurrentObjectName(allPrefixes[0],OBJ_TREND);
    
    TrendCreate(
      0,                    // Chart ID
      trendName,            // Line name
      0,                    // Subwindow number
      trendTime1,           // time of the first point
      trendPrice1,          // price of the first point
      trendTime2,           // time of the second point
      trendPrice2,          // price of the second point
      CUtilites::GetTimeFrameColor(
        CUtilites::GetAllLowerTimeframes()
      ),                    // line color
      Trend_Line_Style,     // line style
      Trend_Line_Width,     // line width
      false,                // background object
      true,                 // is the line selected
      true                  // ray to the right
    );
    
    bar1=iBarShift(NULL,0,trendTime1);
    bar2=iBarShift(NULL,0,trendTime2);
    selectedTime = CUtilites::GetTimeInFuture(
                     //iTime(NULL,PERIOD_CURRENT,0),
                     trendTime1,
                     (int)((bar1-bar2)*m_Free_Trend_Length_Coefficient),
                     COUNT_IN_BARS
                   );
    selectedPrice= ObjectGetValueByTime(0,trendName,selectedTime);
    ObjectSetInteger(0,trendName,OBJPROP_RAY,IsRay());
    ObjectSetInteger(0,trendName,OBJPROP_RAY_RIGHT,IsRay());
    ObjectMove(0,trendName,1,selectedTime,selectedPrice);
    //---
    m_Clicks_Count=0;
    ToggleFreeLineMode();
   }

  ObjectSetString(0,trendName,OBJPROP_TEXT,trendDescription);
  ChartRedraw();
 }

Como a função é bastante longa, provavelmente vou dividi-la em várias funções menores mais tarde. Espero que o destaque e os comentários ajudem a entender como funciona.

Nesta implementação, a função verifica ambos os sinais: o evento indicando que o desenho do primeiro ponto começou e a notificação de que o segundo ponto foi encontrado, e seu desenho começou. A variável m_Clicks_Count foi introduzida para distinguir estes eventos. A letra "m_" no início indica claramente que a variável é global para esta classe, e sua duração é igual à duração da instância objeto.

Se for a primeira chamada de função (ou seja, uma tecla foi pressionada), é necessário encontrar o primeiro ponto e desenhar o marcador.

Se for a segunda chamada, é necessário apagar o marcador, encontrar o segundo ponto e traçar uma linha. Estes foram os cinco blocos principais, enquanto todos os outros são necessários para sua implementação.

Na implementação atual, o preço futuro é determinado usando a própria linha reta. Geralmente, não é uma boa ideia, pois no momento de desenhar, o terminal tem que desenhar primeiro um raio, depois move o final da linha para o ponto desejado e decide se deseja desenhar o raio (dependendo das configurações externas). Normalmente, faço cálculos preliminares utilizando a famosa função de Igor Kim (Kim IV), que também está incluída na biblioteca. A parte rosa do código tem uma chamada comentada desta função. Entretanto, neste caso, se os pontos forem calculados por tempo, podemos ter um erro relacionado ao fim de semana, o que eu gostaria de evitar. Naturalmente, o erro poderia ser facilmente evitado calculando a linha por números de barras e depois recalculando os números em datas reais. Entretanto, a implementação atual me parece mais clara.

Assim, no código destacado em rosa, os extremos básicos já foram encontrados. O que precisamos fazer agora é traçar a linha. Primeiro, traçamos uma linha entre dois pontos extremos básicos - aqui devemos habilitar a propriedade "raio" para que a linha seja traçada no futuro (a função TrendCreate logo no início deste bloco).

Calculamos o tempo futuro necessário com base nas configurações:

selectedTime = CUtilites::GetTimeInFuture(
                     //iTime(NULL,PERIOD_CURRENT,0),
                     trendTime1,
                     (int)((bar1-bar2)*m_Free_Trend_Length_Coefficient),
                     COUNT_IN_BARS
                   );

Em seguidad, obtemos o preço desejado usando a função padrão.

selectedPrice= ObjectGetValueByTime(0,trendName,selectedTime);

Depois disso, resta apenas mover o segundo ponto da linha para as coordenadas desejadas e definir a propriedade real do raio (lembre que esta propriedade é alternada por padrão com a tecla R - da palavra Ray).

Uma vez desenhada a linha, precisamos desabilitar o estado de espera do clique, que é o que as seguintes linhas fazem 

    m_Clicks_Count=0;
    ToggleFreeLineMode();

Para o resto do código, em outros blocos desta função, a questão é um pouco mais complicada. Na verdade, adicionei algumas características relacionados ao trabalho conveniente com o uso de linhas retas.

A primeira característica está relacionada ao efeito de deslocamento das linhas nos tamframes inferiores. Se desenharmos linhas da maneira usual, quando alternarmos entre "janelas de tempo", será desenhado algo assim:

Extremidades da linha D1 Extremidades da linha H4

Pode-se ver que a borda esquerda da linha, que coincide exatamente com o extremo no período D1, é deslocada para a esquerda no período de 4 horas e não coincide com o extremo. Isto é um efeito bastante óbvio, porque o extremo do dia não precisa coincidir com seu início. Se quisermos mais precisão, ao desenhar à mão, podemos traçar uma linha aproximadamente, depois "descermos" para os timeframes inferiores e corrigir as extremidades.

Isso é bom para um, mas para dois gráficos... E se forem 20? Ou 100? Isso é irritante...

E, como a função de desenho automático já existe, por que não deixá-la fazer essa tarefa na hora de criar cada objeto?

Por causa disso foi criada a funçãoDeepPointSearch.


Função "associação profunda" DeepPointSearch

Na função de desenho de linha "livre", esta função é chamada duas vezes - uma para cada ponto. Ela está localizado no arquivo de utilitários e possui o seguinte código:

//+------------------------------------------------------------------+
//| Search for a given point on lower timeframes                     |
//+------------------------------------------------------------------+
//| Parameters:                                                      |
//|   datetime _neededTime - start time on a higher timeframe        |
//|   bool _isUp - search by highs or by lows                        |
//|   ENUM_TIMEFRAMES _higher_TF - the highest period                |
//+------------------------------------------------------------------+
//| Return value:                                                    |
//|   More accurate date (on the lowest possible timeframe)          |
//+------------------------------------------------------------------+
datetime CUtilites::DeepPointSearch(
  datetime _neededTime,
  bool _isUp,
  ENUM_TIMEFRAMES _higher_TF=PERIOD_CURRENT
)
 {
//---
  //--- As a result it gets the most accurate time available
  datetime deepTime=0;
  //--- current timeframe
  ENUM_TIMEFRAMES currentTF;
  //--- The number of the highest timeframe in the list of all available periods
  int highTFIndex = GetTimeFrameIndexByPeriod(_higher_TF); 
  //--- The higher period in seconds
  int highTFSeconds = PeriodSeconds(_higher_TF);
  //--- Current interval in seconds
  int currentTFSeconds;
  //--- Counter
  int i;
  //--- Bar number on a higher timeframe
  int highBar=iBarShift(NULL,_higher_TF,_neededTime);
  //--- Bar number on the current timeframe
  int currentBar;
  //--- The total number of bars on the current timeframe
  int tfBarsCount;
  //--- How many bars of a lower TF fit into one bar of a higher TF
  int lowerBarsInHigherPeriod;
  //--- Maximum allowed number of bars in the terminal
  int terminalMaxBars = TerminalInfoInteger(TERMINAL_MAXBARS);

//--- Loop sequentially through all timeframes
  for(i=0; i<highTFIndex; i++)
   {
    //--- Get a timeframe by a number in the list
    currentTF=GetTimeFrameByIndex(i);
//--- Check if this timeframe has the required time.
    tfBarsCount=iBars(NULL,currentTF);
    if(tfBarsCount>terminalMaxBars-1)
     {
      tfBarsCount=terminalMaxBars-1;
     }
    deepTime=iTime(NULL,currentTF,tfBarsCount-1);
//--- If it has, find it.
    if(deepTime>0 && deepTime<_neededTime)
     {
      currentTFSeconds=PeriodSeconds(currentTF);
      
      //--- Search for the required bar only within the higher TF candlestick
      lowerBarsInHigherPeriod=highTFSeconds/currentTFSeconds;
      currentBar = iBarShift(NULL,currentTF,_neededTime);
      
      if(_isUp)
       {
        currentBar = iHighest(
                       NULL,currentTF,MODE_HIGH,
                       lowerBarsInHigherPeriod+1,
                       currentBar-lowerBarsInHigherPeriod+1
                     );

       }
      else
       {
        currentBar = iLowest(
                       NULL,currentTF,MODE_LOW,
                       lowerBarsInHigherPeriod+1,
                       currentBar-lowerBarsInHigherPeriod+1
                     );
       }
      deepTime=iTime(NULL,currentTF,currentBar);
      //--- Once the required time is found, stop the search
      break;
     }
   }
//--- If reached the end of the loop
  if(i==highTFIndex)
   {
    //--- then the required time is only available on the higher timeframe.
    deepTime=_neededTime;
   }
//---
  return (deepTime);
 }

Para mim, a principal dificuldade foi entender como deveria funcionar o fragmento de pesquisa principal. É claro que primeiro precisamos decidir se há um tempo no histórico de que precisamos. Afinal, é sabido que os timeframes inferiores muitas vezes não contêm o que está nos maiores. A função padrão iBars conta o número de barras no histórico. No entanto, isso não é suficiente, pois o terminal só pode exibir um número limitado. Primeiro, com o seguinte código, veremos a capacidade de exibição do terminal

//--- Maximum allowed number of bars in the terminal
  int terminalMaxBars = TerminalInfoInteger(TERMINAL_MAXBARS);

Se houver muitas barras no histórico, nos limitamos a apenas aquelas que são exibidas.

Em seguida, com a função iTime, definimos o tempo da última barra no histórico. Se este tempo for maior do que o desejado, não vale a pena procurar mais, já que a data mais alta disponível é a mais recente, portanto simplesmente mudamos para a próxima, o timeframe maior. Se o último candle disponível no terminal for anterior àquele que estamos procurando, é muito provável que tenhamos encontrado o lugar mais profundo onde este ponto ainda é significativo.

A rotina começa depois de todas as verificações. O ponto necessário será o mais extremo dentro do alcance do candle do timeframe maior. Só precisamos determinar quantos candles devem ser analisados. Depois disso, as funções padrão ajudam a determinar o extremo mais extremo que nos permite calcular o tempo e terminar o trabalho.

Na implementação atual da biblioteca, esta função só se aplica às linhas que são chamadas pela tecla T e Q. No entanto, na próxima versão, esta função estará disponível para todos os instrumentos. Além disso, pretendo torná-la personalizável para cada instrumento separadamente.


Correção de tempo

A segunda característica específica desta implementação é a correção das linhas por tempo. A animação abaixo explica o problema.

Preste atenção à contração do último retângulo. O final da linha, que estava a uma distância de mais de um dia do retângulo central, acabou se tornando muito próximo a ele. Consequentemente, os pontos acima também se deslocaram (observe o comportamento da linha reta próxima ao topo). Quando a linha contrai, surgem novas rupturas que podem afetar a estratégia de negociação.

Isto pode não ser tão crucial para o mercado forex, onde os picos podem ocorrer, digamos, uma vez por semana. Mas no mercado de ações, tais brechas de tempo podem ocorrer todos os dias, dependendo da bolsa, e isto muitas vezes acontece dentro de um dia.

É aqui que a automação vem a calhar!

Para que a peça funcione como desejado, devemos de alguma forma salvar as coordenadas "corretas" e depois ajustá-las conforme a necessidade.

Eu escolhi a descrição de uma linha reta para salvar coordenadas, já que a maioria dos traders não usa descrições ao criar objetos automáticos. Opcionalmente, podemos usar arquivos com uma lista de linhas ou variável global de terminal se houver muitas linhas.

/* Graphics.mqh */

void CGraphics::DrawFreeLine(//...)
 {

//...
  string trendDescription="p2;";

//...
  trendDescription+=TimeToString(trendTime2)+";"+DoubleToString(trendPrice2,Digits());
  
//...
  ObjectSetString(0,trendName,OBJPROP_TEXT,trendDescription);

A seguir, aplicamos as ações descritas anteriormente às coordenadas na linha "física". Acho que o código abaixo é bastante claro.

/* Utilites.mqh */

//+------------------------------------------------------------------+
// | Adjusts the position of line end in the future in case of price |
//|   gaps                                                           |
//+------------------------------------------------------------------+
//| Parameters:                                                      |
//|   string _line_name - the name of the line to be corrected       |
//+------------------------------------------------------------------+
void CUtilites::CorrectTrendFutureEnd(string _line_name)
 {
//---
  if(ObjectFind(0,_line_name)<0)
   {
    PrintDebugMessage(__FUNCTION__+" _line_name="+_line_name+": Object does not exist");
    //--- If there is no object to search, there is nothing more to do.
    return;
   }
  //--- Get a description
  string line_text=ObjectGetString(0,_line_name,OBJPROP_TEXT);
  
  string point_components[]; // array for point description fragments
  string name_components[];  // array containing line name fragments
  string helpful_name="Helpful line"; // the name of the auxiliary line
  string vertical_name=""; // the name of the corresponding vertical from the crosshair
  
  //--- Get the point time and price in string form
  int point_components_count=StringSplit(line_text,StringGetCharacter(";",0),point_components);
  
  datetime time_of_base_point; // time of the basic point
  datetime time_first_point,time_second_point; // the time of the first and the second point
  datetime time_far_ideal; // estimated time in the future
  double price_of_base_point; // the price of the basic point
  double price_first_point,price_second_point; // the prices of the first and the second point
  int i; // counter

//--- Check if the line is needed
  if(line_text=="" || point_components_count<3 || point_components[0]!="p2")
   {
    PrintDebugMessage(__FUNCTION__+" Error: the line cannot be used");
    return;
   }
//--- Get the coordinates of the "basic" point from the line description
  time_of_base_point=StringToTime(point_components[1]);
  price_of_base_point=StringToDouble(point_components[2]);
  if(time_of_base_point==0 || price_of_base_point==0)
   {
    PrintDebugMessage(__FUNCTION__+" Error: Unusable description");
    return;
   }
//--- Get the real coordinates of the line
  time_first_point = (datetime)ObjectGetInteger(0,_line_name,OBJPROP_TIME,0);
  time_second_point = (datetime)ObjectGetInteger(0,_line_name,OBJPROP_TIME,1);
  price_first_point = ObjectGetDouble(0,_line_name,OBJPROP_PRICE,0);
  price_second_point = ObjectGetDouble(0,_line_name,OBJPROP_PRICE,1);

//--- Create an auxiliary line (from the starting point to the base one)
  MakeHelpfulLine(
    time_first_point,
    price_first_point,
    time_of_base_point,
    price_of_base_point
  );

//--- Calculate the correct time for the current situation
  time_far_ideal=ObjectGetTimeByValue(0,helpful_name,price_second_point);
//---
  if(time_second_point != time_far_ideal)
   {
    //--- move the free end of the trend line
    ObjectMove(0,_line_name,1,time_far_ideal,price_second_point);
    //--- and the corresponding vertical
    StringSplit(_line_name,StringGetCharacter("_",0),name_components);
    for(i=0; i<ObjectsTotal(0,-1,OBJ_VLINE); i++)
     {
      vertical_name = ObjectName(0,i,-1,OBJ_VLINE);
      if(name_components[0]==StringSubstr(vertical_name,0,StringFind(vertical_name,"_",0)))
       {
        if((datetime)ObjectGetInteger(0,vertical_name,OBJPROP_TIME,0)==time_second_point)
         {
          ObjectMove(0,vertical_name,0,time_far_ideal,price_second_point);
          break;
         }
       }
     }
   }
  // Delete the auxiliary line
  RemoveHelpfulLine();
 }

Este código deve ser chamado em alguns intervalos. Eu o defino para o início de cada nova hora.

/* Shortcuts.mq5 */

int OnCalculate(/*...*/)
 {
   //...
   if(CUtilites::IsNewBar(First_Start_True,PERIOD_H1))
   {
    for(i=0; i<all_lines_count; i++)
     {
      line_name=ObjectName(0,i,-1,OBJ_TREND);
      CUtilites::CorrectTrendFutureEnd(line_name);
      ChartRedraw();
     }
   }
   //...
 }


Teclas usadas na implementação atual da biblioteca

Ação
 Tecla Do inglês
 Move o timeframe para cima  U  Up
 Move o timeframe para baixo  D  Down
 Altera o nível Z (acima ou abaixo dos objetos)  Z  Z order
 Traça uma linha de tendência inclinada atravessando dois pontos extremos unidirecionais mais próximos do mouse  T  Trend line
 Muda para o modo de raio para novas retas
 R key  Ray
 Traça uma linha vertical simples
 I(i) [Only visual  vertical]
 Traça uma linha horizontal simples
 H  Horizontal
 Traça um garfo de Andrews
 P  Pitchfork
 Traça um leque de Fibonacci (VFun)
 F key  Fun
 Traça um nível horizontal curto
 S  Short
 Traça um nível horizontal alongado
 L key  Long
 Traça uma linha vertical simples com marcas de níveis
 V  Vertical
 Traça fios
 X  [Only visual  cross]
 Traça uma linha atravessando vértices arbitrários
 Q  [No conformity... "L" and "T" is not free]
 Desenha um grupo de retângulos
 B  Box


Conclusão

Espero que você tenha achado este material útil. No entanto, ficarei feliz em receber críticas construtivas nos comentários.

Agora planejo tornar possível traçar linhas não apenas por vértices rígidos, mas também por tangentes.

Além disso, desejo prestar atenção aos canais. Por enquanto, estou pensando apenas em equidistantes, mas se alguém nos comentários ou em uma mensagem particular expressar o desejo de desenhar algo mais que esteja de acordo com os princípios da biblioteca, estou disposto a considerar essas sugestões.

A longo prazo, veremos como salvar as configurações num arquivo (em vez de, ou junto com, as configurações do indicador usando variáveis input) e adicionar uma interface gráfica para alterar as configurações na hora.


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

Arquivos anexados |
Como se tornar um bom programador (Parte 6): 9 hábitos para desenvolver de maneira produtiva Como se tornar um bom programador (Parte 6): 9 hábitos para desenvolver de maneira produtiva
O resultado final do projeto não tem a ver apenas com a escrita de código. A minha experiência me ensinou a identificar certos hábitos que ajudam a melhorar a produtividade na hora de desenvolver. Mais tarde, falaremos sobre alguns deles neste artigo. Este artigo é uma leitura obrigatória destinada a todos que desejam melhorar suas habilidades na escrita de algoritmos complexos.
Receitas MQL5: Calendário Econômico Receitas MQL5: Calendário Econômico
Este artigo se trata das funcionalidades programáticas usadas ao trabalhar usando o calendário econômico. Para implementá-las, criaremos uma classe para facilitar o acesso às propriedades do calendário e receber valores de eventos. Como exemplo prático, programaremos um indicador que utiliza dados da CFTC sobre as posições líquidas de especuladores.
Trabalhando com o tempo (Parte 2): funções Trabalhando com o tempo (Parte 2): funções
Vamos aprender a reconhecer automaticamente as diferenças de tempo junto à corretora, bem como o Tempo Médio de Greenwich. Em vez de preguntar à corretora, que provavelmente dará uma resposta imprecisa (e quem quer explicar onde está o horário de negociação?), seremos nós mesmos a ver a que horas ela recebe as cotações nas semanas em que os fusos horários são trocados. Mas é claro que não vamos fazer isso manualmente, deixaremos o software fazer o trabalho por nós.
Quase-construtor para criar um Expert Advisor Quase-construtor para criar um Expert Advisor
Disponibilizo meu próprio conjunto de funções de negociação na forma de um Expert Advisor pronto para uso. O método agora proposto permite gerar diversas estratégias de negociação simplesmente adicionando indicadores e mudando os parâmetros de entrada.