Visualizando a otimização de uma estratégia de negociação na MetaTrader 5

Anatoli Kazharski | 4 maio, 2018


Conteúdo

Introdução

Ao desenvolver algoritmos de negociação, é útil visualizar os resultados dos testes e otimizar os parâmetros. No entanto, um único gráfico na aba Gráfico da Otimização pode ser insuficiente para avaliar a eficiência de uma estratégia de negociação. Seria melhor visualizar as curvas de saldo de vários testes simultaneamente, podendo analisá-los mesmo após a otimização. Nós já examinamos tal aplicação no artigo "Visualizar uma estratégia no tester do MetaTrader 5". No entanto, muitas novas oportunidades surgiram desde então. Portanto, agora é possível implementar um aplicativo semelhante, mas muito mais poderoso.

O artigo implementa um aplicativo MQL com uma interface gráfica para a visualização estendida do processo de otimização. A interface gráfica utiliza a última versão da biblioteca EasyAndFast. Muitos usuários da comunidade MQL podem questionar-se sobre a necessidade de utilizar interfaces gráficas em aplicativos MQL. Este artigo mostra seus potenciais usos. Ele também pode ser útil para aqueles que aplicam a biblioteca em seu trabalho.

Desenvolvimento da interface gráfica

Aqui eu vou descrever brevemente o desenvolvimento da interface gráfica. Se você já dominou a biblioteca EasyAndFast, você será capaz de entender rapidamente como usá-la e avaliar como é fácil desenvolver uma interface gráfica para seu aplicativo MQL.

Primeiro, vamos descrever a estrutura geral do aplicativo desenvolvido. O arquivo Program.mqh deve conter a classe de aplicação CProgram. Esta classe base deve ser conectada ao mecanismo da biblioteca gráfica.

//+------------------------------------------------------------------+
//|                                                      Program.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- Classe da biblioteca para a criação da interface gráfica
#include <EasyAndFastGUI\WndEvents.mqh>
//+------------------------------------------------------------------+
//| Classe para o desenvolvimento do aplicativo                      |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
  };

A biblioteca EasyAndFast é exibida em um único bloco (Library GUI) para não confundir a imagem. Você pode visualizá-la na íntegra na página da biblioteca

 Fig. 1. Inclusão da biblioteca para criar a GUI

Fig. 1. Inclusão da biblioteca para criar a GUI

Métodos semelhantes devem ser criados na classe CProgram para se conectar com as principais funções do programa em MQL. Nós vamos precisar dos métodos da categoria OnTesterXXX() para poder trabalhar com os quadros.

class CProgram : public CWndEvents
  {
public:
   //--- Inicialização/desinicialização
   bool              OnInitEvent(void);
   void              OnDeinitEvent(const int reason);
   //--- Manipulador de eventos "Novo tick"
   void              OnTickEvent(void);
   //--- Manipulador de eventos de negociação
   void              OnTradeEvent(void);
   //--- Timer
   void              OnTimerEvent(void);
   //--- Simulador
   double            OnTesterEvent(void);
   void              OnTesterPassEvent(void);
   void              OnTesterInitEvent(void);
   void              OnTesterDeinitEvent(void);
  };

Neste caso, os métodos devem ser chamados da seguinte maneira no arquivo principal do aplicativo:

//--- Classe de inclusão do aplicativo
#include "Program.mqh"
CProgram program;
//+------------------------------------------------------------------+
//| Função de inicialização do Expert                                |
//+------------------------------------------------------------------+
int OnInit(void)
  {
//--- Inicializa o programa
   if(!program.OnInitEvent())
     {
      ::Print(__FUNCTION__," > Failed to initialize!");
      return(INIT_FAILED);
     }  
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Função de desinicialização do Expert                             |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) { program.OnDeinitEvent(reason); }
//+------------------------------------------------------------------+
//| Função tick do Expert                                            |
//+------------------------------------------------------------------+
void OnTick(void) { program.OnTickEvent(); }
//+------------------------------------------------------------------+
//| Funçao timer                                                     |
//+------------------------------------------------------------------+
void OnTimer(void) { program.OnTimerEvent(); }
//+------------------------------------------------------------------+
//| Função ChartEvent                                                |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
   { program.ChartEvent(id,lparam,dparam,sparam); }
//+------------------------------------------------------------------+
//| Função Tester                                                    |
//+------------------------------------------------------------------+
double OnTester(void) { return(program.OnTesterEvent()); }
//+------------------------------------------------------------------+
//| Função TesterInit                                                |
//+------------------------------------------------------------------+
void OnTesterInit(void) { program.OnTesterInitEvent(); }
//+------------------------------------------------------------------+
//| Função TesterPass                                                |
//+------------------------------------------------------------------+
void OnTesterPass(void) { program.OnTesterPassEvent(); }
//+------------------------------------------------------------------+
//| Função TesterDeinit                                              |
//+------------------------------------------------------------------+
void OnTesterDeinit(void) { program.OnTesterDeinitEvent(); }
//+------------------------------------------------------------------+

Assim, a estrutura da aplicação está pronta para o desenvolvimento da interface gráfica. O trabalho principal é realizado na classe CProgram. Todos os arquivos necessários para o seu funcionamento estão incluídos em Program.mqh.

Agora vamos definir o conteúdo da interface gráfica. Lista todos os elementos a serem criados.

  • Formulário para controles.
  • Campo para especificar a quantidade dos saldos a serem exibidos no gráfico.
  • Campo para ajustar a velocidade de exibição dos novos resultados da otimização.
  • Botão para iniciar uma nova exibição.
  • Tabela de estatísticas dos resultados.
  • Tabela para exibir os parâmetros externos do EA.
  • Gráfico da curva de saldo.
  • Gráfico de resultados da otimização.
  • Barra de estado para exibir o resumo das informações adicionais.
  • Barra de progresso exibindo a porcentagem dos resultados exibidos do valor total durante o novo deslocamento.

Abaixo estão as declarações de instâncias de classe dos elementos de controle e seus métodos de criação (veja o código abaixo). Os códigos dos métodos são colocados em um arquivo separado — CreateFrameModeGUI.mqh, que está associado com o arquivo de classe CProgram. À medida que o código do aplicativo desenvolvido cresce, o método de distribuição por arquivos individuais torna-se mais relevante, facilitando a navegação no projeto.

