
Trabalhemos com os resultados da otimização através da interface gráfica do usuário
Sumário
- Introdução
- Desenvolvimento da interface gráfica
- Salvando resultados de otimização
- Extraindo dados do quadro
- Visualização de dados e interação com a interface gráfica
- Fim do artigo
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
- Número de passagem
- Resultado do teste
- Lucro (STAT_PROFIT)
- Número de transações (STAT_TRADES)
- Rebaixamento (STAT_EQUITY_DDREL_PERCENT)
- Fator de recuperação (STAT_RECOVERY_FACTOR)
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 ( i ) 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.
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.
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. |
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/4562





- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso