Gráfico de saldo multissímbolo no MetaTrader 5

Anatoli Kazharski | 7 maio, 2018


Sumário

Introdução

Num dos artigos anteriores, examinamos a visualização de gráficos de saldo multissímbolos. Porém, desde então, apareceram muitas bibliotecas MQL que permitem implementar completamente isso no terminal MetaTrader 5, sem usar programas de terceiros.

Neste artigo, mostrarei um aplicativo de exemplo com uma interface gráfica em que será mostrado um gráfico multissímbolo da saldo e rebaixamento do depósito, a partir dos resultados do último teste. No final do teste do EA, o histórico de trades será salvo num arquivo. Esses dados podem ser lidos e exibidos em gráficos, posteriormente.

Além disso, o artigo apresenta uma versão do EA em que o gráfico de saldo multissímbolo é exibido e atualizado, na interface gráfica, durante a negociação, bem como durante o teste, no modo de visualização.


Desenvolvimento da interface gráfica

O artigo Visualizemos a otimização de uma estratégia de negociação no MetaTrader 5 mostrou em detalhes como conectar e usar a biblioteca EasyAndFast, e, além disso, como implementá-la, a fim de criar uma interface gráfica para um aplicativo MQL próprio. Por conseguinte, aqui abordaremos imediatamente o tema da interface gráfica sobre o tópico em consideração. 

Vamos listar os elementos que serão usados ​​na interface gráfica.

  • Formulário para controles.
  • Botão para atualizar os gráficos com os resultados do último teste.
  • Gráfico para exibir o saldo multissímbolo.
  • Gráfico para exibir os rebaixamentos do depósito.
  • A barra de status para exibir informações de resumo adicionais.

A listagem de código abaixo mostra as declarações de método para criar esses elementos. A implementação dos métodos é feita de forma separada no arquivo anexado.

//+------------------------------------------------------------------+
//| Classe para criar o aplicativo                                   |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Janela
   CWindow           m_window1;
   //--- Barra de Status
   CStatusBar        m_status_bar;
   //--- Gráficos
   CGraph            m_graph1;
   CGraph            m_graph2;
   //--- Botões
   CButton           m_update_graph;
   //---
public:
   //--- Cria uma interface gráfica
   bool              CreateGUI(void);
   //---
private:
   //--- Forma
   bool              CreateWindow(const string text);
   //--- Barra de Status
   bool              CreateStatusBar(const int x_gap,const int y_gap);
   //--- Gráficos
   bool              CreateGraph1(const int x_gap,const int y_gap);
   bool              CreateGraph2(const int x_gap,const int y_gap);
   //--- Botões
   bool              CreateUpdateGraph(const int x_gap,const int y_gap,const string text);
  };
//+------------------------------------------------------------------+
//| Métodos para criar controles                                     |
//+------------------------------------------------------------------+
#include "CreateGUI.mqh"
//+------------------------------------------------------------------+

O principal método de criar o GUI será como se segue:

//+------------------------------------------------------------------+
//| Cria a interface gráfica do usuário                              |
//+------------------------------------------------------------------+
bool CProgram::CreateGUI(void)
  {
//--- Criação do formulário para controles
   if(!CreateWindow("Expert panel"))
      return(false);
//--- Criação de controles
   if(!CreateStatusBar(1,23))
      return(false);
   if(!CreateGraph1(1,50))
      return(false);
   if(!CreateGraph2(1,159))
      return(false);
   if(!CreateUpdateGraph(7,25,"Update data"))
      return(false);
//--- Conclusão da criação da GUI
   CWndEvents::CompletedGUI();
   return(true);
  }

Como resultado disso, se você agora compilar o EA e carregá-lo no gráfico, no terminal, o resultado atual ficará assim:

 Fig. 1 – Interface gráfica do EA.

Fig. 1. Interface gráfica do EA.

Em seguida, tomamos conta do registro de dados num arquivo, após o teste.


EA multissímbolo para testes

Para testes, pegamos no EA MACD Sample da entrega padrão, mas vamos torná-lo multissímbolo. O esquema multissímbolo utilizado nesta versão é impreciso. Nos mesmos parâmetros, o resultado será diferente dependendo do símbolo em que será realizado o teste (selecionado nas configurações do testador). Por isso, este EA se destina apenas a testes e apresentação dos resultados obtidos no âmbito do tópico apresentado.

Nas atualizações mais próximas do terminal MetaTrader 5, serão apresentadas novas possibilidades para a criação de EAs multissímbolos. Então, será possível pensar em criar uma versão final e universal para EAs desse tipo. Se você precisa urgentemente de um esquema multissímbolo rápido e preciso, pode experimentar a opção que foi proposta no fórum.

Nos parâmetros externos, adicionamos mais um parâmetro de string, para especificar os símbolo em que será realizado o teste:

//--- Parâmetros externos
sinput string Symbols           ="EURUSD,USDJPY,GBPUSD,EURCHF"; // Symbols
input  double InpLots           =0.1;                           // Lots
input  int    InpTakeProfit     =167;                           // Take Profit (in pips)
input  int    InpTrailingStop   =97;                            // Trailing Stop Level (in pips)
input  int    InpMACDOpenLevel  =16;                            // MACD open level (in pips)
input  int    InpMACDCloseLevel =19;                            // MACD close level (in pips)
input  int    InpMATrendPeriod  =14;                            // MA trend period

Os símbolos devem ser especificados separados por uma vírgula. Na classe do programa (CProgram), são implementados métodos para leitura deste parâmetro, bem como para verificar e definir os símbolos, na observação do mercado, daqueles que estão na lista no servidor. Como alternativa, você pode especificar símbolos para negociação por meio de uma lista preparada num arquivo, conforme mostrado no artigo Guia prático do MQL5: desenvolvendo um consultor especialista multimoeda com um número ilimitado de parâmetros. Além disso, você pode, no arquivo, criar várias listas opcionais. Você pode ver esse exemplo no artigo Guia prático do MQL5: reduzindo o efeito de sobreajuste e lidando com a falta de cotações. Você pode encontrar muitas outras maneiras de selecionar símbolos e suas listas usando a interface gráfica. Num dos artigos a seguir, mostrarei essa abordagem. 

Antes de validar os símbolos na lista comum, é preciso salvá-los numa matriz. Em seguida, passamos esta matriz (source_array[]) para o método CProgram::CheckTradeSymbols(). Aqui, no primeiro ciclo, passamos esses símbolo para um parâmetro externo, e depois, no segundo ciclo, verificamos se existe este símbolo na lista no servidor da corretora. Se existir, devemos adicioná-lo à janela "Observação do mercado" e à matriz de símbolos verificados. 

No final do método, se nenhum símbolo for encontrado, somente será usado o símbolo atual em que está instalado o EA.

class CProgram : public CWndEvents
  {
private:
   //--- Verifica os símbolos para negociação na matriz passada e retorna uma matriz de símbolos disponíveis
   void              CheckTradeSymbols(string &source_array[],string &checked_array[]);
  };
//+------------------------------------------------------------------+
//--- Verifica os símbolos para negociação na matriz passada e       |
//| retorna uma matriz de símbolos disponíveis                       |
//+------------------------------------------------------------------+
void CProgram::CheckTradeSymbols(string &source_array[],string &checked_array[])
  {
   int symbols_total     =::SymbolsTotal(false);
   int size_source_array =::ArraySize(source_array);
//--- Procuramos os símbolos especificados na lista geral
   for(int i=0; i<size_source_array; i++)
     {
      for(int s=0; s<symbols_total; s++)
        {
         //--- Obtemos o nome do símbolo atual na lista geral
         string symbol_name=::SymbolName(s,false);
         //--- Se coincidir
         if(symbol_name==source_array[i])
           {
            //--- Definimos o símbolo na observação do mercado
            ::SymbolSelect(symbol_name,true);
            //--- Adicionamos à matriz de símbolos confirmados
            int size_array=::ArraySize(checked_array);
            ::ArrayResize(checked_array,size_array+1);
            checked_array[size_array]=symbol_name;
            break;
           }
        }
     }
//--- Se nenhum símbolo for encontrado, usamos apenas o símbolo atual
   if(::ArraySize(checked_array)<1)
     {
      ::ArrayResize(checked_array,1);
      checked_array[0]=_Symbol;
     }
  }

Para ler o parâmetro externo de string em que são especificados os símbolos, é usado o método CProgram::CheckSymbols(). Aqui, a string é dividida numa matriz pelo separador ','. Nas linhas resultantes, são cortados os espaços em branco em ambos os lados. Depois disso, a matriz é enviada para verificação ao método CProgram::CheckTradeSymbols() que consideramos acima.

class CProgram : public CWndEvents
  {
private:
   //--- Verifica e seleciona para a matriz os símbolos para negociação a partir da string
   int               CheckSymbols(const string symbols_enum);
  };
//+-----------------------------------------------------------------------------------+
//| Verifica e seleciona na matriz os símbolos para negociação a partir da string     |
//+-----------------------------------------------------------------------------------+
int CProgram::CheckSymbols(const string symbols_enum)
  {
   if(symbols_enum!="")
      ::Print(__FUNCTION__," > input trade symbols: ",symbols_enum);
//--- Obtemos os símbolos a partir da strings
   string symbols[];
   ushort u_sep=::StringGetCharacter(",",0);
   ::StringSplit(symbols_enum,u_sep,symbols);
//--- Cortamos os espaços em branco de ambos os lados
   int elements_total=::ArraySize(symbols);
   for(int e=0; e<elements_total; e++)
     {
      ::StringTrimLeft(symbols[e]);
      ::StringTrimRight(symbols[e]);
     }
//--- Verificamos os símbolos
   ::ArrayFree(m_symbols);
   CheckTradeSymbols(symbols,m_symbols);
//--- Retornamos o número de símbolos para negociação
   return(::ArraySize(m_symbols));
  }

O arquivo com a classe de estratégia de negociação é anexado ao arquivo com a classe de aplicativo, e é criada uma matriz dinâmica de tipo CStrategy

#include "Strategy.mqh"
//+------------------------------------------------------------------+
//| Classe para criar o aplicativo                                   |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Matriz de estratégias
   CStrategy         m_strategy[];
  };

Durante a inicialização do programa, é, precisamente, aqui que, a partir de um parâmetro externo, obtemos a matriz de símbolos e seu número. Em seguida, pelo número de caracteres, definimos o tamanho da matriz de estratégias e inicializamos todas as instâncias das estratégias, passado em cada uma delas o nome de símbolo.

class CProgram : public CWndEvents
  {
private:
   //--- Total de símbolos
   int               m_symbols_total;
  };
//+------------------------------------------------------------------+
//| Inicialização                                                    |
//+------------------------------------------------------------------+
bool CProgram::OnInitEvent(void)
  {
//--- Obtemos os símbolos para a negociação
   m_symbols_total=CheckSymbols(Symbols);
//--- Tamanho da matriz da estratégia de negociação
   ::ArrayResize(m_strategy,m_symbols_total);
//--- Inicialização
   for(int i=0; i<m_symbols_total; i++)
     {
      if(!m_strategy[i].OnInitEvent(m_symbols[i]))
         return(false);
     }
//--- Inicialização bem-sucedida
   return(true);
  }

Em seguida, consideramos salvar os dados do último teste num arquivo.


Salvando dados num arquivo

Salvamos os dados do último teste na pasta de dados compartilhados do terminal. Assim, o arquivo estará acessível a partir de qualquer terminal MetaTrader 5. No construtor, de uma vez, definimos o nome da pasta e o nome do arquivo:

class CProgram : public CWndEvents
  {
private:
   //--- Caminho para o arquivo com os resultados do último teste
   string            m_last_test_report_path;
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void) : m_symbols_total(0)
  {
//--- Caminho para o arquivo com os resultados do último teste
   m_last_test_report_path=::MQLInfoString(MQL_PROGRAM_NAME)+"\\LastTest.csv";
  }

Examinamos o método CProgram::CreateSymbolBalanceReport(), que nos ajudará a salvar no arquivo. Para trabalhar neste método (e também no outro que vamos considerar mais adiante), precisaremos de matrizes de saldos de símbolos.

//--- Matrizes para os saldos de todos os símbolos
struct CReportBalance { double m_data[]; };
//+------------------------------------------------------------------+
//| Classe para criar o aplicativo                                   |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Matriz dos saldos de todos os símbolos
   CReportBalance    m_symbol_balance[];
   //---
private:
   //--- Cria um relatório de teste para trades no formato CSV
   void              CreateSymbolBalanceReport(void);
  };
//+------------------------------------------------------------------+
//| Cria um relatório de teste para os trades no formato CSV         |
//+------------------------------------------------------------------+
void CProgram::CreateSymbolBalanceReport(void)
  {
   ...
  }

No início do método, abrimos o arquivo para trabalhar na pasta compartilhada dos terminais (FILE_COMMON):

...
//--- Criamos o arquivo para salvar os dados na pasta compartilhada do terminal
   int file_handle=::FileOpen(m_last_test_report_path,FILE_CSV|FILE_WRITE|FILE_ANSI|FILE_COMMON);
//--- Se o identificador for válido (o arquivo for criado/aberto)
   if(file_handle==INVALID_HANDLE)
     {
      ::Print(__FUNCTION__," > Error creating file: ",::GetLastError());
      return;
     }
...

Algumas variáveis ​​auxiliares serão necessárias para formar alguns indicadores do relatório. Salvaremos no arquivo todo o histórico de trades com os dados listados abaixo:

  • Hora do trade
  • Símbolo
  • Tipo
  • Direção
  • Volume
  • Preço
  • Swap
  • Resultado (lucro/perda)
  • Rebaixamento
  • Saldo. Nesta coluna, aparecerá o saldo total, e, nas seguintes, os saldos de símbolos envolvidos no teste

Aqui formamos a primeira linha com os cabeçalhos desses dados:

...
   double max_drawdown    =0.0; // Rebaixamento máximo
   double balance         =0.0; // Saldo
   string delimeter       =","; // Separador
   string string_to_write ="";  // Para formar uma linha para gravação
//--- Formamos a linha de cabeçalho
   string headers="TIME,SYMBOL,DEAL TYPE,ENTRY TYPE,VOLUME,PRICE,SWAP($),PROFIT($),DRAWDOWN(%),BALANCE";
...

Se mais de um símbolo participar do teste, a linha com os cabeçalhos deve ser complementada com seus nomes. Depois disso, os cabeçalhos (a primeira linha) podem ser salvos no arquivo

...
//--- Se mais de um símbolo participar, complementamos a linha de cabeçalhos
   int symbols_total=::ArraySize(m_symbols);
   if(symbols_total>1)
     {
      for(int s=0; s<symbols_total; s++)
         ::StringAdd(headers,delimeter+m_symbols[s]);
     }