class CProgram : public CWndEvents
  {
private:
   //--- Janela
   CWindow           m_window1;
   //--- Barra de status
   CStatusBar        m_status_bar;
   //--- Campos de entrada
   CTextEdit         m_curves_total;
   CTextEdit         m_sleep_ms;
   //--- Botões
   CButton           m_reply_frames;
   //--- Tabelas
   CTable            m_table_stat;
   CTable            m_table_param;
   //--- Gráficos
   CGraph            m_graph1;
   CGraph            m_graph2;
   //--- Barra de progresso
   CProgressBar      m_progress_bar;
   //---
public:
   //--- Cria a interface gráfica para trabalhar com os quadros no modo de otimização
   bool              CreateFrameModeGUI(void);
   //---
private:
   //--- Formulário
   bool              CreateWindow(const string text);
   //--- Barra de status
   bool              CreateStatusBar(const int x_gap,const int y_gap);
   //--- Tabelas
   bool              CreateTableStat(const int x_gap,const int y_gap);
   bool              CreateTableParam(const int x_gap,const int y_gap);
   //--- Campos de entrada
   bool              CreateCurvesTotal(const int x_gap,const int y_gap,const string text);
   bool              CreateSleep(const int x_gap,const int y_gap,const string text);
   //--- Botões
   bool              CreateReplyFrames(const int x_gap,const int y_gap,const string text);
   //--- Gráficos
   bool              CreateGraph1(const int x_gap,const int y_gap);
   bool              CreateGraph2(const int x_gap,const int y_gap);
   //--- Barra de progresso
   bool              CreateProgressBar(const int x_gap,const int y_gap,const string text);
  };
//+------------------------------------------------------------------+
//| Métodos para a criação dos elementos de controle                 |
//+------------------------------------------------------------------+
#include "CreateFrameModeGUI.mqh"
//+------------------------------------------------------------------+

Vamos habilitar a inclusão do arquivo a ser conectado na CreateFrameModeGUI.mqh também. Nós vamos mostrar aqui apenas um método principal para a criação da interface gráfica do aplicativo de exemplo:

//+------------------------------------------------------------------+
//|                                           CreateFrameModeGUI.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Program.mqh"
//+------------------------------------------------------------------+
//| Crie a interface gráfica                                         |
//| para analisar os resultados da otimização e trabalhar com os quadros|
//+------------------------------------------------------------------+
bool CProgram::CreateFrameModeGUI(void)
  {
//--- Cria a interface apenas no modo para trabalhar com os quadros da otimização
   if(!::MQLInfoInteger(MQL_FRAME_MODE))
      return(false);
//--- Cria o formulário para os elementos de controle
   if(!CreateWindow("Frame mode"))
      return(false);
//--- Cria os elementos de controle
   if(!CreateStatusBar(1,23))
      return(false);
   if(!CreateCurvesTotal(7,25,"Curves total:"))
      return(false);
   if(!CreateSleep(145,25,"Sleep:"))
      return(false);
   if(!CreateReplyFrames(255,25,"Replay frames"))
      return(false);
   if(!CreateTableStat(2,50))
      return(false);
   if(!CreateTableParam(2,212))
      return(false);
   if(!CreateGraph1(200,50))
      return(false);
   if(!CreateGraph2(200,159))
      return(false);
//--- Barra de progresso
   if(!CreateProgressBar(2,3,"Processing..."))
      return(false);
//--- Criação completa da GUI
   CWndEvents::CompletedGUI();
   return(true);
  }
...

A conexão entre os arquivos pertencentes a uma classe é mostrada como uma seta amarela de dois lados:

 Fig. 2. Dividindo o projeto em vários arquivos

Fig. 2. Dividindo o projeto em vários arquivos



Desenvolvimento da classe para trabalhar com os dados de quadros

Vamos escrever uma classe separada CFrameGenerator para trabalhar com os quadros. A classe deve estar contida em FrameGenerator.mqh que deve ser incluído em Program.mqh. Como exemplo, eu demonstrarei duas opções para receber esses quadros para exibição nos elementos de interface gráfica. 

  • No primeiro caso, para exibir os quadros como objetos gráficos, os ponteiros para esses objetos são passados ​​para os métodos de classe.
  • No segundo caso, nós recebemos os dados dos quadros para preencher as tabelas de outras categorias usando os métodos especiais. 

Você decide quais dessas opções devem ser deixadas como a principal.

A biblioteca EasyAndFast aplica a classe CGraphic da biblioteca padrão para visualizar os dados. Vamos incluí-lo em FrameGenerator.mqh para acessar os seus métodos.

//+------------------------------------------------------------------+
//|                                               FrameGenerator.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include <Graphics\Graphic.mqh>
//+------------------------------------------------------------------+
//| Classe para receber os resultados da otimização                  |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
  };

O arranjo do programa agora se parece com o seguinte:

 Fig. 3. Conectando-se a projetos de classe para o trabalho

Fig. 3. Conectando aos projetos de classe para o trabalho

Agora vamos ver como a classe CFrameGenerator é organizada. Ele também precisa de métodos para processar os eventos do testador de estratégia (consulte o código abaixo). Eles devem ser chamados nos métodos de classe análogos ao programa que nós desenvolvemos — CProgram. Os ponteiros dos objetos gráficos são passados para o método CFrameGenerator::OnTesterInitEvent() para representar o processo de otimização atual. 

  • O primeiro gráfico (graph_balance) exibe o número especificado da última série dos saldos dos resultados da otimização.
  • O segundo gráfico (graph_result) exibe os resultados gerais da otimização.
class CFrameGenerator
  {
private:
   //--- Ponteiros do gráfico para a visualização dos dados
   CGraphic         *m_graph_balance;
   CGraphic         *m_graph_results;
   //---
public:
   //--- Manipuladores de eventos do testador de estratégia
   void              OnTesterEvent(const double on_tester_value);
   void              OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_result);
   void              OnTesterDeinitEvent(void);
   bool              OnTesterPassEvent(void);
  };
//+------------------------------------------------------------------+
//| Deve ser chamado no manipulador da OnTesterInit()                |
//+------------------------------------------------------------------+
void CFrameGenerator::OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_results)
  {
   m_graph_balance =graph_balance;
   m_graph_results =graph_results;
  }

Nos dois gráficos, os resultados positivos são exibidos em verde, enquanto que os negativos são exibidos em vermelho.

Dentro do método CFrameGenerator::OnTesterEvent(), nós recebemos o saldo do resultado do teste e os parâmetros estatísticos. Esses dados são passados ​​para um quadro usando os métodos CFrameGenerator::GetBalanceData() e CFrameGenerator::GetStatData(). O método CFrameGenerator::GetBalanceData() recebe todo o histórico de testes e resume tudo negociações in-/inout. O resultado obtido é salvo passo a passo no array m_balance[]. Por sua vez, este array é um membro da classe CFrameGenerator.

O array dinâmico a ser enviado para um quadro é passado para o método CFrameGenerator::GetStatData(). Seu tamanho é para corresponder ao tamanho do array para o saldo resultado, que foi recebido anteriormente. Além disso, vários elementos aos quais nós recebemos os parâmetros estatísticos são adicionados.

//--- Número de parâmetros estatísticos
#define STAT_TOTAL 7
//+------------------------------------------------------------------+
//| Classe para trabalhar com os resultados da otimização            |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   //--- Saldo do resultado
   double            m_balance[];
   //---
private:
   //--- Recebe os dados do saldo
   int               GetBalanceData(void);
   //--- Recebe os dados estatísticos
   void              GetStatData(double &dst_array[],double on_tester_value);
  };
//+------------------------------------------------------------------+
//| Obtém os dados do saldo                                          |
//+------------------------------------------------------------------+
int CFrameGenerator::GetBalanceData(void)
  {
   int    data_count      =0;
   double balance_current =0;
//--- Solicita todo o histórico de negociação
   ::HistorySelect(0,LONG_MAX);
   uint deals_total=::HistoryDealsTotal();
//--- Coleta os dados sobre negociações
   for(uint i=0; i<deals_total; i++)
     {
      //--- Recebe um ticket
      ulong ticket=::HistoryDealGetTicket(i);
      if(ticket<1)
         continue;
      //--- Se um saldo inicial ou uma negociação out-/inout
      long entry=::HistoryDealGetInteger(ticket,DEAL_ENTRY);
      if(i==0 || entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT)
        {
         double swap      =::HistoryDealGetDouble(ticket,DEAL_SWAP);
         double profit    =::HistoryDealGetDouble(ticket,DEAL_PROFIT);
         double commision =::HistoryDealGetDouble(ticket,DEAL_COMMISSION);
         //--- Calcula o saldo
         balance_current+=(profit+swap+commision);
         //--- Salva para o array
         data_count++;
         ::ArrayResize(m_balance,data_count,100000);
         m_balance[data_count-1]=balance_current;
        }
     }
//--- Obtém a quantidade de dados
   return(data_count);
  }
//+------------------------------------------------------------------+
//| Recebe os dados estatísticos                                     |
//+------------------------------------------------------------------+
void CFrameGenerator::GetStatData(double &dst_array[],double on_tester_value)
  {
   ::ArrayResize(dst_array,::ArraySize(m_balance)+STAT_TOTAL);
   ::ArrayCopy(dst_array,m_balance,STAT_TOTAL,0);
//--- Preenche os primeiros valores da matriz (STAT_TOTAL) com os resultados do teste
   dst_array[0] =::TesterStatistics(STAT_PROFIT);               // Lucro líquido
   dst_array[1] =::TesterStatistics(STAT_PROFIT_FACTOR);        // Fator de lucro
   dst_array[2] =::TesterStatistics(STAT_RECOVERY_FACTOR);      // Fator de recuperação
   dst_array[3] =::TesterStatistics(STAT_TRADES);               // número de trades
   dst_array[4] =::TesterStatistics(STAT_DEALS);                // número de negócios
   dst_array[5] =::TesterStatistics(STAT_EQUITY_DDREL_PERCENT); // rebaixamento máximo do saldo em %
   dst_array[6] =on_tester_value;                               // valor do critério de otimização personalizada
  }

Os métodos CFrameGenerator::GetBalanceData() e CFrameGenerator::GetStatData() são chamados no manipulador de eventos de conclusão do teste - CFrameGenerator::OnTesterEvent() Dados recebidos. Envie-os para o terminal em um quadro

//+------------------------------------------------------------------+
//| Prepara o array de valores do saldo e envie-o em um quadro       |
//| A função deve ser chamada no manipulador da OnTester()           |
//+------------------------------------------------------------------+
void CFrameGenerator::OnTesterEvent(const double on_tester_value)
  {
//--- Obtém os dados do saldo
   int data_count=GetBalanceData();
//--- Array para enviar os dados para um quadro
   double stat_data[];
   GetStatData(stat_data,on_tester_value);
//--- Cria um quadro com os dados e envia para o terminal
   if(!::FrameAdd(::MQLInfoString(MQL_PROGRAM_NAME),1,data_count,stat_data))
      ::Print(__FUNCTION__," > Frame add error: ",::GetLastError());
   else
      ::Print(__FUNCTION__," > Frame added, Ok");
  }

Agora, vamos considerar os métodos a serem usados ​​no manipulador de eventos da chegada dos quadros durante a otimização — CFrameGenerator::OnTesterPassEvent() Nós vamos precisar das variáveis ​​para trabalhar com os quadros: nome, ID, número do passo, valor aceito e o array de dados aceito. Todos esses dados são enviados para o quadro usando a função FrameAdd(), que é exibida acima.

class CFrameGenerator
  {
private:
   //--- Variáveis ​​para trabalhar com os quadros
   string            m_name;
   ulong             m_pass;
   long              m_id;
   double            m_value;
   double            m_data[];
  };

O método CFrameGenerator::SaveStatData() do array que aceitamos no quadro é usado para obter os parâmetros estatísticos e salvá-los em um array de strings separados. Lá os dados devem conter o nome do indicador e seu valor. O símbolo '=' é usado como um separador.

