Como, na MetaTrader 5, desenvolver e depurar rapidamente sua estratégia de negociação

MetaQuotes | 10 outubro, 2016

"Não se deve acreditar em ninguém, no entanto em mim, sim" (с) Depurador

Os sistemas automáticos de scalping são considerados não só o auge do trading algorítmico, mas também os mais difíceis na escrita do código. Neste artigo, nós mostraremos como -usando os recursos embutidos de depuração e teste visual- construir estratégias baseadas na análise de ticks entrantes. O desenvolvimento de regras de entrada e saída muitas vezes exige anos de negociação manual. Mas com a MetaTrader 5 você pode rapidamente verificar qualquer estratégia semelhante no histórico real.



Ideia de negociação em ticks

Primeiro e acima de tudo, precisamos criar um indicador que construa gráficos de ticks, ou seja, gráficos onde seja possível ver cada alteração do preço. Na biblioteca https://www.mql5.com/pt/code/89, você pode encontrar um dos primeiros indicadores desse tipo. Ao contrário dos convencionais, nos gráficos de ticks, ao receber um novo tick, é necessário deslocar o gráfico inteiro para trás.


Tomaremos, como base da ideia verificável, uma série de alterações do preço entre dois ticks sucessivos, vai ser aproximadamente esta sequência de pontos:

+1, 0, +2, -1, 0, +1, -2, -1, +1, -5, -1, +1, 0, -1, +1, 0, +2, -1, +1, +6, -1, +1,...

A lei de distribuição normal estipula que 99% das alterações entre dois ticks enquadra dentro dos limites de 3 sigmas. Tentaremos, em tempo real, calcular o desvio padrão em cada tick e marcar as alterações bruscas do preço usando ícones vermelhos e azuis. Assim, tentaremos, também, selecionar visualmente uma estratégia para usar essas alterações bruscas, quer dizer, negociar na direção da alteração ou usar "retorno para a média." Como você pode ver, a ideia é bastante simples, e certamente a maioria dos fãs da matemática passaram por esse caminho.


Criação do indicador de ticks

No MetaEditor, executarmos o Assistente MQL, definimos o nome  e dois parâmetros de entrada:

A seguir, marcamos "Indicador em uma janela separada" e especificamos 2 construções gráficas que irão exibir informações em uma subjanela: a linha para ticks e as setas coloridas para sinais sobre o surgimento de alterações bruscas de preço.


Introduzimos ao projeto obtido as alterações marcadas em amarelo

//+------------------------------------------------------------------+
//|                                              TickSpikeHunter.mq5 |
//|                        Copyright 2016, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 3
#property indicator_plots   2
//--- plot TickPrice
#property indicator_label1  "TickPrice"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrGreen
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- plot Signal
#property indicator_label2  "Signal"
#property indicator_type2   DRAW_COLOR_ARROW
#property indicator_color2  clrRed,clrBlue,C'0,0,0',C'0,0,0',C'0,0,0',C'0,0,0',C'0,0,0',C'0,0,0'
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1
//--- input parameters
input int      ticks=50;         // número de ticks nos cálculos
input double   gap=3.0;          // largura do canal em sigmas
//--- indicator buffers
double         TickPriceBuffer[];
double         SignalBuffer[];
double         SignalColors[];
//--- contador das alterações do preço
int ticks_counter;
//--- primeira chamada do indicador
bool first;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,TickPriceBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,SignalBuffer,INDICATOR_DATA);
   SetIndexBuffer(2,SignalColors,INDICATOR_COLOR_INDEX);
//--- indicamos os valores vazios que se devem ignorar ao desenhar  
   PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,0);
   PlotIndexSetDouble(1,PLOT_EMPTY_VALUE,0);
//--- exibiremos os sinais na forma desse ícone
   PlotIndexSetInteger(1,PLOT_ARROW,159);
//--- inicialização de variáveis globais
   ticks_counter=0;
   first=true;