//--- Escrevemos os cabeçalhos do relatório
   ::FileWrite(file_handle,headers);
...

Em seguida, obtemos tanto o histórico de trades completo quanto seu número e, em seguida, definimos os tamanhos das matrizes:

...
//--- Obtemos todo o histórico
   ::HistorySelect(0,LONG_MAX);
//--- Conhecemos o número de trades
   int deals_total=::HistoryDealsTotal();
//--- Definimos o tamanho da matriz de saldos pelo número de símbolos
   ::ArrayResize(m_symbol_balance,symbols_total);
//--- Definimos o tamanho das matrizes de trades para cada símbolo
   for(int s=0; s<symbols_total; s++)
      ::ArrayResize(m_symbol_balance[s].m_data,deals_total);
...

No ciclo principal, percorremos todo o histórico e formamos as linhas para gravar no arquivo. Ao calcular os lucros, também somamos o swap e a comissão. Se for verificado que os símbolos são mais de um, no segundo ciclo vamos por cada um deles e formamos o saldo para cada símbolo.

Escrevemos os dados num arquivo, linha por linha. No final do método, o arquivo é fechado.
...
//--- Passamos pelo ciclo e registramos os dados
   for(int i=0; i<deals_total; i++)
     {
      //--- Obtemos o boleto do trade
      if(!m_deal_info.SelectByIndex(i))
         continue;
      //--- Ficamos sabendo o número de caracteres no preço
      int digits=(int)::SymbolInfoInteger(m_deal_info.Symbol(),SYMBOL_DIGITS);
      //--- Calculamos o saldo total
      balance+=m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
      //--- Formamos uma linha para salvar usando concatenação
      ::StringConcatenate(string_to_write,
                          ::TimeToString(m_deal_info.Time(),TIME_DATE|TIME_MINUTES),delimeter,
                          m_deal_info.Symbol(),delimeter,
                          m_deal_info.TypeDescription(),delimeter,
                          m_deal_info.EntryDescription(),delimeter,
                          ::DoubleToString(m_deal_info.Volume(),2),delimeter,
                          ::DoubleToString(m_deal_info.Price(),digits),delimeter,
                          ::DoubleToString(m_deal_info.Swap(),2),delimeter,
                          ::DoubleToString(m_deal_info.Profit(),2),delimeter,
                          MaxDrawdownToString(i,balance,max_drawdown),delimeter,
                          ::DoubleToString(balance,2));
      //--- Se mais de um símbolo estiver envolvido, escrevemos os valores do seu saldo
      if(symbols_total>1)
        {
         //--- Passamos por todos os símbolos
         for(int s=0; s<symbols_total; s++)
           {
            //--- Se os símbolos corresponderem e o resultado do trade for diferente de zero
            if(m_deal_info.Symbol()==m_symbols[s] && m_deal_info.Profit()!=0)
               //--- Exibimos o trade, no saldo, com este símbolo. Consideramos o swap e a comissão
               m_symbol_balance[s].m_data[i]=m_symbol_balance[s].m_data[i-1]+m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
            //--- Caso contrário, escrevemos o valor anterior
            else
              {
               // --- Se o tipo de trade for "Cálculo do saldo " (a primeira transação), para todos os símbolos, o saldo será o mesmo
               if(m_deal_info.DealType()==DEAL_TYPE_BALANCE)
                  m_symbol_balance[s].m_data[i]=balance;
               //--- Caso contrário, escrevemos o valor anterior no índice atual
               else
                  m_symbol_balance[s].m_data[i]=m_symbol_balance[s].m_data[i-1];
              }
            //--- Adicionamos à linha o saldo do símbolo
            ::StringAdd(string_to_write,delimeter+::DoubleToString(m_symbol_balance[s].m_data[i],2));
           }
        }
      //--- Escrevemos a linha gerada
      ::FileWrite(file_handle,string_to_write);
      //--- Zeramento obrigatório para a próxima linha
      string_to_write="";
     }
//--- Fechamos o arquivo
   ::FileClose(file_handle);
...

No processo de geração de linhas (veja o código acima), a fim de salvar no arquivo para calcular o rebaixamento total do saldo, é usado o método CProgram::MaxDrawdownToString(). Quando esse método é chamado pela primeira vez, o rebaixamento é zero, e, como um máximo/mínimo local, iremos nos lembrar do valor do saldo atual. Nas seguintes chamadas de método, no caso em que o saldo é maior que na memória, consideramos o rebaixamento de acordo com os valores anteriores e atualizamos o máximo local. Caso contrário, atualizamos o mínimo local e retornamos um valor nulo (uma linha em branco).

class CProgram : public CWndEvents
  {
private:
   //--- Retorna o rebaixamento máximo do máximo local
   string            MaxDrawdownToString(const int deal_number,const double balance,double &max_drawdown);
  };
//+------------------------------------------------------------------+
//| Retorna o rebaixamento máximo do máximo local                    |
//+------------------------------------------------------------------+
string CProgram::MaxDrawdownToString(const int deal_number,const double balance,double &max_drawdown)
  {
//--- Linha para exibição no relatório
   string str="";
//--- Para calcular o máximo local e o rebaixamento
   static double max=0.0;
   static double min=0.0;
//--- Se o primeiro trade
   if(deal_number==0)
     {
      //--- Ainda não houver rebaixamento
      max_drawdown=0.0;
      //--- Definimos o ponto inicial como um máximo local
      max=balance;
      min=balance;
     }
   else
     {
      //--- Se o saldo atual for maior que na memória
      if(balance>max)
        {
         //--- Calculamos o rebaixamento de acordo com os valores anteriores
         max_drawdown=100-((min/max)*100);
         //--- Atualizamos o máximo local
         max=balance;
         min=balance;
        }
      else
        {
         //--- Retornamos o valor zero do rebaixamento e atualizamos o mínimo
         max_drawdown=0.0;
         min=fmin(min,balance);
        }
     }
//--- Definimos a linha do relatório
   str=(max_drawdown==0)? "" : ::DoubleToString(max_drawdown,2);
   return(str);
  }

A estrutura do arquivo permite abri-lo no Excel. A imagem abaixo apresenta como é mostrado:

 Fig. 2 – Estrutura do arquivo de relatório.

Fig. 2. Estrutura do arquivo de relatório no Excel.

Como resultado disso, é preciso fazer, no final do teste, a chamada do método CProgram::CreateSymbolBalanceReport(), para salvar o relatório, após o teste:

//+------------------------------------------------------------------+
//| Evento de final de teste                                         |
//+------------------------------------------------------------------+
double CProgram::OnTesterEvent(void)
  {
//--- Salvamos o relatório somente após o teste
   if(::MQLInfoInteger(MQL_TESTER) && !::MQLInfoInteger(MQL_OPTIMIZATION) && 
      !::MQLInfoInteger(MQL_VISUAL_MODE) && !::MQLInfoInteger(MQL_FRAME_MODE))
     {
      //--- Geração do relatório e gravação em arquivos
      CreateSymbolBalanceReport();
     }
//---
   return(0.0);
  }

Em seguida, consideramos a leitura dos dados do relatório.


Extraindo dados de um arquivo

Após tudo o que foi implementado acima, agora, todas as verificações do EA, no Testador de Estratégias, terminarão com um registro do relatório no arquivo. Em seguida, consideraremos os métodos pelos quais serão lidos os dados desse relatório. Primeiro de tudo, é preciso ler o arquivo e colocar seu conteúdo numa matriz, para que seja conveniente trabalhar com ele. Para isso, usamos o método CProgram::ReadFileToArray(). Aqui, abrimos o arquivo, no qual, no final do teste do EA, foi registrado o histórico de trades. No ciclo, lemos o arquivo, até a última linha, e preenchemos a matriz com os dados de origem. 