class CFrameGenerator
  {
private:
   //--- Array com os parâmetros estatísticos
   string            m_stat_data[];
   //---
private:
   //--- Salva os dados estatísticos 
   void              SaveStatData(void);
  };
//+------------------------------------------------------------------+
//| Salva os parâmetros estatísticos do resultado no array           |
//+------------------------------------------------------------------+
void CFrameGenerator::SaveStatData(void)
  {
//--- Array para aceitar os parâmetros estatísticos do quadro
   double stat[];
   ::ArrayCopy(stat,m_data,0,0,STAT_TOTAL);
   ::ArrayResize(m_stat_data,STAT_TOTAL);
//--- Preenche o array com os resultados do teste
   m_stat_data[0] ="Net profit="+::StringFormat("%.2f",stat[0]);
   m_stat_data[1] ="Profit Factor="+::StringFormat("%.2f",stat[1]);
   m_stat_data[2] ="Factor Recovery="+::StringFormat("%.2f",stat[2]);
   m_stat_data[3] ="Trades="+::StringFormat("%G",stat[3]);
   m_stat_data[4] ="Deals="+::StringFormat("%G",stat[4]);
   m_stat_data[5] ="Equity DD="+::StringFormat("%.2f%%",stat[5]);
   m_stat_data[6] ="OnTester()="+::StringFormat("%G",stat[6]);
  }

Os dados estatísticos devem ser salvos em um array separado, para que possam ser recuperados na classe do aplicativo (CProgram) para preenchimento da tabela. O método público CFrameGenerator::CopyStatData() é chamado para recebê-los depois de passar o array para ser copiado.

class CFrameGenerator
  {
public:
   //--- Obtém os parâmetros estatísticos para o array passado
   int               CopyStatData(string &dst_array[]) { return(::ArrayCopy(dst_array,m_stat_data)); }
  };

Para atualizar os gráficos de resultados durante a otimização, nós precisaremos dos métodos auxiliares responsáveis ​​por adicionar os resultados positivos e negativos aos arrays. Por favor note que o resultado é adicionado ao valor atual do contador de quadros pelo eixo X. Como resultado, os vazios formados não são refletidos no gráfico como valores zero.

//--- Tamanho de reserva para os arrays
#define RESERVE_FRAMES 1000000
//+------------------------------------------------------------------+
//| Classe para trabalhar com os resultados da otimização            |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   //--- Contador de quadros
   ulong             m_frames_counter;
   //--- Dados sobre os resultados positivos e negativos
   double            m_loss_x[];
   double            m_loss_y[];
   double            m_profit_x[];
   double            m_profit_y[];
   //---
private:
   //--- Adiciona o resultado (1) negativo e (2) positivo nos arrays
   void              AddLoss(const double loss);
   void              AddProfit(const double profit);
  };
//+------------------------------------------------------------------+
//| Adiciona o resultado negativo ao array                           |
//+------------------------------------------------------------------+
void CFrameGenerator::AddLoss(const double loss)
  {
   int size=::ArraySize(m_loss_y);
   ::ArrayResize(m_loss_y,size+1,RESERVE_FRAMES);
   ::ArrayResize(m_loss_x,size+1,RESERVE_FRAMES);
   m_loss_y[size] =loss;
   m_loss_x[size] =(double)m_frames_counter;
  }
//+------------------------------------------------------------------+
//| Adciona o resultado positivo ao array                            |
//+------------------------------------------------------------------+
void CFrameGenerator::AddProfit(const double profit)
  {
   int size=::ArraySize(m_profit_y);
   ::ArrayResize(m_profit_y,size+1,RESERVE_FRAMES);
   ::ArrayResize(m_profit_x,size+1,RESERVE_FRAMES);
   m_profit_y[size] =profit;
   m_profit_x[size] =(double)m_frames_counter;
  }

Os principais métodos para atualizar os gráficos aqui são CFrameGenerator::UpdateResultsGraph() e CFrameGenerator::UpdateBalanceGraph():

class CFrameGenerator
  {
private:
   //--- Atualiza o gráfico de resultados
   void              UpdateResultsGraph(void);
   //--- Atualiza o gráfico de saldo
   void              UpdateBalanceGraph(void);
  };

No método CFrameGenerator::UpdateResultsGraph(), os resultados do teste (lucro positivo/negativo) são adicionados aos arrays. Em seguida, esses dados são exibidos em um gráfico apropriado. Os nomes das séries gráficas exibem o número atual dos resultados positivos e negativos. 

//+------------------------------------------------------------------+
//| Atualiza o gráfico de resultados                                 |
//+------------------------------------------------------------------+
void CFrameGenerator::UpdateResultsGraph(void)
  {
//--- Resultado negativo
   if(m_data[0]<0)
      AddLoss(m_data[0]);
//--- Resultado positivo
   else
      AddProfit(m_data[0]);
//--- Atualiza a série no gráfico de resultados da otimização
   CCurve *curve=m_graph_results.CurveGetByIndex(0);
   curve.Name("P: "+(string)ProfitsTotal());
   curve.Update(m_profit_x,m_profit_y);
//---
   curve=m_graph_results.CurveGetByIndex(1);
   curve.Name("L: "+(string)LossesTotal());
   curve.Update(m_loss_x,m_loss_y);
//--- Propriedades do eixo horizontal
   CAxis *x_axis=m_graph_results.XAxis();
   x_axis.Min(0);
   x_axis.Max(m_frames_counter);
   x_axis.DefaultStep((int)(m_frames_counter/8.0));
//--- Atualiza o gráfico
   m_graph_results.CalculateMaxMinValues();
   m_graph_results.CurvePlotAll();
   m_graph_results.Update();
  }

No começo do método CFrameGenerator::UpdateBalanceGraph(), os dados relacionados ao saldo são recuperados no array de dados passados ​​no quadro. Como várias séries podem ser exibidas no gráfico, nós devemos tornar a atualização da série consistente. Para conseguir isso, nós vamos usar um contador de séries separado. Para configurar o número de séries de saldo exibidas simultaneamente no gráfico, nós precisamos do método público CFrameGenerator::SetCurvesTotal(). Assim que o contador da série atingir o limite estabelecido, a contagem começa do início. O contador de quadros atua como os nomes das séries. A cor da série também depende do resultado: verde significa resultado positivo, vermelho — negativo.