//--- inicialização bem-sucedida do programa
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

Agora resta adicionar o código ao manipulador predefinido de ticks entrantes OnCalculate(). Na primeira chamada da função, zeramos os valores nos buffers de indicador, bem como instalamos para eles o sinal TimeSeries, desse modo sua indexação será de direita para a esquerda. Isso permite acessar o último valor do buffer de indicador zero, isto é, no TickPriceBuffer[0] será armazenado o valor do último tick.

Além disso, introduziremos processamento primário à função separada ApplyTick():

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//--- na primeira chamada, zeramos os buffers de indicador e definimos o sinal de séries
   if(first)
     {
      ZeroMemory(TickPriceBuffer);
      ZeroMemory(SignalBuffer);
      ZeroMemory(SignalColors);
      //--- as matrizes de séries vão de trás para frente para conveniência
      ArraySetAsSeries(SignalBuffer,true);
      ArraySetAsSeries(TickPriceBuffer,true);
      ArraySetAsSeries(SignalColors,true);
      first=false;
     }
//--- tomamos como preços os valores atuais Close
   double lastprice=close[rates_total-1];
//--- contamos ticks
   ticks_counter++;
   ApplyTick(lastprice); // realizamos o cálculo e deslocamento nos buffers   
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| adota o tick para cálculo                                     |
//+------------------------------------------------------------------+
void ApplyTick(double price)
  {
   int size=ArraySize(TickPriceBuffer);
   ArrayCopy(TickPriceBuffer,TickPriceBuffer,1,0,size-1);
   ArrayCopy(SignalBuffer,SignalBuffer,1,0,size-1);
   ArrayCopy(SignalColors,SignalColors,1,0,size-1);
//--- registramos o último valor de preço
   TickPriceBuffer[0]=price;
//---
  }

Por agora, a função ApplyTick() só executa ações simples: desloca todos os valores de buffer para uma posição profunda no histórico e registra no TickPriceBuffer[0] o último tick. Executamos o indicador sob depuração e observamos durante algum tempo.

Vemos que o preço Bid segundo o qual é construído o Close da vela atual muitas vezes permanece inalterado e é por isso que o gráfico é desenhado por pedaços "planaltos". Como o que queremos é apenas conseguir uma forma de "serra", vamos corrigir um pouco o código, dessa maneira será mais compreensível tudo.

//--- calculamos apenas se o preço ficar alterado
   if(lastprice!=TickPriceBuffer[0])
     {
      ticks_counter++;      // contamos os ticks
      ApplyTick(lastprice); // realizamos o cálculo e deslocamento nos buffers
     }

Assim, já temos criado a primeira versão do indicador, agora não teremos incrementos de preço zerados.


Adicionamos um buffer secundário e o cálculo do desvio padrão

Para calcular os desvios, precisamos uma matriz adicional que irá armazenar o incremento de preço em cada tick. Como matriz, adicionamos outro buffer de indicador e acrescentamos o código apropriado nos lugares certos:

#property indicator_separate_window
#property indicator_buffers 4
#property indicator_plots   2
...
//--- indicator buffers
double         TickPriceBuffer[];
double         SignalBuffer[];
double         DeltaTickBuffer[];
double         ColorsBuffers[];
...
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,TickPriceBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,SignalBuffer,INDICATOR_DATA);
   SetIndexBuffer(2,SignalColors,INDICATOR_COLOR_INDEX);
   SetIndexBuffer(3,DeltaTickBuffer,INDICATOR_CALCULATIONS);
...
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const ...)

//--- na primeira chamada, zeramos os buffers de indicador e definimos o sinal de séries
   if(first)
     {
      ZeroMemory(TickPriceBuffer);
      ZeroMemory(SignalBuffer);
      ZeroMemory(SignalColors);
     ZeroMemory(DeltaTickBuffer);
      //--- as matrizes de séries vão de trás para frente para conveniência
      ArraySetAsSeries(TickPriceBuffer,true);
      ArraySetAsSeries(SignalBuffer,true);
      ArraySetAsSeries(SignalColors,true);
      ArraySetAsSeries(DeltaTickBuffer,true);
      first=false;
     }
