Trabalhemos com os resultados da otimização através da interface gráfica do usuário

Anatoli Kazharski | 9 maio, 2018


Sumário

Introdução

Continuamos a desenvolver o tópico sobre o processamento e análise de resultados de otimização. O artigo anterior mostrava como visualizar os resultados da otimização através da interface gráfica do aplicativo MQL5. Desta vez, complicaremos a tarefa, isto é, devemos escolher os 100 melhores resultados de otimização e exibí-los na tabela da interface gráfica. 

Além disso, continuaremos a desenvolver o tópico sobre gráficos de saldo multissímbolos que também foi apresentado num artigo separado. Combinamos as ideias desses dois artigos e fazemos com que o usuário, selecionando uma série na tabela de resultados de otimização, receba um gráfico multissímbolo de saldo e rebaixamento, em gráficos separados. Assim, após otimizar os parâmetros do EA, o trader poderá analisar e selecionar os resultados do seu interesse mais rapidamente.

Desenvolvimento da interface gráfica

A GUI do EA de teste consistirá nos seguintes elementos.

  • Formulário para controles
  • Barra de status para mostrar informações adicionais finais
  • Guias para a distribuição de elementos por grupo:
    • Frames
      • Campo de entrada para gerenciar o número de saldos de resultados exibidos durante a rolagem dos resultados após a otimização
      • Atraso em milissegundos durante a rolagem dos resultados
      • Botão para iniciar a rolagem dos resultados novamente
      • Gráfico para exibir o número especificado de saldos de resultados
      • Gráfico para exibir todos os resultados
    • Results
      • Tabela de melhores resultados
    • Balance
      • Gráfico para exibir o saldo multissímbolo do resultado selecionado na tabela
      • Gráfico para exibir os rebaixamento do resultado selecionado na tabela
  • Indicador para o processo de reprodução de quadros

O código dos métodos para criação dos elementos, apresentados na lista acima, é colocado num arquivo separado e é conectado a um arquivo com a classe de um programa MQL:

//+------------------------------------------------------------------+
//| Classe para criar o aplicativo                                   |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Janela
   CWindow           m_window1;
   //--- Barra de Status
   CStatusBar        m_status_bar;
   //--- Guias
   CTabs             m_tabs1;
   //--- Caixa de edição
   CTextEdit         m_curves_total;
   CTextEdit         m_sleep_ms;
   //--- Botões
   CButton           m_reply_frames;
   //--- Gráficos
   CGraph            m_graph1;
   CGraph            m_graph2;
   CGraph            m_graph3;
   CGraph            m_graph4;
   //--- Tabelas
   CTable            m_table_param;
   //--- Barra de progresso
   CProgressBar      m_progress_bar;
   //---
public:
   //--- Cria uma interface gráfica
   bool              CreateGUI(void);
   //---