Como o número de negociações em cada resultado é diferente, nós devemos definir as maiores séries e definir o máximo pelo eixo X para ajustar todas as séries necessárias no gráfico.

class CFrameGenerator
  {
private:
   //--- Número de séries
   uint              m_curves_total;
   //--- Índice da série atual no gráfico
   uint              m_last_serie_index;
   //--- Para definir a máxima da série
   double            m_curve_max[];
   //---
public:
   //--- Define o número de séries para exibir no gráfico
   void              SetCurvesTotal(const uint total);
  };
//+------------------------------------------------------------------+
//| Define o número de séries para a exibição no gráfico             |
//+------------------------------------------------------------------+
void CFrameGenerator::SetCurvesTotal(const uint total)
  {
   m_curves_total=total;
   ::ArrayResize(m_curve_max,total);
   ::ArrayInitialize(m_curve_max,0);
  }
//+------------------------------------------------------------------+
//| Atualiza o gráfico de saldo                                      |
//+------------------------------------------------------------------+
void CFrameGenerator::UpdateBalanceGraph(void)
  {
//--- Array para aceitar os valores do saldo do quadro atual
   double serie[];
   ::ArrayCopy(serie,m_data,0,STAT_TOTAL,::ArraySize(m_data)-STAT_TOTAL);
//--- Envia o array para exibição no gráfico de saldo
   CCurve *curve=m_graph_balance.CurveGetByIndex(m_last_serie_index);
   curve.Name((string)m_frames_counter);
   curve.Color((m_data[0]>=0)? ::ColorToARGB(clrLimeGreen) : ::ColorToARGB(clrRed));
   curve.Update(serie);
//--- Obtém o tamanho da série
   int serie_size=::ArraySize(serie);
   m_curve_max[m_last_serie_index]=serie_size;
//--- Define a série com o número máximo de elementos
   double x_max=0;
   for(uint i=0; i<m_curves_total; i++)
      x_max=::fmax(x_max,m_curve_max[i]);
//--- Propriedades do eixo horizontal
   CAxis *x_axis=m_graph_balance.XAxis();
   x_axis.Min(0);
   x_axis.Max(x_max);
   x_axis.DefaultStep((int)(x_max/8.0));
//--- Atualiza o gráfico
   m_graph_balance.CalculateMaxMinValues();
   m_graph_balance.CurvePlotAll();
   m_graph_balance.Update();
//--- Aumenta o contador de séries
   m_last_serie_index++;
//--- Se o limite for atingido, define o contador de séries para zero
   if(m_last_serie_index>=m_curves_total)
      m_last_serie_index=0;
  }

Nós consideramos os métodos necessários para organizar o trabalho no manipulador de quadros. Agora vamos dar uma olhada mais de perto no próprio método do manipulador CFrameGenerator::OnTesterPassEvent(). Ele retorna true, enquanto a otimização está em andamento e a função FrameNext() obtém os dados do quadro. Depois de concluir a otimização, o método retorna false.

Na lista do EA de parâmetros que podem ser obtidos usando a função FrameInputs(), os parâmetros definidos para otimização vão primeiro, seguidos pelos que não participam da otimização. 

Se os dados do quadro forem obtidos, a função FrameInputs() nos permite obter os parâmetros do EA durante o passo de otimização atual. Em seguida, nós salvamos as estatísticas, atualizamos os gráficos e aumentamos o contador de quadros. Depois disso, o método CFrameGenerator::OnTesterPassEvent() retorna true até a próxima chamada.

class CFrameGenerator
  {
private:
   //--- Parâmetros do EA
   string            m_param_data[];
   uint              m_par_count;
  };
//+------------------------------------------------------------------+
//| Recebe o quadro com os dados durante a otimização e exibe o gráfico |
//+------------------------------------------------------------------+
bool CFrameGenerator::OnTesterPassEvent(void)
  {
//--- Depois de obter um novo quadro, tenta recuperar os dados dele
   if(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
      //--- Obtém os parâmetros de entrada do EA o quadro é formado por
      ::FrameInputs(m_pass,m_param_data,m_par_count);
      //--- Salva os parâmetros estatísticos do resultado no array
      SaveStatData();
      //--- Atualiza o gráfico de resultados e balanceamento
      UpdateResultsGraph();
      UpdateBalanceGraph();
      //--- Aumenta o contador de quadros processados
      m_frames_counter++;
      return(true);
     }
//---
   return(false);
  }

Após a otimização ser concluída, o evento TesterDeinit é gerado e o método CFrameGenerator::OnTesterDeinitEvent() é chamado no modo de processamento de quadros. No momento, nem todos os quadros podem ser processados ​​durante a otimização, portanto, o gráfico de visualização de resultados estará incompleto. Para ver a imagem completa, você precisa percorrer todos os quadros usando o método CFrameGenerator::FinalRecalculateFrames() e recarregar o gráfico logo após a otimização.

Para fazer isso, mude o ponteiro para o início da lista de quadros e, em seguida, defina os arrays de resultados e o contador de quadros para zero. Em seguida, percorra a lista completa de quadros, preencha os arrays por resultados positivos e negativos e, eventualmente, atualize o gráfico.

class CFrameGenerator
  {
private:
   //--- Libera os arrays
   void              ArraysFree(void);
   //--- Re-cálculo final dos dados de todos os quadros após a otimização
   void              FinalRecalculateFrames(void);
  };
//+------------------------------------------------------------------+
//| Libera os arrays                                                 |
//+------------------------------------------------------------------+
void CFrameGenerator::ArraysFree(void)
  {
   ::ArrayFree(m_loss_y);
   ::ArrayFree(m_loss_x);
   ::ArrayFree(m_profit_y);
   ::ArrayFree(m_profit_x);
  }