...
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| adota o tick para cálculo                                     |
//+------------------------------------------------------------------+
void ApplyTick(double price)
  {
   int size=ArraySize(TickPriceBuffer);
   ArrayCopy(TickPriceBuffer,TickPriceBuffer,1,0,size-1);
   ArrayCopy(SignalBuffer,SignalBuffer,1,0,size-1);
   ArrayCopy(SignalColors,SignalColors,1,0,size-1);  
   ArrayCopy(DeltaTickBuffer,DeltaTickBuffer,1,0,size-1);
//--- registramos o último valor de preço
   TickPriceBuffer[0]=price;
//--- calculamos a diferença com o valor anterior
   DeltaTickBuffer[0]=TickPriceBuffer[0]-TickPriceBuffer[1];
//--- obtemos desvio padrão desvio
   double stddev=getStdDev(ticks);  

Agora estamos prontos para calcular o desvio padrão. Primeiro, escrevemos a função getStdDev() que faz com que todos os cálculos sejam feitos "de cor", verificando todos os elementos de matriz e levando em conta apenas os necessários.

//+------------------------------------------------------------------+
//| calcula o desvio padrão "de cor"                            |
//+------------------------------------------------------------------+
double getStdDev(int number)
  {
   double summ=0,sum2=0,average,stddev;
//--- contamos a soma das alterações e calculamos o retorno
   for(int i=0;i<ticks;i++)
      summ+=DeltaTickBuffer[i];
   average=summ/ticks;
//--- Agora calculamos o desvio padrão
   sum2=0;
   for(int i=0;i<ticks;i++)
      sum2+=(DeltaTickBuffer[i]-average)*(DeltaTickBuffer[i]-average);
   stddev=MathSqrt(sum2/(number-1));
   return (stddev);
  }

Depois, terminamos de escrever o bloco responsável pela colocação dos sinais no gráfico de ticks, isto é, pôr círculos vermelhos e azuis.

//+------------------------------------------------------------------+
//| adota o tick para cálculo                                     |
//+------------------------------------------------------------------+
void ApplyTick(double price)
  {
   int size=ArraySize(TickPriceBuffer);
   ArrayCopy(TickPriceBuffer,TickPriceBuffer,1,0,size-1);
   ArrayCopy(SignalBuffer,SignalBuffer,1,0,size-1);
   ArrayCopy(SignalColors,SignalColors,1,0,size-1);
   ArrayCopy(DeltaTickBuffer,DeltaTickBuffer,1,0,size-1);   
//--- registramos o último valor de preço
   TickPriceBuffer[0]=price;
//--- calculamos a diferença com o valor anterior
   DeltaTickBuffer[0]=TickPriceBuffer[0]-TickPriceBuffer[1];   
//--- obtemos desvio padrão desvio
   double stddev=getStdDev(ticks);   
//--- se a alteração do preço ultrapassar o limite estabelecido
   if(MathAbs(DeltaTickBuffer[0])>gap*stddev) // sinal é exibido no primeiro tick, deixamos como "feature"
     {
      SignalBuffer[0]=price;     // colocamos o ponto
      string col="Red";          // por padrão, ponto de cor vermelha
      if(DeltaTickBuffer[0]>0)   // o preço cresceu bruscamente
        {
         SignalColors[0]=1;      // então, ponto de cor azul
         col="Blue";             // memorizamos para registro no log
        }
      else                       // o preço recuou bruscamente
      SignalColors[0]=0;         // ponto de cor vermelha
      //--- imprimimos o registro no diário (log) de Expert Advisors
      PrintFormat("tick=%G change=%.1f pts, trigger=%.3f pts,  stddev=%.3f pts %s",
                  TickPriceBuffer[0],DeltaTickBuffer[0]/_Point,gap*stddev/_Point,stddev/_Point,col);
     }
   else SignalBuffer[0]=0;       // sem sinal      
//---
  }

Pressionamos o botão F5 (Início da depuração/continuação da execução) e observamos no terminal MetaTrader 5 como funciona nosso indicador.

Agora é hora de fazer a depuração de código, ela permite identificar erros e agilizar o trabalho do programa.


Criação de perfil do código para acelerar o trabalho

Para programas que funcionam em modo de tempo real, a velocidade de execução é de importância crítica. O ambiente de desenvolvimento MetaEditor permite avaliar rapidamente o tempo gasto para executar certas partes do programa. Para fazer isso, é preciso iniciar a criação de perfil no código e deixar o programa funcionar por algum tempo. Para a criação de perfil bastará um minuto.


Como você pode ver, a maior parte do tempo (95.21%) foi para o processamento da função ApplyTick(), que foi chamada 41 vezes a partir da função OnCalculate(). A própria função OnCalculate() foi chamada 143 vezes, mas, só em 41 dos casos, o preço no tick entrante diferiu do preço do anterior. Além disso, na própria função ApplyTick() a maior parte do tempo foi gasta nas chamadas da função ArrayCopy() que executam apenas ações adicionais -graças às quais eu inventei este indicador- e não realizam cálculos. O cálculo do desvio padrão na cadeia 111 do código levou apenas 0,57% do tempo total de execução do programa. 

Vamos tentar reduzir as sobrecargas, para isso, tentamos copiar apenas os 200 últimos elementos das matrizes. Na verdade, no gráfico será suficiente ver os 200 últimos valores, além disso, o número de ticks numa sessão de negociação pode alcançar dezenas ou centenas de milhares. Não há nenhuma necessidade de vê-los todos. Portanto, introduzimos o parâmetro de entrada shift=200, ele especifica o número de valores deslocados. Adicione, ao código, as seguintes cadeias de caracteres destacadas em amarelo:

//--- input parameters
input int      ticks=50;         // número de ticks nos cálculos
input int      shift=200;        // número de valores deslocados
input double   gap=3.0;          // largura do canal em sigmas
...
void ApplyTick(double price)
  {
//--- quantos elementos deslocamos nos buffers de indicador em cada tick
   int move=ArraySize(TickPriceBuffer)-1;
   if(shift!=0) move=shift;
   ArrayCopy(TickPriceBuffer,TickPriceBuffer,1,0,move);
   ArrayCopy(SignalBuffer,SignalBuffer,1,0,move);
   ArrayCopy(SignalColors,SignalColors,1,0,move);
   ArrayCopy(DeltaTickBuffer,DeltaTickBuffer,1,0,move);


Executamos novamente a criação de perfil e vemos o novo resultado: o tempo de cópia de matrizes caiu em centenas ou milhares de vezes, agora o tempo principal é ocupado pela chamada StdDev(), ela responde pelo cálculo do desvio padrão.


Assim, temos acelerado a função ApplyTick(), o que nos dará uma economia substancial ao otimizar a estratégia e ao trabalhar em modo de tempo real. Veja só, os recursos computacionais nunca estão de mais.


Otimização analítica do código

Às vezes até um código otimamente escrito pode ser obrigado a trabalhar ainda mais rápido. Nesse caso, se nós reescrevermos ligeiramente a fórmula, o cálculo do desvio padrão pode ser acelerado. 


Assim, podemos simplesmente calcular o quadrado da soma e a soma dos quadrados dos incrementos de preço: isso nos permitirá realizar operações menos matemáticas em cada tick. Em cada tick, simplesmente removemos o item de menu suspenso na matriz e adicionamos o elemento de entrada da matriz às variáveis que contêm as somas.

Criamos a nova função getStdDevOptimized(), na qual usamos o método -que já conhecemos- de deslocamento dos valores da matriz dentro de si mesma.

//+------------------------------------------------------------------+
//| Calcula o desvio padrão com base nas fórmulas                     |
//+------------------------------------------------------------------+
double getStdDevOptimized(int number)
  {
//---
   static double X2[],X[],X2sum=0,Xsum=0;
   static bool firstcall=true;
//--- primeira chamada
   if(firstcall)
     {
      //--- definimos o tamanho das matrizes dinâmicas 1 vez maior do que o número de ticks
      ArrayResize(X2,ticks+1);
      ArrayResize(X,ticks+1);
      //--- garantimo-nos um valor de zero no início do cálculo
      ZeroMemory(X2);
      ZeroMemory(X);

      firstcall=false;
     }
//--- deslocamos as matizes
   ArrayCopy(X,X,1,0,ticks);
   ArrayCopy(X2,X2,1,0,ticks);
//--- calculamos os novos valores de entrada das somas
   X[0]=DeltaTickBuffer[0];
   X2[0]=DeltaTickBuffer[0]*DeltaTickBuffer[0];
//--- calculamos as novas somas
   Xsum=Xsum+X[0]-X[ticks];
   X2sum=X2sum+X2[0]-X2[ticks];
//--- quadrado do desvio padrão
   double S2=(1.0/(ticks-1))*(X2sum-Xsum*Xsum/ticks);
//--- contamos a soma de ticks e calculamos o retorno
   double stddev=MathSqrt(S2);
//---
   return (stddev);
  } 

Adicionamos a função ApplyTick(), o cálculo do desvio padrão usando o segundo método através da função getStdDevOptimized() e executamos novamente a criação de perfil.

//--- calculamos a diferença com o valor anterior
   DeltaTickBuffer[0]=TickPriceBuffer[0]-TickPriceBuffer[1];
//--- obtemos desvio padrão desvio
   double stddev=getStdDev(ticks);
   double std_opt=getStdDevOptimized(ticks);

Resultado da execução:


Podemos ver que a nova função getStdDevOptimized() requer duas vezes menos tempo, 4.56%, do que qualquer engano na conta na getStdDev(), 9.54%. Ela é mesmo realizada mais rápido do que a função embutida PrintFormat() usada 4.74% do tempo de trabalho do programa. Dessa forma, o uso do método ideal de cálculo dá um desempenho ainda melhor. Recomendamos também que você confira o artigo 3 método de aceleração de indicadores segundo o exemplo de regressão linear.

A propósito da chamada de funções padrão, nós obtemos o preço a partir do TimeSeries close[] que se constrói de acordo com os preços Bid. Existem duas maneiras de conseguir esse preço usando as funções SymbolInfoDouble() e SymbolInfoTick(). Adicionamos essas chamadas no código e fazemos novamente a criação de perfil.


Como você pode ver, aqui, também, há uma diferença na velocidade. E isso é compreensível porque a leitura de preço pronto a partir de close[] não requer gastos de tempo comparada com a chamada de funções genéricas.

Depuração com base em ticks reais no testador

Ao escrever indicadores e robôs de negociação, é impossível prever todas as possíveis situações que podem acontecer durante o trabalho on-line. Felizmente, o MetaEditor permite realizar a depuração com base nos dados históricos. Basta iniciar a depuração no modo de teste visual e você poderá verificar seu programa no intervalo definido de histórico. Você conseguirá acelerar, parar e rolar para a data desejada de teste.

Importante: na janela de Depuração, defina o modo de simulação "Cada tick baseado em ticks reais". Isto permitirá o uso -para depurar- de cotações reais registradas que são armazenadas pelo servidor de negociação. Eles são automaticamente carregados em seu computador ao iniciar pela primeira vez seu computador.


Se esses parâmetros não forem especificados no MetaEditor, serão usadas -no modo de teste- as configurações atuais do testador. Indique nelas o modo "Cada tick baseado em ticks reais."



Vemos que no gráfico de ticks aparecem estranhas lacunas. Isso significa que no algoritmo foi admitido algum erro. Não se sabe quanto tempo levaria para sua manifestação no teste em tempo real. Neste caso, de acordo com as conclusões no Diário de teste visual, podemos ver que as estranhas lacunas ocorrerem no momento de uma nova barra. Exatamente! Esquecemos que, ao mudar para uma nova barra, o tamanho dos buffers de indicador é automaticamente incrementado 1. Corrigimos o código:

void ApplyTick(double price)
  {
//--- vamos lembrar que o tamanho da matriz TickPriceBuffer é igual ao número de barras no gráfico
   static int prev_size=0;
   int size=ArraySize(TickPriceBuffer);
//--- se o tamanho dos buffers de indicador não se alterarem, deslocamos todos os itens uma posição de volta
   if(size==prev_size)
     {
      //--- quantos elementos deslocamos nos buffers de indicador em cada tick
      int move=ArraySize(TickPriceBuffer)-1;
      if(shift!=0) move=shift;
      ArrayCopy(TickPriceBuffer,TickPriceBuffer,1,0,move);
      ArrayCopy(SignalBuffer,SignalBuffer,1,0,move);
      ArrayCopy(SignalColors,SignalColors,1,0,move);
      ArrayCopy(DeltaTickBuffer,DeltaTickBuffer,1,0,move);
     }
   prev_size=size;
//--- registramos o último valor de preço
   TickPriceBuffer[0]=price;
//--- calculamos a diferença com o valor anterior

Executamos o teste visual e colocamos um ponto de interrupção para capturar o momento de abertura de uma nova barra. Adicionamos os valores observados e nos certificamos de que tudo fosse feito corretamente: aumento 1 do número de barras no gráfico, volume de ticks da barra atual igual a 1, ele como primeiro tick da barra nova.


Realizamos a otimização de código, corrigimos bugs, medimos o tempo de execução das diferentes funções, agora o indicador está pronto para uso. Você pode executar o teste visual e observar o que acontece depois do aparecimento dos sinais no gráfico de ticks. Será que é possível melhorar outra coisa no código do indicador? Um perfeccionista de codificação dirá sim! Ainda não tentamos usar o buffer circular para acelerar o trabalho. Os interessados podem verificar por si próprios se isto melhora o desempenho.


Metaeditor, um laboratório acabado para desenvolver estratégias de negociação

Para escrever o sistema de negociação automatizado é importante ter não só um ambiente de trabalho confortável e uma poderosa linguagem de programação, mas também ferramentas adicionais para depuração e calibração do programa.  Neste artigo nós mostramos como:

  1. criar rapidamente um gráfico de ticks na primeira aproximação;
  2. usar a depuração em tempo real no gráfico, clicando F5;
  3. iniciar a criação de perfil para identificar locais ineficientes no código;
  4. realizar uma depuração rápida com base em dados históricos no modo de teste visual;
  5. visualizar os valores das variáveis desejadas durante a depuração.

O desenvolvimento do indicador -que mostra os sinais de negociação- muitas vezes é um primeiro passo necessário para a escrita do robô de negociação. A visualização ajuda a desenvolver regras de negociação ou rejeitar ideias, mesmo antes de começar o projeto.

Use todos os recursos do ambiente de desenvolvimento MetaEditor para criar robôs de negociação eficientes!

Artigos relacionados:

  1. MQL5: crie o seu próprio indicador
  2. Criando indicadores de ponto no MQL4
  3. Os princípios do cálculo econômico de indicadores
  4. Série de preço médio para cálculos intermediários sem utilizar buffers adicionais
  5. Depuração dos programas da MQL5