class CProgram : public CWndEvents
  {
private:
   //--- Matriz para os dados do arquivo
   string            m_source_data[];
   //--- 
private:
   //--- Leitura do arquivo para a matriz passada
   bool              ReadFileToArray(const int file_handle);
  };
//+------------------------------------------------------------------+
//| Leitura do arquivo na matriz passada                             |
//+------------------------------------------------------------------+
bool CProgram::ReadFileToArray(const int file_handle)
  {
//--- Abrimos o arquivo
   int file_handle=::FileOpen(m_last_test_report_path,FILE_READ|FILE_ANSI|FILE_COMMON);
//--- Sair se o arquivo não abrir
   if(file_handle==INVALID_HANDLE)
      return(false);
//--- Liberamos a matriz
   ::ArrayFree(m_source_data);
//--- Lemos o arquivo numa matriz
   while(!::FileIsEnding(file_handle))
     {
      int size=::ArraySize(m_source_data);
      ::ArrayResize(m_source_data,size+1,RESERVE);
      m_source_data[size]=::FileReadString(file_handle);
     }
//--- Fechar o arquivo
   ::FileClose(file_handle);
   return(true);
  }

Será necessário o método auxiliar CProgram::GetStartIndex() para definir o índice da coluna denominada BALANCE. Como argumentos, você precisa passar para ele uma linha de cabeçalho, na qual será realizada a pesquisa do nome da coluna e da matriz dinâmica para os elementos da linha dividida pelo separador ','.  

class CProgram : public CWndEvents
  {
private:
   //--- Índice inicial de saldos no relatório
   bool              GetBalanceIndex(const string headers);
  };
//+-------------------------------------------------------------------------------+
//| Definimos o índice a partir do qual é necessário iniciar a cópia de dados     |
//+-------------------------------------------------------------------------------+
bool CProgram::GetBalanceIndex(const string headers)
  {
//--- Obtemos os elementos da linha pelo separador
   string str_elements[];
   ushort u_sep=::StringGetCharacter(",",0);
   ::StringSplit(headers,u_sep,str_elements);
//--- Procuramos uma coluna chamada 'BALANCE'
   int elements_total=::ArraySize(str_elements);
   for(int e=elements_total-1; e>=0; e--)
     {
      string str=str_elements[e];
      ::StringToUpper(str);
      //--- Se encontrarmos a coluna com o cabeçalho desejado
      if(str=="BALANCE")
        {
         m_balance_index=e;
         break;
        }
     }
//--- Exibir a mensagem, se não for encontrada a coluna denominada 'BALANCE'
   if(m_balance_index==WRONG_VALUE)
     {
      ::Print(__FUNCTION__," > In the report file there is no heading \'BALANCE\' ! ");
      return(false);
     }
//--- Se for bem-sucedido
   return(true);
  }

No eixo X, ambos os gráficos exibirão os números dos trades. O intervalo de datas será mostrado como informação adicional no rodapé do saldo. Para determinar a data inicial e final do histórico de trades, é implementado o método CProgram::GetDateRange(). Duas variáveis ​​de string são passadas para ele, por referência, para a data inicial e final do histórico de trades.

class CProgram : public CWndEvents
  {
private:
   //--- Intervalo de datas
   void              GetDateRange(string &from_date,string &to_date);
  };
//+------------------------------------------------------------------+
//| Obtemos as datas inicial e final do intervalo de teste           |
//+------------------------------------------------------------------+
void CProgram::GetDateRange(string &from_date,string &to_date)
  {
//--- Sair, se a linha for inferior a 3
   int strings_total=::ArraySize(m_source_data);
   if(strings_total<3)
      return;
//--- Obtemos a datas inicial e final do relatório
   string str_elements[];
   ushort u_sep=::StringGetCharacter(",",0);
//---
   ::StringSplit(m_source_data[1],u_sep,str_elements);
   from_date=str_elements[0];
   ::StringSplit(m_source_data[strings_total-1],u_sep,str_elements);
   to_date=str_elements[0];
  }

Para obter os dados dos dados de saldos e rebaixamentos, são usados os métodos CProgram::GetReportDataToArray() e CProgram::AddDrawDown(). O segundo é chamado no primeiro, e seu código é muito breve (veja a listagem abaixo). Aqui, o índice de transação e o valor do rebaixamento são transferidos. Eles são colocados em matrizes correspondentes cujos valores serão exibidos no gráfico. Na matriz m_dd_y[] armazenamos o valor do rebaixamento, enquanto na matriz m_dd_x[] - o índice em que deve ser exibido esse valor. Assim, nos índices onde não há valores, nada será exibido nos gráficos (valores vazios).

class CProgram : public CWndEvents
  {
private:
   //--- Rebaixamento do saldo geral
   double            m_dd_x[];
   double            m_dd_y[];
   //--- 
private:
   //--- Adiciona o rebaixamento na matriz
   void              AddDrawDown(const int index,const double drawdown);
  };
//+------------------------------------------------------------------+
//| Adiciona o rebaixamento na matriz                                |
//+------------------------------------------------------------------+
void CProgram::AddDrawDown(const int index,const double drawdown)
  {
   int size=::ArraySize(m_dd_y);
   ::ArrayResize(m_dd_y,size+1,RESERVE);
   ::ArrayResize(m_dd_x,size+1,RESERVE);
   m_dd_y[size] =drawdown;
   m_dd_x[size] =(double)index;
  }

No método CProgram::GetReportDataToArray(), primeiro são definidos os tamanhos das matrizes e a quantidade de séries, para o gráfico de saldos. Logo, inicializamos a matriz de cabeçalhos. Em seguida, no ciclo, os elementos de linha são extraídos, linha por linha, pelo separador, e os dados são colocados em matrizes de rebaixamento e saldo.  

class CProgram : public CWndEvents
  {
private:
   //--- Obtém os dados dos símbolos a partir do relatório
   int               GetReportDataToArray(string &headers[]);
  };
//+------------------------------------------------------------------+
//| Recebemos os dados dos símbolos a partir do relatório            |
//+------------------------------------------------------------------+
int CProgram::GetReportDataToArray(string &headers[])
  {
//--- Obtemos os elementos da linha de cabeçalho
   string str_elements[];
   ushort u_sep=::StringGetCharacter(",",0);
   ::StringSplit(m_source_data[0],u_sep,str_elements);
//--- Dimensões de matriz
   int strings_total  =::ArraySize(m_source_data);
   int elements_total =::ArraySize(str_elements);
//--- Liberamos as matrizes
   ::ArrayFree(m_dd_y);
   ::ArrayFree(m_dd_x);
//--- Obtemos o número de séries
   int curves_total=elements_total-m_balance_index;
   curves_total=(curves_total<3)? 1 : curves_total;
//--- Definimos o tamanho da matriz de acordo com o número de séries
   ::ArrayResize(headers,curves_total);
   ::ArrayResize(m_symbol_balance,curves_total);
//--- Definimos o tamanho da série
   for(int i=0; i<curves_total; i++)
      ::ArrayResize(m_symbol_balance[i].m_data,strings_total,RESERVE);
//--- Se houver vários símbolos (obtemos os cabeçalhos)
   if(curves_total>2)
     {
      for(int i=0,e=m_balance_index; e<elements_total; e++,i++)
         headers[i]=str_elements[e];
     }
   else
      headers[0]=str_elements[m_balance_index];
//---Obtemos os dados
   for(int i=1; i<strings_total; i++)
     {
      ::StringSplit(m_source_data[i],u_sep,str_elements);
      //--- Coletamos os dados nas matrizes
      if(str_elements[m_balance_index-1]!="")
         AddDrawDown(i,double(str_elements[m_balance_index-1]));
      //--- Se houver vários símbolos
      if(curves_total>2)
         for(int b=0,e=m_balance_index; e<elements_total; e++,b++)
            m_symbol_balance[b].m_data[i]=double(str_elements[e]);
      else
         m_symbol_balance[0].m_data[i]=double(str_elements[m_balance_index]);
     }
//--- Primeiro valor da série
   for(int i=0; i<curves_total; i++)
      m_symbol_balance[i].m_data[0]=(strings_total<2)? 0 : m_symbol_balance[i].m_data[1];
//--- Retornar o número de séries
   return(curves_total);
  }