//+------------------------------------------------------------------+
//| Re-cálculo final dos dados de todos os quadros após a otimização |
//+------------------------------------------------------------------+
void CFrameGenerator::FinalRecalculateFrames(void)
  {
//--- Define o ponteiro do quadro para o início
   ::FrameFirst();
//--- Redefine o contador e os arrays
   ArraysFree();
   m_frames_counter=0;
//--- Inicia o ciclo pelos quadros
   while(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
      //--- Resultado negativo
      if(m_data[0]<0)
         AddLoss(m_data[0]);
      //--- Resultado positivo
      else
         AddProfit(m_data[0]);
      //--- Aumenta o contador de quadros processados
      m_frames_counter++;
     }
//--- Atualiza a série no gráfico
   CCurve *curve=m_graph_results.CurveGetByIndex(0);
   curve.Name("P: "+(string)ProfitsTotal());
   curve.Update(m_profit_x,m_profit_y);
//---
   curve=m_graph_results.CurveGetByIndex(1);
   curve.Name("L: "+(string)LossesTotal());
   curve.Update(m_loss_x,m_loss_y);
//--- Propriedades do eixo horizontal
   CAxis *x_axis=m_graph_results.XAxis();
   x_axis.Min(0);
   x_axis.Max(m_frames_counter);
   x_axis.DefaultStep((int)(m_frames_counter/8.0));
//--- Atualiza o gráfico
   m_graph_results.CalculateMaxMinValues();
   m_graph_results.CurvePlotAll();
   m_graph_results.Update();
  }

Nesse caso, o código do método CFrameGenerator::OnTesterDeinitEvent() segue abaixo Aqui nós também nos lembramos do número total de quadros e definimos o contador para zero.

//+------------------------------------------------------------------+
//| Deve ser chamado no manipulador da OnTesterDeinit()              |
//+------------------------------------------------------------------+
void CFrameGenerator::OnTesterDeinitEvent(void)
  {
//--- Re-cálculo final dos dados de todos os quadros após a otimização
   FinalRecalculateFrames();
//--- Lembre-se do número total de quadros e defina os contadores como zero
   m_frames_total     =m_frames_counter;
   m_frames_counter   =0;
   m_last_serie_index =0;
  }

Em seguida, vamos dar uma olhada no uso dos métodos da classe CFrameGenerator na classe do aplicativo. 


Trabalhando com os dados da otimização na classe do aplicativo

A interface gráfica é criada no método de inicialização do teste CProgram::OnTesterInitEvent(). Depois disso, a interface gráfica deve ficar inacessível. Para fazer isso, nós precisamos dos métodos adicionais CProgram::IsAvailableGUI() e CProgram::IsLockedGUI() que será usado em outros métodos da classe CProgram.

Vamos inicializar o gerador de quadros: passar os ponteiros para os gráficos a serem usados ​​para visualizar os resultados da otimização.

class CProgram : public CWndEvents
  {
private:
   //--- Disponibilidade da interface
   void              IsAvailableGUI(const bool state);
   void              IsLockedGUI(const bool state);
  }
//+------------------------------------------------------------------+
//| Evento de início da otimização                                   |
//+------------------------------------------------------------------+
void CProgram::OnTesterInitEvent(void)
  {
//--- Cria a interface gráfica
   if(!CreateFrameModeGUI())
     {
      ::Print(__FUNCTION__," > Could not create the GUI!");
      return;
     }
//--- Torna a interface inacessível
   IsLockedGUI(false);
//--- Inicializa o gerador de quadros
   m_frame_gen.OnTesterInitEvent(m_graph1.GetGraphicPointer(),m_graph2.GetGraphicPointer());
  }
//+------------------------------------------------------------------+
//| Disponibilidade da interface                                     |
//+------------------------------------------------------------------+
void CProgram::IsAvailableGUI(const bool state)
  {
   m_window1.IsAvailable(state);
   m_sleep_ms.IsAvailable(state);
   m_curves_total.IsAvailable(state);
   m_reply_frames.IsAvailable(state);
  }
//+------------------------------------------------------------------+
//| Bloqueia a interface                                             |
//+------------------------------------------------------------------+
void CProgram::IsLockedGUI(const bool state)
  {
   m_window1.IsAvailable(state);
   m_sleep_ms.IsLocked(!state);
   m_curves_total.IsLocked(!state);
   m_reply_frames.IsLocked(!state);
  }

Nós já mencionamos que os dados nas tabelas devem ser atualizados na classe do aplicativo usando os métodos CProgram::UpdateStatTable() e CProgram::UpdateParamTable(). O código de ambas as tabelas é idêntico, então nós vamos dar um exemplo de apenas um deles. Os nomes dos parâmetros e valores na mesma linha são exibidos usando '=' como um separador. Portanto, nós passamos por eles em um loop e dividimos em um array separado, dividindo-o em dois elementos. Em seguida, nós inserimos esses valores nas células da tabela.

class CProgram : public CWndEvents
  {
private:
   //--- Atualiza a tabela de estatísticas
   void              UpdateStatTable(void);
   //--- Atualiza a tabela de parâmetros
   void              UpdateParamTable(void);
  }
//+------------------------------------------------------------------+
//| Atualiza a tabela da estatística                                 |
//+------------------------------------------------------------------+
void CProgram::UpdateStatTable(void)
  {
//--- Obtém o array de dados para a tabela da estatística
   string stat_data[];
   int total=m_frame_gen.CopyStatData(stat_data);
   for(int i=0; i<total; i++)
     {
      //--- Divide em duas linhas e entra na tabela
      string array[];
      if(::StringSplit(stat_data[i],'=',array)==2)
        {
         if(m_frame_gen.CurrentFrame()>1)
            m_table_stat.SetValue(1,i,array[1],0,true);
         else
           {
            m_table_stat.SetValue(0,i,array[0],0,true);
            m_table_stat.SetValue(1,i,array[1],0,true);
           }
        }
     }
//--- Atualiza a tabela
   m_table_stat.Update();
  }

Ambos os métodos para atualizar os dados nas tabelas são chamados no método CProgram::OnTesterPassEvent() por uma resposta positiva do método do mesmo nome CFrameGenerator::OnTesterPassEvent():

//+------------------------------------------------------------------+
//| Evento de processamento do passo da otimização                   |
//+------------------------------------------------------------------+
void CProgram::OnTesterPassEvent(void)
  {
//--- Processa os resultados dos testes obtidos e exibe o gráfico
   if(m_frame_gen.OnTesterPassEvent())
     {
      UpdateStatTable();
      UpdateParamTable();
     }
  }