private:
   //--- Formulário
   bool              CreateWindow(const string text);
   //--- Barra de Status
   bool              CreateStatusBar(const int x_gap,const int y_gap);
   //--- Guias
   bool              CreateTabs1(const int x_gap,const int y_gap);
   //--- Caixa de edição
   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);
   bool              CreateGraph3(const int x_gap,const int y_gap);
   bool              CreateGraph4(const int x_gap,const int y_gap);
   //--- Botões
   bool              CreateUpdateGraph(const int x_gap,const int y_gap,const string text);
   //--- Tabelas
   bool              CreateMainTable(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 criar controles                                     |
//+------------------------------------------------------------------+
#include "CreateGUI.mqh"
//+------------------------------------------------------------------+

Na tabela, como eu disse acima, serão exibidos 100 melhores resultados de otimização (no que diz respeito ao maior lucro total). Como a GUI é criada antes do início da otimização, a tabela está inicialmente vazia. Na classe de processamento de quadros de otimização, vamos definir o número de colunas e o texto para os cabeçalhos.

Criamos a tabela com o seguinte conjunto de funções.

  • Exibição de cabeçalhos
  • Capacidade de classificação
  • Alocação de série
  • Alocação da série selecionada (sem a possibilidade de desmarcar)
  • Alteração da largura da coluna manualmente
  • Formato no estilo de "zebra"

O código para criar a tabela é mostrado abaixo. Para a tabela ser atribuída à segunda guia, é preciso transferir o objeto de tabela para o objeto da guia, especificando o índice da guia. Neste caso, para a tabela, a classe principal é o elemento "Guias". Assim, ao alterar o tamanho da área da guia, o tamanho da tabela é alterado em relação ao seu elemento principal, desde que seja especificado nas propriedades do membro "Tabela".

//+------------------------------------------------------------------+
//| Cria a tabela principal                                          |
//+------------------------------------------------------------------+
bool CProgram::CreateMainTable(const int x_gap,const int y_gap)
  {
//--- Criamos o ponteiro para o elemento principal
   m_table_param.MainPointer(m_tabs1);
//--- Fixar na guia
   m_tabs1.AddToElementsArray(1,m_table_param);
//--- Propriedades
   m_table_param.TableSize(1,1);
   m_table_param.ShowHeaders(true);
   m_table_param.IsSortMode(true);
   m_table_param.SelectableRow(true);
   m_table_param.IsWithoutDeselect(true);
   m_table_param.ColumnResizeMode(true);
   m_table_param.IsZebraFormatRows(clrWhiteSmoke);
   m_table_param.AutoXResizeMode(true);
   m_table_param.AutoYResizeMode(true);
   m_table_param.AutoXResizeRightOffset(2);
   m_table_param.AutoYResizeBottomOffset(2);
//--- Criamos um elemento de controle
   if(!m_table_param.CreateTable(x_gap,y_gap))
      return(false);
//--- Adicionamos um objeto à matriz comum de grupos de objetos
   CWndContainer::AddToElementsArray(0,m_table_param);
   return(true);
  }

Salvando resultados de otimização

Para trabalhar com os resultados da otimização, é implementada a classe CFrameGenerator. Nós vamos pegar a versão do artigo Visualizando a otimização da estratégia de negociação na MetaTrader 5, modificamo-la e adicionamos os métodos necessários. Nos quadros, precisaremos armazenar não apenas o saldo geral e as estatísticas finais, mas também o saldo e o rebaixamento do depósito para cada símbolo. Para armazenar os saldos, usaremos uma estrutura de matriz separada, nomeadamente, CSymbolBalance. Ela tem um duplo propósito. Em suas matrizes, serão armazenados os dados que serão transferidos para o quadro, na matriz geral. Em seguida, após a otimização, os dados serão extraídos da matriz de quadro e transferidos de volta para as matrizes desta estrutura, para serem exibidos nos gráficos de saldo multissímbolos.

//--- Matrizes para os saldos de todos os símbolos
struct CSymbolBalance
  {
   double            m_data[];
  };
//+------------------------------------------------------------------+
//| Classe para trabalhar com os resultados da otimização            |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   //--- Estrutura dos saldos
   CSymbolBalance    m_symbols_balance[];
  };

Como parâmetro de string, a enumeração de símbolos será transferida para o quadro através do separador ','. Inicialmente, era suposto armazenar os dados num quadro, como um relatório completo numa matriz de strings. Mas, por enquanto, as matrizes de string não podem ser transferidas para o quadro. Ao tentar transferir uma matriz de tipo string para a função FrameAdd(), durante a compilação, surgirá a mensagem de erro: "matrizes de strings e estruturas contendo objetos não são permitidas."

string arrays and structures containing objects are not allowed

Outra opção é salvar o relatório num arquivo e enviá-lo para o quadro. Mas essa variante também não nos convém, pois teríamos que salvar os resultados com muita frequência num disco rígido.

É por isso que decidi coletar todos os dados necessários numa matriz e, depois, extraí-los, com base nas chaves contidas nos parâmetros do quadro. No início desta matriz, haverá indicadores estatísticos. Depois, os dados do saldo total e, em seguida, o saldo de cada símbolo, separadamente. No final, haverá dados de rebaixamento para os dois eixos, separadamente. 

O diagrama abaixo mostra a ordem em que serão empacotados os dados na matriz. Por uma questão de brevidade, é mostrada uma variante de dois símbolos.

 


Fig. 1. Sequência de disposição de dados na matriz.

Para determinar os índices de cada intervalo, nesta matriz, como mencionado acima, é preciso de chaves. O número de indicadores estatísticos é constante e determinado com antecedência. Nesse caso, exibiremos cinco indicadores e um número de passagem na tabela, para garantir que os dados desse resultado possam ser acessados ​​após a otimização:

//--- Número de indicadores estatísticos
#define STAT_TOTAL 6

A quantidade de dados de saldo - total e separadamente - será a mesma para cada símbolo. Este valor será enviado para a função FrameAdd(), como parâmetro double. Para determinar quais símbolos participaram do teste, a cada passagem na função OnTester (), vamos determiná-los no histórico de transações. Esta informação será enviada para a função FrameAdd(), como um parâmetro de string.

::FrameAdd(m_report_symbols,1,data_count,stat_data);

O sentenciamento dos símbolos especificados no parâmetro de string é o mesmo que a sequência de dados na matriz. Assim, tendo todos esses parâmetros, podem-se extrair todos os dados empacotados na matriz, sem confundir nada. 

A listagem de código abaixo mostra o método CFrameGenerator::GetHistorySymbols() que é projetado para determinar os símbolos no histórico de transações:

#include <Trade\DealInfo.mqh>
//+------------------------------------------------------------------+
//| Classe para trabalhar com os resultados da otimização            |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   //--- Trabalho com transações
   CDealInfo         m_deal_info;
   //--- Símbolos do relatório
   string            m_report_symbols;
   //---
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 CFrameGenerator::GetHistorySymbols(void)
  {
//--- 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(m_report_symbols,m_deal_info.Symbol(),0)==-1)
         ::StringAdd(m_report_symbols,(m_report_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(m_report_symbols,u_sep,m_symbols_name);
//--- Retornamos o número de símbolos
   return(symbols_total);
  }

Se houver mais de um símbolo no histórico de transações, o tamanho da matriz será definido acrescentando mais um elemento. O primeiro elemento é reservado para o saldo total. 

//--- Definimos o tamanho da matriz de saldos pelo número de símbolos + 1 para o saldo total
   ::ArrayResize(m_symbols_balance,(m_symbols_total>1)? m_symbols_total+1 : 1);

Depois que os dados do histórico de transações são armazenados em matrizes separadas, eles precisam ser colocados numa matriz comum. Para fazer isso, é usado o método CFrameGenerator::CopyDataToMainArray(). Aqui, sequencialmente, no ciclo, aumentamos a matriz comum o número de dados a serem adicionados, e, na última iteração, copiamos os dados de rebaixamento.

class CFrameGenerator
  {
private:
   //--- Saldo do resultado
   double            m_balances[];
   //---
private:
   //--- Copia os dados dos saldos para a matriz principal
   void              CopyDataToMainArray(void);
  };
//+------------------------------------------------------------------+
//| Copia os dados do saldo para a matriz principal                  |
//+------------------------------------------------------------------+
void CFrameGenerator::CopyDataToMainArray(void)
  {
//--- Número de saldos
   int balances_total=::ArraySize(m_symbols_balance);
//--- Tamanho da matriz de saldo
   int data_total=::ArraySize(m_symbols_balance[0].m_data);
//--- Preenchemos a matriz comum com dados
   for(int i=0; i<=balances_total; i++)
     {
      //--- Tamanho atual da matriz
      int array_size=::ArraySize(m_balances);
      //--- Copiamos para a matriz os saldos
      if(i<balances_total)
        {
         //--- Copiamos o saldo para a matriz
         ::ArrayResize(m_balances,array_size+data_total);
         ::ArrayCopy(m_balances,m_symbols_balance[i].m_data,array_size);
        }
      //--- Copiamos para a matriz os rebaixamentos
      else
        {
         data_total=::ArraySize(m_dd_x);
         ::ArrayResize(m_balances,array_size+(data_total*2));
         ::ArrayCopy(m_balances,m_dd_x,array_size);
         ::ArrayCopy(m_balances,m_dd_y,array_size+data_total);
        }
     }
  }

Indicadores estatísticos são adicionados ao início da matriz comum no método CFrameGenerator::GetStatData(). Neste método, por referência, é transferida uma matriz que eventualmente será armazenada no quadro. Ele define o tamanho da matriz de dados de saldo e o número de indicadores estatísticos. Os dados dos saldos são colocados a partir do último índice no intervalo dos indicadores estatísticos. 

class CFrameGenerator
  {
private:
   //--- Obtém os dados estatísticos
   void              GetStatData(double &dst_array[],double on_tester_value);
  };
//+------------------------------------------------------------------+
//| Obtém os dados estatísticos                                      |
//+------------------------------------------------------------------+
void CFrameGenerator::GetStatData(double &dst_array[],double on_tester_value)
  {
//--- Copiar a matriz
   ::ArrayResize(dst_array,::ArraySize(m_balances)+STAT_TOTAL);
   ::ArrayCopy(dst_array,m_balances,STAT_TOTAL,0);
//--- Preenchemos os primeiros valores da matriz (STAT_TOTAL) com os resultados do teste
   dst_array[0] =0;                                             // número da passagem
   dst_array[1] =on_tester_value;                               // valor do critério de otimização personalizado
   dst_array[2] =::TesterStatistics(STAT_PROFIT);               // lucro líquido
   dst_array[3] =::TesterStatistics(STAT_TRADES);               // número de transações
   dst_array[4] =::TesterStatistics(STAT_EQUITY_DDREL_PERCENT); // rebaixamento máximo de fundos em porcentagem
   dst_array[5] =::TesterStatistics(STAT_RECOVERY_FACTOR);      // fator de recuperação
  }

Como resultado, as ações descritas acima são realizadas no método CFrameGenerator::OnTesterEvent() que é chamado no arquivo principal do programa, na função OnTester()

//+---------------------------------------------------------------------------+
//| Prepara uma matriz de valores de saldo e envia-a para o quadro            |
//| A função deve ser chamada no Expert Advisor, no manipulador OnTester()    |
//+---------------------------------------------------------------------------+
void CFrameGenerator::OnTesterEvent(const double on_tester_value)
  {
//--- Obtemos os dados do saldo
   int data_count=GetBalanceData();
//--- Matriz para enviar dados para o quadro
   double stat_data[];
   GetStatData(stat_data,on_tester_value);
//--- Criamos o quadro com os dados e o enviamos para o terminal
   if(!::FrameAdd(m_report_symbols,1,data_count,stat_data))
      ::Print(__FUNCTION__," > Frame add error: ",::GetLastError());
   else
      ::Print(__FUNCTION__," > Frame added, OK");
  }

As matrizes da tabela serão preenchidas, no final da otimização, no método FinalRecalculateFrames() que é chamado no método CFrameGenerator::OnTesterDeinitEvent(). Aqui, é realizado o recálculo final dos resultados de otimização, é determinado o número de parâmetros otimizados, é preenchida a matriz de cabeçalhos da tabela, são coletados os dados em matrizes de tabelas. Depois disso, os dados são classificados de acordo com os critérios especificados. 

Consideremos alguns métodos auxiliares que serão chamados no ciclo final de processamento de quadros. Comecemos com o método CFrameGenerator::GetParametersTotal() que determina o número de parâmetros do Expert Advisor envolvido na otimização.

Para obter os parâmetros do Expert Advisor do quadro, é chamada a função FrameInputs(). Passando o número de passagem para esta função, obtemos uma matriz de parâmetros e seu número. Em sua lista, todos os que participaram da otimização são os primeiros e, depois, todos os outros. Como, na tabela, só serão mostrados parâmetros otimizados, é necessário determinar o índice do primeiro parâmetro não otimizado para cortar o grupo que não deve entrar na tabela. No nosso caso, pode-se especificar antecipadamente o primeiro parâmetro externo não otimizado do Expert Advisor que orientará o programa. Neste caso, trata-se de Symbols. Tendo definido o índice, pode-se calcular o número de parâmetros otimizados do Expert Advisor.

class CFrameGenerator
  {
private:
   //--- Primeiro parâmetro não otimizado
   string            m_first_not_opt_param;
   //---
private:
   //--- Obtém o número de parâmetros otimizados
   void              GetParametersTotal(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CFrameGenerator::CFrameGenerator(void) : m_first_not_opt_param("Symbols")
  {
  }
//+------------------------------------------------------------------+
//| Obtém o número de parâmetros otimizados                          |
//+------------------------------------------------------------------+
void CFrameGenerator::GetParametersTotal(void)
  {
//--- No primeiro quadro, definimos o número de parâmetros otimizados
   if(m_frames_counter<1)
     {
      //--- Obtemos os parâmetros de entrada do Expert Advisor, para os quais é gerado o quadro
      ::FrameInputs(m_pass,m_param_data,m_par_count);
      //--- Encontramos o índice do primeiro parâmetro não otimizável
      int limit_index=0;
      int params_total=::ArraySize(m_param_data);
      for(int i=0; i<params_total; i++)
        {
         if(::StringFind(m_param_data[i],m_first_not_opt_param)>-1)
           {
            limit_index=i;
            break;
           }
        }
      //--- Número de parâmetros a serem otimizados
      m_param_total=(m_par_count-(m_par_count-limit_index));
     }
  }

Os dados da tabela serão armazenados na estrutura de matriz CReportTable. Depois que já sabemos o número de parâmetros otimizados do Expert Advisor, torna-se possível determinar e definir o número de colunas da tabela. Isso é feito no método CFrameGenerator::SetColumnsTotal(). Inicialmente, o número de linhas é zero

//--- Matrizes de tabela
struct CReportTable
  {
   string            m_rows[];
  };
//+------------------------------------------------------------------+
//| Classe para trabalhar com os resultados da otimização            | 
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   //--- Tabela do relatório
   CReportTable      m_columns[];
   //---
private:
   //--- Definimos o número de colunas na tabela
   void              SetColumnsTotal(void);
  };
//+------------------------------------------------------------------+
//| Definindo o número de colunas na tabela                          |
//+------------------------------------------------------------------+
void CFrameGenerator::SetColumnsTotal(void)
  {
//--- Definimos o número de colunas para a tabela de resultados
   if(m_frames_counter<1)
     {
      int columns_total=int(STAT_TOTAL+m_param_total);
      ::ArrayResize(m_columns,columns_total);
      for(int i=0; i<columns_total; i++)
         ::ArrayFree(m_columns[i].m_rows);
     }
  }

Séries são adicionadas no método CFrameGenerator::AddRow(). Durante a pesquisa detalhada de quadros, apenas aqueles resultados em que existem transações entrarão na tabela. Nas primeiras colunas da tabela, começando com o número de passagem, estarão localizados os indicadores estatísticos e, em seguida, os parâmetros otimizados do Expert Advisor. Quando os parâmetros são recebidos do quadro, eles são exibidos no formato "parameterN=valueN" [nome do parâmetro][delimitador][valor do parâmetro]. Precisamos apenas dos valores dos parâmetros que devem entrar na tabela. Portanto, dividimos a linha usando o separador ‘=’ e armazenamos o valor do segundo elemento da matriz.

class CFrameGenerator
  {
private:
   //--- Adiciona uma série de dados
   void              AddRow(void);
  };
//+------------------------------------------------------------------+
//| Adiciona uma série de dados                                      |
//+------------------------------------------------------------------+
void CFrameGenerator::AddRow(void)
  {
//--- Definimos o número de colunas na tabela
   SetColumnsTotal();
//--- Sair se não houver transações
   if(m_data[3]<1)
      return;
//--- Preenchemos a tabela
   int columns_total=::ArraySize(m_columns);
   for(int i=0; i<columns_total; i++)
     {
      //--- Adicionamos uma linha
      int prev_rows_total=::ArraySize(m_columns[i].m_rows);
      ::ArrayResize(m_columns[i].m_rows,prev_rows_total+1,RESERVE);
      //--- Número de passagem
      if(i==0)
        {
         m_columns[i].m_rows[prev_rows_total]=string(m_pass);
         continue;
        }
      //--- Indicadores estatísticos
      if(i<STAT_TOTAL)
         m_columns[i].m_rows[prev_rows_total]=string(m_data[i]);
      //--- Parâmetros otimizados do Expert Advisor
      else
        {
         string array[];
         if(::StringSplit(m_param_data[i-STAT_TOTAL],'=',array)==2)
            m_columns[i].m_rows[prev_rows_total]=array[1];
        }
     }
  }

Tomamos os cabeçalhos para a tabela no método separado CFrameGenerator::GetHeaders(), mas apenas o primeiro elemento da matriz de elementos da linha dividida:

class CFrameGenerator
  {
private:
   //--- Obtém os cabeçalhos para a tabela
   void              GetHeaders(void);
  };
//+------------------------------------------------------------------+
//| Recebe os cabeçalhos para a tabela                               |
//+------------------------------------------------------------------+
void CFrameGenerator::GetHeaders(void)
  {
   int columns_total =::ArraySize(m_columns);
//--- Cabeçalhos
   ::ArrayResize(m_headers,STAT_TOTAL+m_param_total);
   for(int c=STAT_TOTAL; c<columns_total; c++)
     {
      string array[];
      if(::StringSplit(m_param_data[c-STAT_TOTAL],'=',array)==2)
         m_headers[c]=array[0];
     }
  }

A fim de especificar, para o programa, por qual critério é necessário selecionar na tabela 100 resultados de otimização, utilizamos o método CFrameGenerator::ColumnSortIndex(). O índice da coluna é passado para ele. Após a otimização, a tabela de resultados será classificada por este índice, e os 100 melhores resultados serão incluídos na tabela para exibição na interface gráfica. Por padrão, é definida uma terceira coluna (índice 2), ou seja, a classificação será baseada no lucro máximo.

class CFrameGenerator
  {
private:
   //--- Índice da coluna classificada
   uint              m_column_sort_index;
   //---
public:
   //--- Definição do índice da coluna pelo qual será ordenada a tabela
   void              ColumnSortIndex(const uint index) { m_column_sort_index=index; }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CFrameGenerator::CFrameGenerator(void) : m_column_sort_index(2)
  {
  }

Se quisermos selecionar os resultados, segundo outro critério, o método CFrameGenerator::ColumnSortIndex() deve ser chamado no métodoCProgram::OnTesterInitEvent() no início da otimização:

//+------------------------------------------------------------------+
//| Evento de início do processo de otimização                       |
//+------------------------------------------------------------------+
void CProgram::OnTesterInitEvent(void)
  {
...
   m_frame_gen.ColumnSortIndex(3);
...
  }

Como resultado, o método CFrameGenerator::FinalRecalculateFrames() para o recálculo final de quadros agora funciona de acordo com o seguinte algoritmo.

  • Deslocamos o ponteiro do quadro para o topo da lista. Redefinimos o contador de quadros e redefinimos as matrizes. 
  • Além disso, num ciclo, passamos por todos os quadros e:
    • obtemos o número de parâmetros otimizados, 
    • distribuímos os resultados negativos e positivos em matrizes, 
    • adicionamos uma série de dados à tabela.
  • Após o ciclo de pesquisa detalhada de quadros, obtemos os cabeçalhos da tabela.
  • Em seguida, ordenamos a tabela de acordo com a coluna especificada nas configurações.
  • O método de atualização do gráfico finaliza com os resultados da otimização.

Código do método CFrameGenerator::FinalRecalculateFrames():

class CFrameGenerator
  {
private:
   //--- Recálculo final dos dados de todos os quadros após a otimização
   void              FinalRecalculateFrames(void);
  };
//+------------------------------------------------------------------+
//| Recálculo final dos dados de todos os quadros após a otimização  |
//+------------------------------------------------------------------+
void CFrameGenerator::FinalRecalculateFrames(void)
  {
//--- Deslocamos o ponteiro dos quadros para o começo
   ::FrameFirst();
//--- Redefinimos o contador e as matrizes
   ArraysFree();
   m_frames_counter=0;
//--- Executamos a pesquisa detalhada de quadros
   while(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
      //--- Obtém o número de parâmetros otimizados
      GetParametersTotal();
      //--- Resultado negativo
      if(m_data[m_profit_index]<0)
         AddLoss(m_data[m_profit_index]);
      //--- Resultado positivo
      else
         AddProfit(m_data[m_profit_index]);
      //--- Adiciona uma série de dados
      AddRow();
      //--- Aumentamos o contador dos quadros processados
      m_frames_counter++;
     }
//--- Obtemos os cabeçalhos para a tabela
   GetHeaders();
//--- Número de colunas e linhas
   int rows_total =::ArraySize(m_columns[0].m_rows);
//--- Ordenamos a tabela pela coluna especificada
   QuickSort(0,rows_total-1,m_column_sort_index);
//--- Atualizamos as séries 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));
//--- Atualizar gráfico
   m_graph_results.CalculateMaxMinValues();
   m_graph_results.CurvePlotAll();
   m_graph_results.Update();
  }

Abaixo, consideraremos os métodos pelos quais é possível obter os dados de um quadro a pedido do usuário.

Extraindo dados do quadro

Acima vimos a estrutura de uma matriz comum com uma sequência de dados de diferentes categorias. Agora precisamos entender como serão extraídos dessa matriz os dados. Acima, falamos sobre isso, pois as chaves no quadro contêm a enumeração de símbolos e o tamanho das matrizes de saldo. Se o tamanho da matriz de saldos fosse igual ao tamanho das matrizes de rebaixamento, os índices de todos os intervalos dos dados empacotados poderiam ser determinados por uma única fórmula no ciclo, como no diagrama abaixo. Mas os tamanhos das matrizes são diferentes. Portanto, na última iteração, no ciclo, é necessário determinar quantos elementos restam no intervalo de dados relacionado aos rebaixamentos do depósito, e dividi-lo em dois, pois os tamanhos das matrizes de rebaixamento são iguais. 

 


Fig. 2. Esquema com parâmetros para calcular o índice de uma matriz da seguinte categoria.

Para receber dados de um quadro, é implementado o método público CFrameGenerator::GetFrameData(). Vamos considerá-lo em mais detalhes.

No início do método, o ponteiro do quadro precisa ser movido para o topo da lista. Em seguida, começa o processo de pesquisa detalhada de todos os quadros com resultados de otimização. É necessário encontrar o quadro, número de passagem passado para o método como um argumento. Se for encontrado, o programa funcionará de acordo com o seguinte algoritmo.

  • Obtemos o tamanho da matriz comum com os dados do quadro. 
  • Nós obtemos os elementos do parâmetro de string e seu número. Se houver mais de um símbolo, o número de saldos na matriz será uma vez maior, quer dizer, o primeiro intervalo é o saldo total e os restantes estão relacionado aos saldos dos símbolos.
  • Em seguida, é preciso transferir os dados para as matrizes de saldos. Iniciamos o ciclo para extrair dados da matriz comum (o número de iterações é igual ao número de saldos). Para determinar o índice do qual é necessário copiar os dados, basta fazer o deslocamento tantas vezes quantos indicadores estatísticos existirem (STAT_TOTAL) e multiplicar o índice da iteração ( ) pelo tamanho da matriz de saldo (m_value). Assim, a cada iteração, obtemos os dados de todos os saldos, em matrizes separadas.
  • Na última iteração, obtemos dados do rebaixamento, em matrizes separadas. Como se trata dos últimos dados na matriz, só é preciso saber o número restante de elementos e dividi-lo em 2. Em seguida, em duas etapas consecutivas, obtemos os dados do rebaixamento
  • A última etapa é atualizar os gráficos com novos dados e interromper o ciclo de pesquisa detalhada de quadros.
class CFrameGenerator
  {
public:
   //--- Obtemos dos dados de acordo com o número especificado do quadro
   void              GetFrameData(const ulong pass_number);
  };
//+------------------------------------------------------------------+
//| Obtemos dos dados de acordo com o número especificado do quadro  |
//+------------------------------------------------------------------+
void CFrameGenerator::GetFrameData(const ulong pass_number)
  {
//--- Deslocamos o ponteiro dos quadros para o começo
   ::FrameFirst();
//--- Extração de dados
   while(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
      //--- Os números de passagem não correspondem, ir para o próximo
      if(m_pass!=pass_number)
         continue;
      //--- Tamanho da matriz com dados
      int data_total=::ArraySize(m_data);
      //--- Obtemos os elementos da linha pelo separador
      ushort u_sep          =::StringGetCharacter(",",0);
      int    symbols_total  =::StringSplit(m_name,u_sep,m_symbols_name);
      int    balances_total =(symbols_total>1)? symbols_total+1 : symbols_total;
      //--- Definimos o tamanho do número de saldos para a matriz
      ::ArrayResize(m_symbols_balance,balances_total);
      //--- Distribuímos os dados em matrizes
      for(int i=0; i<balances_total; i++)
        {
         //--- Liberar a matriz de dados
         ::ArrayFree(m_symbols_balance[i].m_data);
         //--- Definimos o índice do qual é necessário copiar os dados de origem
         int src_index=STAT_TOTAL+int(i*m_value);
         //--- Copiamos os dados para a matriz da estrutura de saldos
         ::ArrayCopy(m_symbols_balance[i].m_data,m_data,0,src_index,(int)m_value);
         //--- Se esta for a última iteração, obtemos os dados do rebaixamento
         if(i+1==balances_total)
           {
            //--- Obtemos a quantidade de dados restantes e o tamanho para as matrizes ao longo de dois eixos
            double dd_total   =data_total-(src_index+(int)m_value);
            double array_size =dd_total/2.0;
            //--- Índice a partir do qual começamos a copiar
            src_index=int(data_total-dd_total);
            //--- Definimos o tamanho de rebaixamento para as matrizes
            ::ArrayResize(m_dd_x,(int)array_size);
            ::ArrayResize(m_dd_y,(int)array_size);
            //--- Copiamos dados sem interrupção
            ::ArrayCopy(m_dd_x,m_data,0,src_index,(int)array_size);
            ::ArrayCopy(m_dd_y,m_data,0,src_index+(int)array_size,(int)array_size);
           }
        }
      //--- Atualizamos os gráficos e paramos o ciclo
      UpdateMSBalanceGraph();
      UpdateDrawdownGraph();
      break;
     }
  }

Para obter dados das células da tabela da matriz, chamamos o método público CFrameGenerator::GetValue(), especificando o índice da coluna e linhas da tabela nos argumentos. 

class CFrameGenerator
  {
public:
   //--- Retorna o valor da célula especificada
   string            GetValue(const uint column_index,const uint row_index);
  };
//+------------------------------------------------------------------+
//| Retorna o valor da célula especificada                           |
//+------------------------------------------------------------------+
string CFrameGenerator::GetValue(const uint column_index,const uint row_index)
  {
//--- Verificação de saída do intervalo das colunas
   uint csize=::ArraySize(m_columns);
   if(csize<1 || column_index>=csize)
      return("");
//--- Verificação de saída do intervalo das séries
   uint rsize=::ArraySize(m_columns[column_index].m_rows);
   if(rsize<1 || row_index>=rsize)
      return("");
//---
   return(m_columns[column_index].m_rows[row_index]);
  }

Visualização de dados e interação com a interface gráfica

Para atualizar os gráficos com os dados dos saldos e rebaixamentos, na classe CFrameGenerator são declarados mais dois objetos do tipo CGraphic. Como no caso de outros objetos deste tipo na classe CFrameGenerator, é necessário transferir para eles ponteiros para os elementos da interface gráfica, no início da otimização, no método CFrameGenerator::OnTesterInitEvent(). 

#include <Graphics\Graphic.mqh>
//+------------------------------------------------------------------+
//| Classe para trabalhar com os resultados da otimização            |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   //--- Ponteiros para os gráficos para visualização de dados
   CGraphic         *m_graph_ms_balance;
   CGraphic         *m_graph_drawdown;
   //---
public:
   //--- Manipulador de eventos do testador de estratégias
   void              OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_results,CGraphic *graph_ms_balance,CGraphic *graph_drawdown);
  };
//+------------------------------------------------------------------+
//| Deve ser chamada no processador OnTesterInit()                   |
//+------------------------------------------------------------------+
void CFrameGenerator::OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_results,
                                        CGraphic *graph_ms_balance,CGraphic *graph_drawdown)
  {
   m_graph_balance    =graph_balance;
   m_graph_results    =graph_results;
   m_graph_ms_balance =graph_ms_balance;
   m_graph_drawdown   =graph_drawdown;
  }

Os dados na tabela da interface gráfica são exibidos usando o método CProgram::GetFrameDataToTable(). Definimos o número de colunas, obtendo na matriz os cabeçalhos da tabela do objeto CFrameGenerator. Depois disso, definimos o tamanho da tabela (100 linhas) na interface gráfica. Em seguida, definimos os cabeçalhos e o tipo de dados.

Agora é preciso inicializar a tabela com os resultados da otimização. Definimos os valores nela através do método CTable::SetValue(). Para obter os valores das células da tabela de dados, usa-se o método CFrameGenerator::GetValue(). Para que as alterações sejam exibidas, a tabela precisa ser atualizada.

class CProgram
  {
private:
   //--- Obtém os dados dos quadros na tabela de resultados de otimização
   void              GetFrameDataToTable(void);
  };
//+------------------------------------------------------------------+
//| Obtemos os dados na tabela de resultados de otimização           |
//+------------------------------------------------------------------+
void CProgram::GetFrameDataToTable(void)
  {
//--- Obtemos os cabeçalhos
   string headers[];
   m_frame_gen.CopyHeaders(headers);
//--- Definimos o tamanho da tabela
   uint columns_total=::ArraySize(headers);
   m_table_param.Rebuilding(columns_total,100,true);
//--- Definimos os cabeçalhos o tipo de dados
   for(uint c=0; c<columns_total; c++)
     {
      m_table_param.DataType(c,TYPE_DOUBLE);
      m_table_param.SetHeaderText(c,headers[c]);
     }
//--- Preenchemos a tabela com dados dos quadros
   for(uint c=0; c<columns_total; c++)
     {
      for(uint r=0; r<m_table_param.RowsTotal(); r++)
        {
         if(c==1 || c==2 || c==4 || c==5)
            m_table_param.SetValue(c,r,m_frame_gen.GetValue(c,r),2);
         else
            m_table_param.SetValue(c,r,m_frame_gen.GetValue(c,r),0);
        }
     }
//--- Atualizar tabela
   m_table_param.Update(true);
   m_table_param.GetScrollHPointer().Update(true);
   m_table_param.GetScrollVPointer().Update(true);
  }

O método CProgram::GetFrameDataToTable() é chamado após o processo de otimização de parâmetros do Expert Advisor no método OnTesterDeinit(). Depois disso, a interface gráfica fica disponível para o usuário. Após ir para a guia Results, é possível ver os resultados de otimização selecionados pelo critério especificado. No nosso exemplo, a seleção é realizada pelo indicador na segunda coluna (Profit).

 Fig. 3 - Tabela de resultados de otimização na interface gráfica.

Fig. 3. Tabela de resultados de otimização na interface gráfica.

Agora vamos ver como o usuário pode ver os saldos multissímbolos dos resultados desta tabela. Se for selecionada alguma linha na tabela, será gerado o evento personalizado ON_CLICK_LIST_ITEM com o ID da tabela. Graças a ele, podemos determinar de qual tabela veio a mensagem (se houver várias). Como a primeira coluna da tabela de dados armazena o número de passagem, quer dizer, a possibilidade de obter os dados deste resultado, transferindo este número para o método CFrameGenerator::GetFrameData().

//+------------------------------------------------------------------+
//| Manipulador de eventos                                           |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Evento de pressionado nas séries da tabela
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM)
     {
      if(lparam==m_table_param.Id())
        {
         //--- Obtemos o número da passagem da tabela
         ulong pass=(ulong)m_table_param.GetValue(0,m_table_param.SelectedItem());
         //--- Obtemos os dados do número da passagem
         m_frame_gen.GetFrameData(pass);
        }
      //---
      return;
     }
...
  }

Cada vez que o usuário seleciona uma linha na tabela, o gráfico dos saldos multissímbolos é atualizado na guia Balance:

 Fig. 4 – Apresentação do resultado.

Fig. 4. Apresentação do resultado.

Acabamos tendo uma ferramenta bastante conveniente para visualizar rapidamente os resultados dos testes com vários símbolos. 

Fim do artigo

Mostrei mais uma maneira de como se pode trabalhar com os resultados da otimização após sua conclusão. Este tópico ainda não está esgotado e pode e deve ser desenvolvido. Com a biblioteca para criar interfaces gráficas, você pode criar muitas soluções interessantes e convenientes. Ofereça suas ideias nos comentários do artigo; talvez num dos artigos a seguir apareça uma ferramenta para que você trabalhe com resultados de otimização. 

Abaixo você pode baixar para o seu computador os arquivos de teste e um estudo mais detalhado do código apresentado no artigo.

Nome do arquivo Comentário
MacdSampleMSFrames.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
FrameGenerator.mqh Arquivo com uma classe para trabalhar com resultados de otimização.