Na próxima seção, veremos como exibir os dados nos gráficos.


Exibindo dados em gráficos

A chamada de métodos auxiliares, que abordamos na seção anterior, ocorrerá no início do método, para atualização do gráfico de saldo - CProgram::UpdateBalanceGraph(). Logo, as séries atuais são excluídas do gráfico, porque a quantidade símbolos participando no último teste pode mudar. Então, no ciclo, pelo número atual de símbolos definidos no método CProgram::GetReportDataToArray(), acrescentamos as novas séries de dados de saldo e, ao mesmo tempo, definimos os valores mínimo e máximo ao longo do eixo Y. 

Aqui decoramos, nos campos da classe, o tamanho da série e a marca de escala ao longo do eixo X. Esses valores também serão necessários para formatar o gráfico de rebaixamento. Para o eixo Y, são ajustados recuos, para os extremos do gráfico, iguais a 5%. Como resultado, todos esses valores se aplicam ao gráfico de saldo, enquanto o gráfico é atualizado para mostrar as alterações mais recentes. 

class CProgram : public CWndEvents
  {
private:
   //--- Total de dados na série
   double            m_data_total;
   //--- Marca de escala X
   double            m_default_step;
   //--- 
private:
   //--- Atualiza os dados no gráfico de saldo 
   void              UpdateBalanceGraph(void);
  };
//+------------------------------------------------------------------+
//| Atualizar o gráfico de saldo                                     |
//+------------------------------------------------------------------+
void CProgram::UpdateBalanceGraph(void)
  {
//--- Obtemos os dados do intervalo de teste
   string from_date=NULL,to_date=NULL;
   GetDateRange(from_date,to_date);
//--- Definimos o índice a partir do qual é necessário começar a copiar dados
   if(!GetBalanceIndex(m_source_data[0]))
      return;
//--- Obtemos os dados dos símbolos a partir do relatório
   string headers[];
   int curves_total=GetReportDataToArray(headers);

//--- Atualizamos todas as séries do gráfico com novos dados
   CColorGenerator m_generator;
   CGraphic *graph=m_graph1.GetGraphicPointer();
//--- Limpar o gráfico
   int total=graph.CurvesTotal();
   for(int i=total-1; i>=0; i--)
      graph.CurveRemoveByIndex(i);
//--- Máximo e mínimo do gráfico
   double y_max=0.0,y_min=m_symbol_balance[0].m_data[0];
//--- Adicionamos dados
   for(int i=0; i<curves_total; i++)
     {
      //--- Definimos o máximo/mínimo ao longo do eixo Y
      y_max=::fmax(y_max,m_symbol_balance[i].m_data[::ArrayMaximum(m_symbol_balance[i].m_data)]);
      y_min=::fmin(y_min,m_symbol_balance[i].m_data[::ArrayMinimum(m_symbol_balance[i].m_data)]);
      //--- Adicionamos uma série ao gráfico
      CCurve *curve=graph.CurveAdd(m_symbol_balance[i].m_data,m_generator.Next(),CURVE_LINES,headers[i]);
     }
//--- Número de valores e espaçamento de grade do eixo X
   m_data_total   =::ArraySize(m_symbol_balance[0].m_data)-1;
   m_default_step =(m_data_total<10)? 1 : ::MathFloor(m_data_total/5.0);
//--- Intervalo e recuos
   double range  =::fabs(y_max-y_min);
   double offset =range*0.05;
//--- Cor para a primeira série
   graph.CurveGetByIndex(0).Color(::ColorToARGB(clrCornflowerBlue));
//--- Propriedades do eixo horizontal
   CAxis *x_axis=graph.XAxis();
   x_axis.AutoScale(false);
   x_axis.Min(0);
   x_axis.Max(m_data_total);
   x_axis.MaxGrace(0);
   x_axis.MinGrace(0);
   x_axis.DefaultStep(m_default_step);
   x_axis.Name(from_date+" - "+to_date);
//--- Propriedades do eixo vertical
   CAxis *y_axis=graph.YAxis();
   y_axis.AutoScale(false);
   y_axis.Min(y_min-offset);
   y_axis.Max(y_max+offset);
   y_axis.MaxGrace(0);
   y_axis.MinGrace(0);
   y_axis.DefaultStep(range/10.0);
//--- Atualizar gráfico
   graph.CurvePlotAll();
   graph.Update();
  }

Para atualização do gráfico de rebaixamentos, usa-se o método CProgram::UpdateDrawdownGraph(). Como os dados são calculados no método CProgram::UpdateBalanceGraph(), eles precisam serem aplicados ao gráfico e atualizá-lo.

class CProgram : public CWndEvents
  {
private:
   //--- Atualiza os dados no gráfico de rebaixamento
   void              UpdateDrawdownGraph(void);
  };
//+------------------------------------------------------------------+
//| Atualizar o gráfico de rebaixamento                              |
//+------------------------------------------------------------------+
void CProgram::UpdateDrawdownGraph(void)
  {
//--- Atualizamos o gráfico de rebaixamento
   CGraphic *graph=m_graph2.GetGraphicPointer();
   CCurve *curve=graph.CurveGetByIndex(0);
   curve.Update(m_dd_x,m_dd_y);
   curve.PointsFill(false);
   curve.PointsSize(6);
   curve.PointsType(POINT_CIRCLE);
//--- Propriedades do eixo horizontal
   CAxis *x_axis=graph.XAxis();
   x_axis.AutoScale(false);
   x_axis.Min(0);
   x_axis.Max(m_data_total);
   x_axis.MaxGrace(0);
   x_axis.MinGrace(0);
   x_axis.DefaultStep(m_default_step);
//--- Atualizar gráfico
   graph.CalculateMaxMinValues();
   graph.CurvePlotAll();
   graph.Update();
  }

A chamada dos métodos CProgram::UpdateBalanceGraph() e CProgram::UpdateDrawdownGraph() é realizada no método CProgram::UpdateGraphs(). Antes de chamar esses métodos, primeiro, é chamado o método CProgram::ReadFileToArray(). Ele recebe os dados do arquivo com os resultados do último teste do EA. 

class CProgram : public CWndEvents
  {
private:
   //--- Atualiza os dados nos gráficos de resultados do último teste
   void              UpdateGraphs(void);
  };