Depois de concluir a otimização, o método CProgram::CalculateProfitsAndLosses() calcula a razão percentual dos resultados positivos e negativos e exibe os dados na barra de status:

class CProgram : public CWndEvents
  {
private:
   //--- Calcula a razão de resultados positivos e negativos
   void              CalculateProfitsAndLosses(void);
  }
//+------------------------------------------------------------------+
//| Calcula a razão de resultados positivos e negativos              |
//+------------------------------------------------------------------+
void CProgram::CalculateProfitsAndLosses(void)
  {
//--- Sai se não houver quadros
   if(m_frame_gen.FramesTotal()<1)
      return;
//--- Número de resultados negativos e positivos
   int losses  =m_frame_gen.LossesTotal();
   int profits =m_frame_gen.ProfitsTotal();
//--- Razão percentual
   string pl =::DoubleToString(((double)losses/(double)m_frame_gen.FramesTotal())*100,2);
   string pp =::DoubleToString(((double)profits/(double)m_frame_gen.FramesTotal())*100,2);;
//--- Exibir na barra de status
   m_status_bar.SetValue(1,"Profits: "+(string)profits+" ("+pp+"%)"+" / Losses: "+(string)losses+" ("+pl+"%)");
   m_status_bar.GetItemPointer(1).Update(true);
  }

O código do método para processamento do evento TesterDeinit é exibido abaixo. Inicializando o núcleo gráfico significa que o movimento do cursor do mouse deve ser rastreado e o timer deve ser ligado. Infelizmente, na versão atual da MetaTrader 5, o cronômetro não liga quando a otimização é concluída. Vamos esperar que esta oportunidade apareça no futuro.

//+------------------------------------------------------------------+
//| Evento de conclusão da otimização                                |
//+------------------------------------------------------------------+
void CProgram::OnTesterDeinitEvent(void)
  {
//--- Conclusão da otimização
   m_frame_gen.OnTesterDeinitEvent();
//--- Torna a interface acessível
   IsLockedGUI(true);
//--- Calcula a razão de resultados positivos e negativos
   CalculateProfitsAndLosses();
//--- inicializa o núcleo da GUI
   CWndEvents::InitializeCore();
  }

Agora nós também podemos trabalhar com os dados dos quadros após a conclusão da otimização. O EA é colocado no gráfico do terminal e os quadros podem ser acessados ​​para analisar os resultados. A interface gráfica torna tudo intuitivo. Dentro do método do manipulador do evento CProgram::OnEvent(), nós rastreamos:

  • mudanças no campo de entrada para definir o número de séries de saldo exibidas no gráfico;
  • iniciando a visualização dos resultados da otimização.

O método CProgram::UpdateBalanceGraph() é usado para atualizar o gráfico depois de alterar o número de séries. Aqui, nós definimos o número de séries para trabalhar no gerador de quadros e depois reservamos esse número no gráfico.

class CProgram : public CWndEvents
  {
private:
   //--- Atualiza o gráfico
   void              UpdateBalanceGraph(void);
  };
//+------------------------------------------------------------------+
//| Atualiza o gráfico                                               |
//+------------------------------------------------------------------+
void CProgram::UpdateBalanceGraph(void)
  {
//--- Define o número de séries para o trabalho
   int curves_total=(int)m_curves_total.GetValue();
   m_frame_gen.SetCurvesTotal(curves_total);
//--- Exclui as séries
   CGraphic *graph=m_graph1.GetGraphicPointer();
   int total=graph.CurvesTotal();
   for(int i=total-1; i>=0; i--)
      graph.CurveRemoveByIndex(i);
//--- Adiciona as séries
   double data[];
   for(int i=0; i<curves_total; i++)
      graph.CurveAdd(data,CURVE_LINES,"");
//--- Atualiza o gráfico
   graph.CurvePlotAll();
   graph.Update();
  }

No manipulador de eventos, o método CProgram::UpdateBalanceGraph() é chamado quando alterna-se os botões no campo de entrada (ON_CLICK_BUTTON) e quando o é inserido um valor no campo do teclado (ON_END_EDIT):

//+------------------------------------------------------------------+
//| Manipulador de eventos                                           |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Botão pressionando eventos
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- Altera o número de séries no gráfico
      if(lparam==m_curves_total.Id())
        {
         UpdateBalanceGraph();
         return;
        }
      return;
     }
//--- Evento de inserção de um valor no campo de entrada
   if(id==CHARTEVENT_CUSTOM+ON_END_EDIT)
     {
      //--- Altera o número de séries no gráfico
      if(lparam==m_curves_total.Id())
        {
         UpdateBalanceGraph();
         return;
        }
      return;
     }
  }

Para ver os resultados após a otimização na classe CFrameGenerator, o método público CFrameGenerator::ReplayFrames() é implementado. Aqui, no início, nós definimos o seguinte pelo contador de quadros: se o processo acabou de ser iniciado, os arrays são definidos como zero, e o ponteiro de quadros é deslocado para o início da lista. Posteriormente, os quadros são percorridos e as mesmas ações, como descritas anteriormente no método CFrameGenerator::OnTesterPassEvent() são executados. Se um quadro for recebido, o método retorna true. Após a conclusão, os contadores de quadro e série são definidos como zero e o método retorna false

class CFrameGenerator
  {
public:
   //--- Percorre os quadros
   bool              ReplayFrames(void);
  };
//+------------------------------------------------------------------+
//| Reproduz os quadros após a conclusão da otimização               |
//+------------------------------------------------------------------+
bool CFrameGenerator::ReplayFrames(void)
  {
//--- Define o ponteiro do quadro para o início
   if(m_frames_counter<1)
     {
      ArraysFree();
      ::FrameFirst();
     }
//--- Inicia o ciclo pelos quadros
   if(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
      //--- Obtém as entradas do EA, para as quais um quadro foi formado
      ::FrameInputs(m_pass,m_param_data,m_par_count);
      //--- Salva os parâmetros do resultado estatístico para o array
      SaveStatData();
      //--- Atualiza o gráfico de resultados e balanceamento
      UpdateResultsGraph();
      UpdateBalanceGraph();
      //--- Aumenta o contador de quadros processados
      m_frames_counter++;
      return(true);
     }
//--- Ciclo completo
   m_frames_counter   =0;
   m_last_serie_index =0;
   return(false);
  }

O método CFrameGenerator::ReplayFrames() é chamado na classe CProgram do método ViewOptimizationResults(). Antes de lançar os quadros, a interface gráfica fica indisponível. A velocidade de rolagem pode ser ajustada especificando uma pausa no campo de entrada Sleep. Enquanto isso, a barra de status exibe a barra de progresso mostrando o tempo antes do final do processo.

class CFrameGenerator
  {
private:
   //--- Visualiza os resultados da otimização
   void              ViewOptimizationResults(void);
  };
//+------------------------------------------------------------------+
//| Visualiza os resultados da otimização                            |
//+------------------------------------------------------------------+
void CProgram::ViewOptimizationResults(void)
  {
//--- Torna a interface indisponível
   IsAvailableGUI(false);
//--- Pausa
   int pause=(int)m_sleep_ms.GetValue();
//--- Reproduz os quadros
   while(m_frame_gen.ReplayFrames() && !::IsStopped())
     {
      //--- Atualiza as tabelas
      UpdateStatTable();
      UpdateParamTable();
      //--- Atualiza a barra de progresso
      m_progress_bar.Show();
      m_progress_bar.LabelText("Replay frames: "+string(m_frame_gen.CurrentFrame())+"/"+string(m_frame_gen.FramesTotal()));
      m_progress_bar.Update((int)m_frame_gen.CurrentFrame(),(int)m_frame_gen.FramesTotal());
      //--- Pausa
      ::Sleep(pause);
     }
//--- Calcula a razão de resultados positivos e negativos
   CalculateProfitsAndLosses();
//--- Oculta a barra de progresso
   m_progress_bar.Hide();
//--- Disponibiliza a interface
   IsAvailableGUI(true);
   m_reply_frames.MouseFocus(false);
   m_reply_frames.Update(true);
  }

O método CProgram::ViewOptimizationResults() é chamado pressionando o botão Replay Frames na interface gráfica da aplicação. O evento ON_CLICK_BUTTON é gerado.

//+------------------------------------------------------------------+
//| Manipulador de eventos                                           |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Evento de pressionar os botões
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- Visualiza os resultados da otimização 
      if(lparam==m_reply_frames.Id())
        {
         ViewOptimizationResults();
         return;
        }
      //--- 
      ...
      return;
     }
  }

Agora é hora de ver os resultados e definir o que um usuário realmente vê no gráfico durante a otimização ao trabalhar com quadros.


Exibindo os resultados obtidos

Para testes, nós usaremos o algoritmo de negociação padrão — Moving Average. Vamos implementá-lo como uma classe ("como está") sem adições e correções. Todos os arquivos do aplicativo desenvolvido devem estar localizados na mesma pasta. O arquivo da estratégia está incluso no arquivo Program.mqh.

O FormatString.mqh está incluído aqui como uma adição com funções para formatação de linhas. Eles ainda não fazem parte de nenhuma classe, então vamos marcar a flecha com a cor preta. A estrutura do aplicativo resultante é a seguinte:

Fig. 4. Incluindo a classe da estratégia de negociação e o arquivo com as funções adicionais 

Fig. 4. Incluindo a classe da estratégia de negociação e o arquivo com as funções adicionais

Vamos tentar otimizar os parâmetros e ver como fica o gráfico no terminal. Configurações do testador: EURUSD H1, intervalo de tempo 01.01.2017 - 01.01.2018.

Fig. 5. Demonstrando o resultado do EA Moving Average a partir do pacote padrão

Fig. 5. Demonstrando o resultado do EA Moving Average a partir do pacote padrão

Como podemos ver, ele acabou por ser bastante informativo. Quase todos os resultados para este algoritmo de negociação são negativos (95,23%). Se nós aumentarmos o intervalo de tempo, eles se tornarão ainda piores. No entanto, ao desenvolver um sistema de negociação, nós devemos nos certificar de que a maioria dos resultados seja positiva. Caso contrário, o algoritmo é deficitário e não deve ser usado. É necessário otimizar os parâmetros em mais dados e garantir que haja tantos negócios quanto possível.  

Vamos tentar testar outro algoritmo de negociação a partir do pacote padrão — MACD Sample.mq5. Ele já está implementado como uma classe. Após pequenas melhorias, nós podemos simplesmente conectá-lo ao nosso aplicativo, como o anterior. Nós devemos testá-lo no mesmo símbolo e tempo gráfico. Embora devamos aumentar o intervalo de tempo para mais negociações nos testes (01.01.2010 - 01.01.2018). Abaixo está o resultado de otimização do EA de negociação:

 Fig. 6. Mostrando o resultado da do MACD Sample do pacote padrão

Fig. 6. Mostrando o resultado da otimização do MACD Sample

Aqui nós vemos um resultado muito diferente: 90.89% de resultados positivos.

A otimização dos parâmetros pode demorar muito, dependendo da quantidade de dados usada. Você não precisa se sentar em frente ao seu PC durante todo o processo. Após a otimização, você pode iniciar a visualização repetida dos resultados no modo acelerado pressionando Replay frames. Vamos começar a reproduzir os quadros com o limite de exibição de 25 séries. É assim que ele se parece:

Fig. 7. Exibição do resultado do EA MACD Sample após a otimização

Fig. 7. Exibição do resultado do EA MACD Sample após a otimização


Conclusão

Neste artigo, nós apresentamos a versão moderna do programa para receber e analisar os quadros de otimização. Os dados são visualizados no ambiente de interface gráfica desenvolvido com base na biblioteca EasyAndFast

Uma desvantagem dessa solução é que, ao concluir a otimização no modo de processamento de quadros, é impossível iniciar o cronômetro. Isso impõe algumas limitações ao trabalho com a mesma interface gráfica. A segunda questão é que a desinicialização na função OnDeinit() não é acionada ao remover o EA do gráfico. Isso interfere no processamento correto do evento. Talvez, essas questões serão resolvidas nas versões futuras da MetaTrader 5 .