//+------------------------------------------------------------------+
//| Atualizar gráficos                                               |
//+------------------------------------------------------------------+
void CProgram::UpdateGraphs(void)
  {
//--- Preenchemos a matriz de dados a partir do arquivo
   if(!ReadFileToArray())
     {
      ::Print(__FUNCTION__," > Could not open the test results file!");
      return;
     }
//--- Atualizar o gráfico de saldos e rebaixamentos
   UpdateBalanceGraph();
   UpdateDrawdownGraph();
  }

Apresentando o resultado

Para exibir os resultados do último teste nos gráficos da interface, é preciso pressionar apenas um botão. O evento dessa ação é processado no método CProgram::OnEvent():

//+------------------------------------------------------------------+
//| Manipulador de eventos                                           |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Evento de pressionado de botões
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- Pressionado do botão 'Update data'
      if(lparam==m_update_graph.Id())
        {
         //--- Atualizar gráficos
         UpdateGraphs();
         return;
        }
      //---
      return;
     }
  }

Se o EA já tiver sido testado antes de clicar no botão, veremos algo assim:

Fig. 3 – Resultado do último teste do EA. 

Fig. 3. Resultado do último teste do EA.

Por conseguinte, se o EA é carregado no gráfico, você pode, conferindo os resultados de muitos testes, após otimizar os parâmetros, ver imediatamente as mudanças no gráfico de saldo multissímbolo. 

Gráfico de saldo multissímbolo durante a negociação e testes

Agora, consideremos a segunda versão do EA, quer dizer, quando o gráfico multissímbolo do saldo é desenhado e atualizado durante o processo de negociação. 

A interface gráfica permanece quase igual à da versão descrita acima. A única diferença é que, em vez de um botão para atualização, haverá um calendário suspenso com o qual você pode especificar data a partir de qual exibir os resultados da negociação nos gráficos.

No método OnTrade(), vamos conferir, na chegada do evento, a alteração no histórico. O método CProgram::IsLastDealTicket() é usado para verificar que o histórico foi carregado com um novo trade. Nele obtemos o histórico a partir do tempo salvo na memória, após a última chamada. Em seguida, verificamos as boletas do último trade e a boleta armazenada na memória. Se as boletas forem diferentes, atualizamos, na memória, a boleta e a hora do último trade para a próxima verificação, e retornamos true, a fim de indicar que o histórico mudou.

class CProgram : public CWndEvents
  {
private:
   //--- Hora e boleta do último trae verificado
   datetime          m_last_deal_time;
   ulong             m_last_deal_ticket;
   //--- 
private:
   //--- Verificação de novo trade
   bool              IsLastDealTicket(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void) : m_last_deal_time(NULL),
                           m_last_deal_ticket(WRONG_VALUE)
  {
  }
//+------------------------------------------------------------------+
//| Retorna o evento do último trade no novo símbolo especificado    |
//+------------------------------------------------------------------+
bool CProgram::IsLastDealTicket(void)
  {
//--- Sair se o histórico não for recebido
   if(!::HistorySelect(m_last_deal_time,LONG_MAX))
      return(false);
//--- Obtemos o número de trades na lista
   int total_deals=::HistoryDealsTotal();
/--- Percorremos todos os trades na lista recebida, desde a última transação até a primeira
   for(int i=total_deals-1; i>=0; i--)
     {
      //--- Obtemos o boleto do trade
      ulong deal_ticket=::HistoryDealGetTicket(i);
      //--- Se as boletas forem iguais, sairemos
      if(deal_ticket==m_last_deal_ticket)
         return(false);
      //--- Se as boletas não forem iguais, informamos sobre isso
      else
        {
         datetime deal_time=(datetime)::HistoryDealGetInteger(deal_ticket,DEAL_TIME);
         //--- Lembramos a hora a boleta do último trade
         m_last_deal_time   =deal_time;
         m_last_deal_ticket =deal_ticket;
         return(true);
        }
     }
//--- Boletas de outro símbolo
   return(false);
  }

Antes de percorrer o histórico de trades e preencher as matrizes com dados, é preciso determinar quais e quantos símbolos estão no histórico. Isso é necessário para definir o tamanho das matrizes. Para fazer isso, usa-se o método CProgram::GetHistorySymbols(). Antes de chamá-lo, deve-se selecionar o histórico no intervalo desejado. Em seguida, adicionamos à lina os símbolos que encontramos no histórico. Para garantir que os símbolos na linha não se repitam, verificamos a presença da substring especificada. Depois disso adicionamos os símbolos encontrados no histórico à matriz e retornamos o número de símbolos.

class CProgram : public CWndEvents
  {
private:
   //--- Matriz de símbolos a partir do histórico
   string            m_symbols_name[];
   //--- 
private:
   //--- Obtemos os símbolos a partir do histórico da conta e retornamos seu número
   int               GetHistorySymbols(void);
  };
//+----------------------------------------------------------------------+
//| Obtemos os símbolos a partir do histórico e retornamos seu número    |
//+----------------------------------------------------------------------+
int CProgram::GetHistorySymbols(void)
  {
   string check_symbols="";
//--- Passamos, pela primeira vez, no ciclo e obtemos os símbolos de negociação
   int deals_total=::HistoryDealsTotal();
   for(int i=0; i<deals_total; i++)
     {
      //--- Obtemos o boleto do trade
      if(!m_deal_info.SelectByIndex(i))
         continue;
      //--- Se houver um nome de símbolo
      if(m_deal_info.Symbol()=="")
         continue;
      //--- Se não existir essa linha, adicionamo-la
      if(::StringFind(check_symbols,m_deal_info.Symbol(),0)==-1)
         ::StringAdd(check_symbols,(check_symbols=="")? m_deal_info.Symbol() : ","+m_deal_info.Symbol());
     }
//--- Obtemos os elementos da linha pelo separador
   ushort u_sep=::StringGetCharacter(",",0);
   int symbols_total=::StringSplit(check_symbols,u_sep,m_symbols_name);
//--- Retornamos o número de símbolos
   return(symbols_total);
  }

Para obter o saldo de multissímbolo, é preciso chamar o métodoCProgram::GetHistorySymbolsBalance():

class CProgram : public CWndEvents
  {
private:
   //--- Obtém o saldo geral e os saldos de todos os símbolos separadamente
   void              GetHistorySymbolsBalance(void);
  };
//+----------------------------------------------------------------------------+
//| Obtém o saldo geral e os saldos de todos os símbolos, separadamente        |
//+----------------------------------------------------------------------------+
void CProgram::GetHistorySymbolsBalance(void)
  {
   ...
  }

Aqui, no início, é preciso obter o saldo inicial da conta. Obtemos o histórico desde o primeiro trade que será este saldo inicial. Assumimos que é possível especificar no calendário a data a partir da qual você deseja mostrar o resultado da negociação. Por isso, escolhemos o histórico novamente. Em seguida, por meio do método CProgram::GetHistorySymbols(), obtemos os símbolos no histórico selecionado e sua quantidade, e, depois, definimos o tamanho para a matriz. Para exibir o intervalo de resultados do histórico, determinamos as datas de início e final

...
//--- Tamanho inicial do depósito
   ::HistorySelect(0,LONG_MAX);
   double balance=(m_deal_info.SelectByIndex(0))? m_deal_info.Profit() : 0;
//--- Obtemos o histórico a partir da data especificada
   ::HistorySelect(m_from_trade.SelectedDate(),LONG_MAX);
//--- Obtemos o número de símbolos
   int symbols_total=GetHistorySymbols();
//--- Liberamos as matrizes
   ::ArrayFree(m_dd_x);
   ::ArrayFree(m_dd_y);
//--- Definimos o tamanho da matriz de saldo de acordo com o número de símbolos + 1 para o saldo total
   ::ArrayResize(m_symbols_balance,(symbols_total>1)? symbols_total+1 : 1);
//--- Definimos o tamanho das matrizes de trades para cada símbolo
   int deals_total=::HistoryDealsTotal();
   for(int s=0; s<=symbols_total; s++)
     {
      if(symbols_total<2 && s>0)
         break;
      //---
      ::ArrayResize(m_symbols_balance[s].m_data,deals_total);
      ::ArrayInitialize(m_symbols_balance[s].m_data,0);
     }
//--- Número de saldos
   int balances_total=::ArraySize(m_symbols_balance);
//--- Começo e fim do histórico
   m_begin_date =(m_deal_info.SelectByIndex(0))? m_deal_info.Time() : m_from_trade.SelectedDate();
   m_end_date   =(m_deal_info.SelectByIndex(deals_total-1))? m_deal_info.Time() : ::TimeCurrent();
...

No próximo ciclo, são calculados os saldos dos símbolos e os rebaixamentos. Os dados recebidos são colocados em matrizes. Para o cálculo de rebaixamento, neste caso, também são usado os métodos descritos nas seções anteriores.

...
//--- Rebaixamento máximo
   double max_drawdown=0.0;
//--- Escrevemos as matrizes de saldo na matriz transferida
   for(int i=0; i<deals_total; i++)
     {
      //--- Obtemos o boleto do trade
      if(!m_deal_info.SelectByIndex(i))
         continue;
      //--- Inicialização no primeiro trade
      if(i==0 && m_deal_info.DealType()==DEAL_TYPE_BALANCE)
         balance=0;
      //--- A partir da data especificada
      if(m_deal_info.Time()>=m_from_trade.SelectedDate())
        {
         //--- Calculamos o saldo total
         balance+=m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
         m_symbols_balance[0].m_data[i]=balance;
         //--- Calculamos o rebaixamento
         if(MaxDrawdownToString(i,balance,max_drawdown)!="")
            AddDrawDown(i,max_drawdown);
        }
      //--- Se mais de um símbolo estiver envolvido, escrevemos os valores do seu saldo
      if(symbols_total<2)
         continue;
      //--- Apenas a partir da data selecionada
      if(m_deal_info.Time()<m_from_trade.SelectedDate())
         continue;
      //--- Passamos por todos os símbolos
      for(int s=1; s<balances_total; s++)
        {
         int prev_i=i-1;
         // --- Se o tipo de trade for "Cálculo do saldo " (a primeira transação) ...
         if(prev_i<0 || m_deal_info.DealType()==DEAL_TYPE_BALANCE)
           {
            //--- ... para todos os símbolos, o saldo é o mesmo
            m_symbols_balance[s].m_data[i]=balance;
            continue;
           }
         //--- Se os símbolos forem iguais e o resutado do trade for diferente de zero
         if(m_deal_info.Symbol()==m_symbols_name[s-1] && m_deal_info.Profit()!=0)
           {
            //--- Exibimos o trade, no saldo, com este símbolo. Vamos levar em conta o swap e a comissão.
            m_symbols_balance[s].m_data[i]=m_symbols_balance[s].m_data[prev_i]+m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
           }
         //--- Caso contrário, escrevemos o valor anterior
         else
            m_symbols_balance[s].m_data[i]=m_symbols_balance[s].m_data[prev_i];
        }
     }
...

Os dados são adicionados aos gráficos e atualizados usando os métodos CProgram::UpdateBalanceGraph() e CProgram::UpdateDrawdownGraph(). Seu código é quase o mesmo da primeira versão do EA discutido nas seções anteriores, por isso passapmos para a parte em que são chamados.

Em primeiro lugar, esses métodos são chamados ao criar uma interface gráfica, para que o usuário veja imediatamente o resultado da negociação. Após isso, os gráficos são atualizados ao ocorrerem eventos de negociação no método OnTrade()

class CProgram : public CWndEvents
  {
private:
   //--- Inicialização de gráficos
   void              UpdateBalanceGraph(const bool update=false);
   void              UpdateDrawdownGraph(void);
  };
//+------------------------------------------------------------------+
//| Evento de operação de negociação                                 |
//+------------------------------------------------------------------+
void CProgram::OnTradeEvent(void)
  {
//--- Atualização de gráficos de saldo e rebaixamento
   UpdateBalanceGraph();
   UpdateDrawdownGraph();
  }

Além disso, o usuário na interface gráfica pode especificar a data a partir da qual deseja construir os gráficos de saldo. Para forçar a atualização do gráfico sem verificar a última boleta do trade, para o método CProgram::UpdateBalanceGraph() você precisa transferir o valor true.

Evento de alteração de data no calendário (ON_CHANGE_DATE) é processado assim:

//+------------------------------------------------------------------+
//| Manipulador de eventos                                           |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Eventos escolha de data no calendário
   if(id==CHARTEVENT_CUSTOM+ON_CHANGE_DATE)
     {
      if(lparam==m_from_trade.Id())
        {
         UpdateBalanceGraph(true);
         UpdateDrawdownGraph();
         m_from_trade.ChangeComboBoxCalendarState();
        }
      //---
      return;
     }
  }

Veja como funciona no testador no modo de visualização:

Fig. 4 – Resultado no testador no modo de visualização.

Fig. 4. Resultado no testador, no modo de visualização.

Visualizamos relatórios do serviço Sinais

Como outro complemento que pode ser útil para os usuários, criaremos um EA com o qual será possível visualizar os resultados da negociação a partir dos relatórios no serviço Sinais.

Vá para a página do sinal de seu interesse e selecione a guia Histórico de transações:

Fig. 5. Histórico de trades do sinal.

O link para baixar o arquivo CSV com histórico de transações pode ser encontrado na parte inferior desta lista:

 Fig. 6 – Exportação do histórico de transações para um arquivo CSV.

Fig. 6 Exportação do histórico de trades para um arquivo CSV.

Esses arquivos, para a implementação do EA, devem ser colocados no diretório do terminal, no caminho \MQL5\Files. Adicionamos um parâmetro externo ao Expert Advisor. Ele especificará o nome do arquivo de relatório cujos dados devem ser visualizados nos gráficos.

//+------------------------------------------------------------------+
//|                                                      Program.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                               http://www.mql5.com |
//+------------------------------------------------------------------+
//--- Parâmetros externos
input string PathToFile=""; // Path to file
...

Fig. 7 – Parâmetro externo para especificar o arquivo de relatório.

Fig. 7. Parâmetro externo para especificar o arquivo de relatório.

Na interface gráfica desta versão do EA, haverá apenas dois gráficos. Durante o carregamento do EA no gráfico, no terminal, ele tenta abrir o arquivo especificado nas configurações. Se esse arquivo não for encontrado, o programa exibirá uma mensagem no Diário. O conjunto de métodos aqui é aproximadamente o mesmo que nas versões acima. Em alguns lugares existem pequenas diferenças, mas basicamente o princípio é o mesmo. Consideramos apenas os métodos em que a abordagem tenha mudado significativamente.

Assim, o arquivo é lido e linhas são movidas para a matriz, para os dados de origem. Agora, é preciso distribuir esses dados numa matriz bidimensional, como é feito nas tabelas. Isso é necessário para a classificação conveniente de dados no horário de abertura dos trades, desde o início até o final. Para isso, precisamos de uma matriz separada de matrizes. 

//--- Matrizes para dados a partir do arquivo
struct CReportTable
  {
   string            m_rows[];
  };
//+------------------------------------------------------------------+
//| Classe para criar o aplicativo                                   |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Tabela do relatório
   CReportTable      m_columns[];
   //--- Número de linhas e colunas
   uint              m_rows_total;
   uint              m_columns_total;
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void) : m_rows_total(0),
                           m_columns_total(0)
  {
...
  }

Para classificar a matriz de matrizes, você precisa dos seguintes métodos:

class CProgram : public CWndEvents
  {
private:
   //--- Método de classificação rápida
   void              QuickSort(uint beg,uint end,uint column);
   //--- Verificação da condição de classificação
   bool              CheckSortCondition(uint column_index,uint row_index,const string check_value,const bool direction);
   //--- Alterar os valores nas células especificadas, em lugares
   void              Swap(uint r1,uint r2);
  };

Todos esses métodos foram discutidos em detalhes num dos artigos anteriores

Todas as operações básicas são realizadas no método CProgram::GetData(). Iremos considerá-lo mais detalhadamente. 

class CProgram : public CWndEvents
  {
private:
   //--- Obtemos os dados em matrizes
   int               GetData(void);
  };
//+------------------------------------------------------------------+
//| Recebemos os dados dos símbolos a partir do relatório            |
//+------------------------------------------------------------------+
int CProgram::GetData(void)
  {
...
  }

Primeiro, definimos o número de linhas e elementos da linha pelo separador ';'. Em seguida, obtemos, numa matriz separada, os nomes de símbolos que estão no relatório e seu número. Depois, temos que preparar as matrizes e preenchê-las com os dados do relatório.

...
//--- Obtemos os elementos da linha de cabeçalho
   string str_elements[];
   ushort u_sep=::StringGetCharacter(";",0);
   ::StringSplit(m_source_data[0],u_sep,str_elements);
//--- Número de linas e elementos de linha
   int strings_total  =::ArraySize(m_source_data);
   int elements_total =::ArraySize(str_elements);
//--- Obtemos os símbolos
   if((m_symbols_total=GetHistorySymbols())==WRONG_VALUE)
     return;
//--- Liberamos as matrizes
   ::ArrayFree(m_dd_y);
   ::ArrayFree(m_dd_x);
//--- Tamanho da série de dados
   ::ArrayResize(m_columns,elements_total);
   for(int i=0; i<elements_total; i++)
      ::ArrayResize(m_columns[i].m_rows,strings_total-1);
//--- Preenchemos as matrizes com dados do arquivo
   for(int r=0; r<strings_total-1; r++)
     {
      ::StringSplit(m_source_data[r+1],u_sep,str_elements);
      for(int c=0; c<elements_total; c++)
         m_columns[c].m_rows[r]=str_elements[c];
     }
...

Tudo está pronto para ordenar os dados. Aqui, é necessário definir o tamanho das matrizes de saldos de símbolos, antes de preenchê-las:

...
//--- Número de linhas e colunas
   m_rows_total    =strings_total-1;
   m_columns_total =elements_total;
//--- Classificamos pela hora na primeira coluna
   QuickSort(0,m_rows_total-1,0);
//--- Tamanho da série
   ::ArrayResize(m_symbol_balance,m_symbols_total);
   for(int i=0; i<m_symbols_total; i++)
      ::ArrayResize(m_symbol_balance[i].m_data,m_rows_total);
...

Em seguida, primeiro, preenchemos a matriz geral de saldo e rebaixamentos. Todos os trades relacionados ao reabastecimento do depósito serão omitidos.

...
//--- Saldo e rebaixamento máximo
   double balance      =0.0;
   double max_drawdown =0.0;
//--- Obtemos os dados do salgo total
   for(uint i=0; i<m_rows_total; i++)
     {
      //--- Saldo inicial
      if(i==0)
        {
         balance+=(double)m_columns[elements_total-1].m_rows[i];
         m_symbol_balance[0].m_data[i]=balance;
        }
      else
        {
         //--- Ignoramos o reabastecimento
         if(m_columns[1].m_rows[i]=="Balance")
            m_symbol_balance[0].m_data[i]=m_symbol_balance[0].m_data[i-1];
         else
           {
            balance+=(double)m_columns[elements_total-1].m_rows[i]+(double)m_columns[elements_total-2].m_rows[i]+(double)m_columns[elements_total-3].m_rows[i];
            m_symbol_balance[0].m_data[i]=balance;
           }
        }
      //--- Calculamos o rebaixamento
      if(MaxDrawdownToString(i,balance,max_drawdown)!="")
         AddDrawDown(i,max_drawdown);
     }
...

Em seguida, preenchemos as matrizes de saldos para cada símbolo individual. 

...
//--- Obtemos os dados dos saldos dos símbolos
   for(int s=1; s<m_symbols_total; s++)
     {
      //--- Saldo inicial
      balance=m_symbol_balance[0].m_data[0];
      m_symbol_balance[s].m_data[0]=balance;
      //---
      for(uint r=0; r<m_rows_total; r++)
        {
         //--- Se símbolos não corresponderem, então o valor anterior
         if(m_symbols_name[s]!=m_columns[m_symbol_index].m_rows[r])
           {
            if(r>0)
               m_symbol_balance[s].m_data[r]=m_symbol_balance[s].m_data[r-1];
            //---
            continue;
           }
         //--- Se o resultado do trade for diferente de zero
         if((double)m_columns[elements_total-1].m_rows[r]!=0)
           {
            balance+=(double)m_columns[elements_total-1].m_rows[r]+(double)m_columns[elements_total-2].m_rows[r]+(double)m_columns[elements_total-3].m_rows[r];
            m_symbol_balance[s].m_data[r]=balance;
           }
         //--- Caso contrário, escrevemos o valor anterior
         else
            m_symbol_balance[s].m_data[r]=m_symbol_balance[s].m_data[r-1];
        }
     }
...

Depois disso, os dados são exibidos nos gráficos da interface gráfica. Veja alguns exemplos de diferentes provedores de sinais:

 Fig. 8 – Apresentação de resultados (exemplo 1).

Fig. 8.  Apresentação de resultados (exemplo 1).

 Fig. 9 – Apresentação de resultados (exemplo 2).

Fig. 9.  Apresentação de resultados (exemplo 2).

 Fig. 10 – Apresentação de resultados (exemplo 3).

Fig. 10. Apresentação de resultados (exemplo 3).

 Fig. 11 – Apresentação de resultados (exemplo 4).

Fig. 11.  Apresentação de resultados (exemplo 4).

Fim do artigo

O artigo mostra uma versão moderna do aplicativo MQL para visualizar gráficos de saldo multissímbolo. Anteriormente, você tinha que usar programas de terceiros para obter esse resultado. Agora tudo pode ser implementado apenas com ajuda de MQL, sem deixar o terminal MetaTrader 5.

Abaixo, você pode baixar arquivos para seu computador, para testar e estudar detalhadamente o código apresentado no artigo. Cada versão do programa tem essa estrutura de arquivos: 

Nome do arquivo Comentário
MacdSampleMultiSymbols.mq5 Expert Advisor modificado da entrega padrão - MACD Sample
Program.mqh Arquivo com a classe do programa
CreateGUI.mqh Arquivo com a implementação dos métodos da classe do programa, no arquivo Program.mqh
Strategy.mqh Arquivo com a classe modificada de estratégia do MACD Sample (versão multissímbolo)
FormatString.mqh Arquivo com funções auxiliares para formatação de linhas