Visualizando a otimização de uma estratégia de negociação na MetaTrader 5
Conteúdo
- Introdução
- Desenvolvimento da interface gráfica
- Desenvolvimento da classe para trabalhar com os dados de quadros
- Trabalhando com os dados da otimização na classe do aplicativo
- Exibindo os resultados obtidos
- Conclusão
Introdução
Ao desenvolver algoritmos de negociação, é útil visualizar os resultados dos testes e otimizar os parâmetros. No entanto, um único gráfico na aba Gráfico da Otimização pode ser insuficiente para avaliar a eficiência de uma estratégia de negociação. Seria melhor visualizar as curvas de saldo de vários testes simultaneamente, podendo analisá-los mesmo após a otimização. Nós já examinamos tal aplicação no artigo "Visualizar uma estratégia no tester do MetaTrader 5". No entanto, muitas novas oportunidades surgiram desde então. Portanto, agora é possível implementar um aplicativo semelhante, mas muito mais poderoso.
O artigo implementa um aplicativo MQL com uma interface gráfica para a visualização estendida do processo de otimização. A interface gráfica utiliza a última versão da biblioteca EasyAndFast. Muitos usuários da comunidade MQL podem questionar-se sobre a necessidade de utilizar interfaces gráficas em aplicativos MQL. Este artigo mostra seus potenciais usos. Ele também pode ser útil para aqueles que aplicam a biblioteca em seu trabalho.
Desenvolvimento da interface gráfica
Aqui eu vou descrever brevemente o desenvolvimento da interface gráfica. Se você já dominou a biblioteca EasyAndFast, você será capaz de entender rapidamente como usá-la e avaliar como é fácil desenvolver uma interface gráfica para seu aplicativo MQL.
Primeiro, vamos descrever a estrutura geral do aplicativo desenvolvido. O arquivo Program.mqh deve conter a classe de aplicação CProgram. Esta classe base deve ser conectada ao mecanismo da biblioteca gráfica.
//+------------------------------------------------------------------+ //| Program.mqh | //| Copyright 2018, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ //--- Classe da biblioteca para a criação da interface gráfica #include <EasyAndFastGUI\WndEvents.mqh> //+------------------------------------------------------------------+ //| Classe para o desenvolvimento do aplicativo | //+------------------------------------------------------------------+ class CProgram : public CWndEvents { };
A biblioteca EasyAndFast é exibida em um único bloco (Library GUI) para não confundir a imagem. Você pode visualizá-la na íntegra na página da biblioteca.
Fig. 1. Inclusão da biblioteca para criar a GUI
Métodos semelhantes devem ser criados na classe CProgram para se conectar com as principais funções do programa em MQL. Nós vamos precisar dos métodos da categoria OnTesterXXX() para poder trabalhar com os quadros.
class CProgram : public CWndEvents { public: //--- Inicialização/desinicialização bool OnInitEvent(void); void OnDeinitEvent(const int reason); //--- Manipulador de eventos "Novo tick" void OnTickEvent(void); //--- Manipulador de eventos de negociação void OnTradeEvent(void); //--- Timer void OnTimerEvent(void); //--- Simulador double OnTesterEvent(void); void OnTesterPassEvent(void); void OnTesterInitEvent(void); void OnTesterDeinitEvent(void); };
Neste caso, os métodos devem ser chamados da seguinte maneira no arquivo principal do aplicativo:
//--- Classe de inclusão do aplicativo #include "Program.mqh" CProgram program; //+------------------------------------------------------------------+ //| Função de inicialização do Expert | //+------------------------------------------------------------------+ int OnInit(void) { //--- Inicializa o programa if(!program.OnInitEvent()) { ::Print(__FUNCTION__," > Failed to initialize!"); return(INIT_FAILED); } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Função de desinicialização do Expert | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { program.OnDeinitEvent(reason); } //+------------------------------------------------------------------+ //| Função tick do Expert | //+------------------------------------------------------------------+ void OnTick(void) { program.OnTickEvent(); } //+------------------------------------------------------------------+ //| Funçao timer | //+------------------------------------------------------------------+ void OnTimer(void) { program.OnTimerEvent(); } //+------------------------------------------------------------------+ //| Função ChartEvent | //+------------------------------------------------------------------+ void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { program.ChartEvent(id,lparam,dparam,sparam); } //+------------------------------------------------------------------+ //| Função Tester | //+------------------------------------------------------------------+ double OnTester(void) { return(program.OnTesterEvent()); } //+------------------------------------------------------------------+ //| Função TesterInit | //+------------------------------------------------------------------+ void OnTesterInit(void) { program.OnTesterInitEvent(); } //+------------------------------------------------------------------+ //| Função TesterPass | //+------------------------------------------------------------------+ void OnTesterPass(void) { program.OnTesterPassEvent(); } //+------------------------------------------------------------------+ //| Função TesterDeinit | //+------------------------------------------------------------------+ void OnTesterDeinit(void) { program.OnTesterDeinitEvent(); } //+------------------------------------------------------------------+
Assim, a estrutura da aplicação está pronta para o desenvolvimento da interface gráfica. O trabalho principal é realizado na classe CProgram. Todos os arquivos necessários para o seu funcionamento estão incluídos em Program.mqh.
Agora vamos definir o conteúdo da interface gráfica. Lista todos os elementos a serem criados.
- Formulário para controles.
- Campo para especificar a quantidade dos saldos a serem exibidos no gráfico.
- Campo para ajustar a velocidade de exibição dos novos resultados da otimização.
- Botão para iniciar uma nova exibição.
- Tabela de estatísticas dos resultados.
- Tabela para exibir os parâmetros externos do EA.
- Gráfico da curva de saldo.
- Gráfico de resultados da otimização.
- Barra de estado para exibir o resumo das informações adicionais.
- Barra de progresso exibindo a porcentagem dos resultados exibidos do valor total durante o novo deslocamento.
Abaixo estão as declarações de instâncias de classe dos elementos de controle e seus métodos de criação (veja o código abaixo). Os códigos dos métodos são colocados em um arquivo separado — CreateFrameModeGUI.mqh, que está associado com o arquivo de classe CProgram. À medida que o código do aplicativo desenvolvido cresce, o método de distribuição por arquivos individuais torna-se mais relevante, facilitando a navegação no projeto.
class CProgram : public CWndEvents { private: //--- Janela CWindow m_window1; //--- Barra de status CStatusBar m_status_bar; //--- Campos de entrada CTextEdit m_curves_total; CTextEdit m_sleep_ms; //--- Botões CButton m_reply_frames; //--- Tabelas CTable m_table_stat; CTable m_table_param; //--- Gráficos CGraph m_graph1; CGraph m_graph2; //--- Barra de progresso CProgressBar m_progress_bar; //--- public: //--- Cria a interface gráfica para trabalhar com os quadros no modo de otimização bool CreateFrameModeGUI(void); //--- private: //--- Formulário bool CreateWindow(const string text); //--- Barra de status bool CreateStatusBar(const int x_gap,const int y_gap); //--- Tabelas bool CreateTableStat(const int x_gap,const int y_gap); bool CreateTableParam(const int x_gap,const int y_gap); //--- Campos de entrada bool CreateCurvesTotal(const int x_gap,const int y_gap,const string text); bool CreateSleep(const int x_gap,const int y_gap,const string text); //--- Botões bool CreateReplyFrames(const int x_gap,const int y_gap,const string text); //--- Gráficos bool CreateGraph1(const int x_gap,const int y_gap); bool CreateGraph2(const int x_gap,const int y_gap); //--- Barra de progresso bool CreateProgressBar(const int x_gap,const int y_gap,const string text); }; //+------------------------------------------------------------------+ //| Métodos para a criação dos elementos de controle | //+------------------------------------------------------------------+ #include "CreateFrameModeGUI.mqh" //+------------------------------------------------------------------+
Vamos habilitar a inclusão do arquivo a ser conectado na CreateFrameModeGUI.mqh também. Nós vamos mostrar aqui apenas um método principal para a criação da interface gráfica do aplicativo de exemplo:
//+------------------------------------------------------------------+ //| CreateFrameModeGUI.mqh | //| Copyright 2018, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #include "Program.mqh" //+------------------------------------------------------------------+ //| Crie a interface gráfica | //| para analisar os resultados da otimização e trabalhar com os quadros| //+------------------------------------------------------------------+ bool CProgram::CreateFrameModeGUI(void) { //--- Cria a interface apenas no modo para trabalhar com os quadros da otimização if(!::MQLInfoInteger(MQL_FRAME_MODE)) return(false); //--- Cria o formulário para os elementos de controle if(!CreateWindow("Frame mode")) return(false); //--- Cria os elementos de controle if(!CreateStatusBar(1,23)) return(false); if(!CreateCurvesTotal(7,25,"Curves total:")) return(false); if(!CreateSleep(145,25,"Sleep:")) return(false); if(!CreateReplyFrames(255,25,"Replay frames")) return(false); if(!CreateTableStat(2,50)) return(false); if(!CreateTableParam(2,212)) return(false); if(!CreateGraph1(200,50)) return(false); if(!CreateGraph2(200,159)) return(false); //--- Barra de progresso if(!CreateProgressBar(2,3,"Processing...")) return(false); //--- Criação completa da GUI CWndEvents::CompletedGUI(); return(true); } ...
A conexão entre os arquivos pertencentes a uma classe é mostrada como uma seta amarela de dois lados:
Fig. 2. Dividindo o projeto em vários arquivos
Desenvolvimento da classe para trabalhar com os dados de quadros
Vamos escrever uma classe separada CFrameGenerator para trabalhar com os quadros. A classe deve estar contida em FrameGenerator.mqh que deve ser incluído em Program.mqh. Como exemplo, eu demonstrarei duas opções para receber esses quadros para exibição nos elementos de interface gráfica.
- No primeiro caso, para exibir os quadros como objetos gráficos, os ponteiros para esses objetos são passados para os métodos de classe.
- No segundo caso, nós recebemos os dados dos quadros para preencher as tabelas de outras categorias usando os métodos especiais.
Você decide quais dessas opções devem ser deixadas como a principal.
A biblioteca EasyAndFast aplica a classe CGraphic da biblioteca padrão para visualizar os dados. Vamos incluí-lo em FrameGenerator.mqh para acessar os seus métodos.
//+------------------------------------------------------------------+ //| FrameGenerator.mqh | //| Copyright 2018, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #include <Graphics\Graphic.mqh> //+------------------------------------------------------------------+ //| Classe para receber os resultados da otimização | //+------------------------------------------------------------------+ class CFrameGenerator { };
O arranjo do programa agora se parece com o seguinte:
Fig. 3. Conectando aos projetos de classe para o trabalho
Agora vamos ver como a classe CFrameGenerator é organizada. Ele também precisa de métodos para processar os eventos do testador de estratégia (consulte o código abaixo). Eles devem ser chamados nos métodos de classe análogos ao programa que nós desenvolvemos — CProgram. Os ponteiros dos objetos gráficos são passados para o método CFrameGenerator::OnTesterInitEvent() para representar o processo de otimização atual.
- O primeiro gráfico (graph_balance) exibe o número especificado da última série dos saldos dos resultados da otimização.
- O segundo gráfico (graph_result) exibe os resultados gerais da otimização.
class CFrameGenerator { private: //--- Ponteiros do gráfico para a visualização dos dados CGraphic *m_graph_balance; CGraphic *m_graph_results; //--- public: //--- Manipuladores de eventos do testador de estratégia void OnTesterEvent(const double on_tester_value); void OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_result); void OnTesterDeinitEvent(void); bool OnTesterPassEvent(void); }; //+------------------------------------------------------------------+ //| Deve ser chamado no manipulador da OnTesterInit() | //+------------------------------------------------------------------+ void CFrameGenerator::OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_results) { m_graph_balance =graph_balance; m_graph_results =graph_results; }
Nos dois gráficos, os resultados positivos são exibidos em verde, enquanto que os negativos são exibidos em vermelho.
Dentro do método CFrameGenerator::OnTesterEvent(), nós recebemos o saldo do resultado do teste e os parâmetros estatísticos. Esses dados são passados para um quadro usando os métodos CFrameGenerator::GetBalanceData() e CFrameGenerator::GetStatData(). O método CFrameGenerator::GetBalanceData() recebe todo o histórico de testes e resume tudo negociações in-/inout. O resultado obtido é salvo passo a passo no array m_balance[]. Por sua vez, este array é um membro da classe CFrameGenerator.
O array dinâmico a ser enviado para um quadro é passado para o método CFrameGenerator::GetStatData(). Seu tamanho é para corresponder ao tamanho do array para o saldo resultado, que foi recebido anteriormente. Além disso, vários elementos aos quais nós recebemos os parâmetros estatísticos são adicionados.
//--- Número de parâmetros estatísticos #define STAT_TOTAL 7 //+------------------------------------------------------------------+ //| Classe para trabalhar com os resultados da otimização | //+------------------------------------------------------------------+ class CFrameGenerator { private: //--- Saldo do resultado double m_balance[]; //--- private: //--- Recebe os dados do saldo int GetBalanceData(void); //--- Recebe os dados estatísticos void GetStatData(double &dst_array[],double on_tester_value); }; //+------------------------------------------------------------------+ //| Obtém os dados do saldo | //+------------------------------------------------------------------+ int CFrameGenerator::GetBalanceData(void) { int data_count =0; double balance_current =0; //--- Solicita todo o histórico de negociação ::HistorySelect(0,LONG_MAX); uint deals_total=::HistoryDealsTotal(); //--- Coleta os dados sobre negociações for(uint i=0; i<deals_total; i++) { //--- Recebe um ticket ulong ticket=::HistoryDealGetTicket(i); if(ticket<1) continue; //--- Se um saldo inicial ou uma negociação out-/inout long entry=::HistoryDealGetInteger(ticket,DEAL_ENTRY); if(i==0 || entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT) { double swap =::HistoryDealGetDouble(ticket,DEAL_SWAP); double profit =::HistoryDealGetDouble(ticket,DEAL_PROFIT); double commision =::HistoryDealGetDouble(ticket,DEAL_COMMISSION); //--- Calcula o saldo balance_current+=(profit+swap+commision); //--- Salva para o array data_count++; ::ArrayResize(m_balance,data_count,100000); m_balance[data_count-1]=balance_current; } } //--- Obtém a quantidade de dados return(data_count); } //+------------------------------------------------------------------+ //| Recebe os dados estatísticos | //+------------------------------------------------------------------+ void CFrameGenerator::GetStatData(double &dst_array[],double on_tester_value) { ::ArrayResize(dst_array,::ArraySize(m_balance)+STAT_TOTAL); ::ArrayCopy(dst_array,m_balance,STAT_TOTAL,0); //--- Preenche os primeiros valores da matriz (STAT_TOTAL) com os resultados do teste dst_array[0] =::TesterStatistics(STAT_PROFIT); // Lucro líquido dst_array[1] =::TesterStatistics(STAT_PROFIT_FACTOR); // Fator de lucro dst_array[2] =::TesterStatistics(STAT_RECOVERY_FACTOR); // Fator de recuperação dst_array[3] =::TesterStatistics(STAT_TRADES); // número de trades dst_array[4] =::TesterStatistics(STAT_DEALS); // número de negócios dst_array[5] =::TesterStatistics(STAT_EQUITY_DDREL_PERCENT); // rebaixamento máximo do saldo em % dst_array[6] =on_tester_value; // valor do critério de otimização personalizada }
Os métodos CFrameGenerator::GetBalanceData() e CFrameGenerator::GetStatData() são chamados no manipulador de eventos de conclusão do teste - CFrameGenerator::OnTesterEvent() Dados recebidos. Envie-os para o terminal em um quadro.
//+------------------------------------------------------------------+ //| Prepara o array de valores do saldo e envie-o em um quadro | //| A função deve ser chamada no manipulador da OnTester() | //+------------------------------------------------------------------+ void CFrameGenerator::OnTesterEvent(const double on_tester_value) { //--- Obtém os dados do saldo int data_count=GetBalanceData(); //--- Array para enviar os dados para um quadro double stat_data[]; GetStatData(stat_data,on_tester_value); //--- Cria um quadro com os dados e envia para o terminal if(!::FrameAdd(::MQLInfoString(MQL_PROGRAM_NAME),1,data_count,stat_data)) ::Print(__FUNCTION__," > Frame add error: ",::GetLastError()); else ::Print(__FUNCTION__," > Frame added, Ok"); }
Agora, vamos considerar os métodos a serem usados no manipulador de eventos da chegada dos quadros durante a otimização — CFrameGenerator::OnTesterPassEvent() Nós vamos precisar das variáveis para trabalhar com os quadros: nome, ID, número do passo, valor aceito e o array de dados aceito. Todos esses dados são enviados para o quadro usando a função FrameAdd(), que é exibida acima.
class CFrameGenerator { private: //--- Variáveis para trabalhar com os quadros string m_name; ulong m_pass; long m_id; double m_value; double m_data[]; };
O método CFrameGenerator::SaveStatData() do array que aceitamos no quadro é usado para obter os parâmetros estatísticos e salvá-los em um array de strings separados. Lá os dados devem conter o nome do indicador e seu valor. O símbolo '=' é usado como um separador.
class CFrameGenerator { private: //--- Array com os parâmetros estatísticos string m_stat_data[]; //--- private: //--- Salva os dados estatísticos void SaveStatData(void); }; //+------------------------------------------------------------------+ //| Salva os parâmetros estatísticos do resultado no array | //+------------------------------------------------------------------+ void CFrameGenerator::SaveStatData(void) { //--- Array para aceitar os parâmetros estatísticos do quadro double stat[]; ::ArrayCopy(stat,m_data,0,0,STAT_TOTAL); ::ArrayResize(m_stat_data,STAT_TOTAL); //--- Preenche o array com os resultados do teste m_stat_data[0] ="Net profit="+::StringFormat("%.2f",stat[0]); m_stat_data[1] ="Profit Factor="+::StringFormat("%.2f",stat[1]); m_stat_data[2] ="Factor Recovery="+::StringFormat("%.2f",stat[2]); m_stat_data[3] ="Trades="+::StringFormat("%G",stat[3]); m_stat_data[4] ="Deals="+::StringFormat("%G",stat[4]); m_stat_data[5] ="Equity DD="+::StringFormat("%.2f%%",stat[5]); m_stat_data[6] ="OnTester()="+::StringFormat("%G",stat[6]); }
Os dados estatísticos devem ser salvos em um array separado, para que possam ser recuperados na classe do aplicativo (CProgram) para preenchimento da tabela. O método público CFrameGenerator::CopyStatData() é chamado para recebê-los depois de passar o array para ser copiado.
class CFrameGenerator { public: //--- Obtém os parâmetros estatísticos para o array passado int CopyStatData(string &dst_array[]) { return(::ArrayCopy(dst_array,m_stat_data)); } };
Para atualizar os gráficos de resultados durante a otimização, nós precisaremos dos métodos auxiliares responsáveis por adicionar os resultados positivos e negativos aos arrays. Por favor note que o resultado é adicionado ao valor atual do contador de quadros pelo eixo X. Como resultado, os vazios formados não são refletidos no gráfico como valores zero.
//--- Tamanho de reserva para os arrays #define RESERVE_FRAMES 1000000 //+------------------------------------------------------------------+ //| Classe para trabalhar com os resultados da otimização | //+------------------------------------------------------------------+ class CFrameGenerator { private: //--- Contador de quadros ulong m_frames_counter; //--- Dados sobre os resultados positivos e negativos double m_loss_x[]; double m_loss_y[]; double m_profit_x[]; double m_profit_y[]; //--- private: //--- Adiciona o resultado (1) negativo e (2) positivo nos arrays void AddLoss(const double loss); void AddProfit(const double profit); }; //+------------------------------------------------------------------+ //| Adiciona o resultado negativo ao array | //+------------------------------------------------------------------+ void CFrameGenerator::AddLoss(const double loss) { int size=::ArraySize(m_loss_y); ::ArrayResize(m_loss_y,size+1,RESERVE_FRAMES); ::ArrayResize(m_loss_x,size+1,RESERVE_FRAMES); m_loss_y[size] =loss; m_loss_x[size] =(double)m_frames_counter; } //+------------------------------------------------------------------+ //| Adciona o resultado positivo ao array | //+------------------------------------------------------------------+ void CFrameGenerator::AddProfit(const double profit) { int size=::ArraySize(m_profit_y); ::ArrayResize(m_profit_y,size+1,RESERVE_FRAMES); ::ArrayResize(m_profit_x,size+1,RESERVE_FRAMES); m_profit_y[size] =profit; m_profit_x[size] =(double)m_frames_counter; }
Os principais métodos para atualizar os gráficos aqui são CFrameGenerator::UpdateResultsGraph() e CFrameGenerator::UpdateBalanceGraph():
class CFrameGenerator { private: //--- Atualiza o gráfico de resultados void UpdateResultsGraph(void); //--- Atualiza o gráfico de saldo void UpdateBalanceGraph(void); };
No método CFrameGenerator::UpdateResultsGraph(), os resultados do teste (lucro positivo/negativo) são adicionados aos arrays. Em seguida, esses dados são exibidos em um gráfico apropriado. Os nomes das séries gráficas exibem o número atual dos resultados positivos e negativos.
//+------------------------------------------------------------------+ //| Atualiza o gráfico de resultados | //+------------------------------------------------------------------+ void CFrameGenerator::UpdateResultsGraph(void) { //--- Resultado negativo if(m_data[0]<0) AddLoss(m_data[0]); //--- Resultado positivo else AddProfit(m_data[0]); //--- Atualiza a série no gráfico de resultados da otimização CCurve *curve=m_graph_results.CurveGetByIndex(0); curve.Name("P: "+(string)ProfitsTotal()); curve.Update(m_profit_x,m_profit_y); //--- curve=m_graph_results.CurveGetByIndex(1); curve.Name("L: "+(string)LossesTotal()); curve.Update(m_loss_x,m_loss_y); //--- Propriedades do eixo horizontal CAxis *x_axis=m_graph_results.XAxis(); x_axis.Min(0); x_axis.Max(m_frames_counter); x_axis.DefaultStep((int)(m_frames_counter/8.0)); //--- Atualiza o gráfico m_graph_results.CalculateMaxMinValues(); m_graph_results.CurvePlotAll(); m_graph_results.Update(); }
No começo do método CFrameGenerator::UpdateBalanceGraph(), os dados relacionados ao saldo são recuperados no array de dados passados no quadro. Como várias séries podem ser exibidas no gráfico, nós devemos tornar a atualização da série consistente. Para conseguir isso, nós vamos usar um contador de séries separado. Para configurar o número de séries de saldo exibidas simultaneamente no gráfico, nós precisamos do método público CFrameGenerator::SetCurvesTotal(). Assim que o contador da série atingir o limite estabelecido, a contagem começa do início. O contador de quadros atua como os nomes das séries. A cor da série também depende do resultado: verde significa resultado positivo, vermelho — negativo.
Como o número de negociações em cada resultado é diferente, nós devemos definir as maiores séries e definir o máximo pelo eixo X para ajustar todas as séries necessárias no gráfico.
class CFrameGenerator { private: //--- Número de séries uint m_curves_total; //--- Índice da série atual no gráfico uint m_last_serie_index; //--- Para definir a máxima da série double m_curve_max[]; //--- public: //--- Define o número de séries para exibir no gráfico void SetCurvesTotal(const uint total); }; //+------------------------------------------------------------------+ //| Define o número de séries para a exibição no gráfico | //+------------------------------------------------------------------+ void CFrameGenerator::SetCurvesTotal(const uint total) { m_curves_total=total; ::ArrayResize(m_curve_max,total); ::ArrayInitialize(m_curve_max,0); } //+------------------------------------------------------------------+ //| Atualiza o gráfico de saldo | //+------------------------------------------------------------------+ void CFrameGenerator::UpdateBalanceGraph(void) { //--- Array para aceitar os valores do saldo do quadro atual double serie[]; ::ArrayCopy(serie,m_data,0,STAT_TOTAL,::ArraySize(m_data)-STAT_TOTAL); //--- Envia o array para exibição no gráfico de saldo CCurve *curve=m_graph_balance.CurveGetByIndex(m_last_serie_index); curve.Name((string)m_frames_counter); curve.Color((m_data[0]>=0)? ::ColorToARGB(clrLimeGreen) : ::ColorToARGB(clrRed)); curve.Update(serie); //--- Obtém o tamanho da série int serie_size=::ArraySize(serie); m_curve_max[m_last_serie_index]=serie_size; //--- Define a série com o número máximo de elementos double x_max=0; for(uint i=0; i<m_curves_total; i++) x_max=::fmax(x_max,m_curve_max[i]); //--- Propriedades do eixo horizontal CAxis *x_axis=m_graph_balance.XAxis(); x_axis.Min(0); x_axis.Max(x_max); x_axis.DefaultStep((int)(x_max/8.0)); //--- Atualiza o gráfico m_graph_balance.CalculateMaxMinValues(); m_graph_balance.CurvePlotAll(); m_graph_balance.Update(); //--- Aumenta o contador de séries m_last_serie_index++; //--- Se o limite for atingido, define o contador de séries para zero if(m_last_serie_index>=m_curves_total) m_last_serie_index=0; }
Nós consideramos os métodos necessários para organizar o trabalho no manipulador de quadros. Agora vamos dar uma olhada mais de perto no próprio método do manipulador CFrameGenerator::OnTesterPassEvent(). Ele retorna true, enquanto a otimização está em andamento e a função FrameNext() obtém os dados do quadro. Depois de concluir a otimização, o método retorna false.
Na lista do EA de parâmetros que podem ser obtidos usando a função FrameInputs(), os parâmetros definidos para otimização vão primeiro, seguidos pelos que não participam da otimização.
Se os dados do quadro forem obtidos, a função FrameInputs() nos permite obter os parâmetros do EA durante o passo de otimização atual. Em seguida, nós salvamos as estatísticas, atualizamos os gráficos e aumentamos o contador de quadros. Depois disso, o método CFrameGenerator::OnTesterPassEvent() retorna true até a próxima chamada.
class CFrameGenerator { private: //--- Parâmetros do EA string m_param_data[]; uint m_par_count; }; //+------------------------------------------------------------------+ //| Recebe o quadro com os dados durante a otimização e exibe o gráfico | //+------------------------------------------------------------------+ bool CFrameGenerator::OnTesterPassEvent(void) { //--- Depois de obter um novo quadro, tenta recuperar os dados dele if(::FrameNext(m_pass,m_name,m_id,m_value,m_data)) { //--- Obtém os parâmetros de entrada do EA o quadro é formado por ::FrameInputs(m_pass,m_param_data,m_par_count); //--- Salva os parâmetros estatísticos do resultado no array SaveStatData(); //--- Atualiza o gráfico de resultados e balanceamento UpdateResultsGraph(); UpdateBalanceGraph(); //--- Aumenta o contador de quadros processados m_frames_counter++; return(true); } //--- return(false); }
Após a otimização ser concluída, o evento TesterDeinit é gerado e o método CFrameGenerator::OnTesterDeinitEvent() é chamado no modo de processamento de quadros. No momento, nem todos os quadros podem ser processados durante a otimização, portanto, o gráfico de visualização de resultados estará incompleto. Para ver a imagem completa, você precisa percorrer todos os quadros usando o método CFrameGenerator::FinalRecalculateFrames() e recarregar o gráfico logo após a otimização.
Para fazer isso, mude o ponteiro para o início da lista de quadros e, em seguida, defina os arrays de resultados e o contador de quadros para zero. Em seguida, percorra a lista completa de quadros, preencha os arrays por resultados positivos e negativos e, eventualmente, atualize o gráfico.
class CFrameGenerator { private: //--- Libera os arrays void ArraysFree(void); //--- Re-cálculo final dos dados de todos os quadros após a otimização void FinalRecalculateFrames(void); }; //+------------------------------------------------------------------+ //| Libera os arrays | //+------------------------------------------------------------------+ void CFrameGenerator::ArraysFree(void) { ::ArrayFree(m_loss_y); ::ArrayFree(m_loss_x); ::ArrayFree(m_profit_y); ::ArrayFree(m_profit_x); } //+------------------------------------------------------------------+ //| Re-cálculo final dos dados de todos os quadros após a otimização | //+------------------------------------------------------------------+ void CFrameGenerator::FinalRecalculateFrames(void) { //--- Define o ponteiro do quadro para o início ::FrameFirst(); //--- Redefine o contador e os arrays ArraysFree(); m_frames_counter=0; //--- Inicia o ciclo pelos quadros while(::FrameNext(m_pass,m_name,m_id,m_value,m_data)) { //--- Resultado negativo if(m_data[0]<0) AddLoss(m_data[0]); //--- Resultado positivo else AddProfit(m_data[0]); //--- Aumenta o contador de quadros processados m_frames_counter++; } //--- Atualiza a série no gráfico CCurve *curve=m_graph_results.CurveGetByIndex(0); curve.Name("P: "+(string)ProfitsTotal()); curve.Update(m_profit_x,m_profit_y); //--- curve=m_graph_results.CurveGetByIndex(1); curve.Name("L: "+(string)LossesTotal()); curve.Update(m_loss_x,m_loss_y); //--- Propriedades do eixo horizontal CAxis *x_axis=m_graph_results.XAxis(); x_axis.Min(0); x_axis.Max(m_frames_counter); x_axis.DefaultStep((int)(m_frames_counter/8.0)); //--- Atualiza o gráfico m_graph_results.CalculateMaxMinValues(); m_graph_results.CurvePlotAll(); m_graph_results.Update(); }
Nesse caso, o código do método CFrameGenerator::OnTesterDeinitEvent() segue abaixo Aqui nós também nos lembramos do número total de quadros e definimos o contador para zero.
//+------------------------------------------------------------------+ //| Deve ser chamado no manipulador da OnTesterDeinit() | //+------------------------------------------------------------------+ void CFrameGenerator::OnTesterDeinitEvent(void) { //--- Re-cálculo final dos dados de todos os quadros após a otimização FinalRecalculateFrames(); //--- Lembre-se do número total de quadros e defina os contadores como zero m_frames_total =m_frames_counter; m_frames_counter =0; m_last_serie_index =0; }
Em seguida, vamos dar uma olhada no uso dos métodos da classe CFrameGenerator na classe do aplicativo.
Trabalhando com os dados da otimização na classe do aplicativo
A interface gráfica é criada no método de inicialização do teste CProgram::OnTesterInitEvent(). Depois disso, a interface gráfica deve ficar inacessível. Para fazer isso, nós precisamos dos métodos adicionais CProgram::IsAvailableGUI() e CProgram::IsLockedGUI() que será usado em outros métodos da classe CProgram.
Vamos inicializar o gerador de quadros: passar os ponteiros para os gráficos a serem usados para visualizar os resultados da otimização.
class CProgram : public CWndEvents { private: //--- Disponibilidade da interface void IsAvailableGUI(const bool state); void IsLockedGUI(const bool state); } //+------------------------------------------------------------------+ //| Evento de início da otimização | //+------------------------------------------------------------------+ void CProgram::OnTesterInitEvent(void) { //--- Cria a interface gráfica if(!CreateFrameModeGUI()) { ::Print(__FUNCTION__," > Could not create the GUI!"); return; } //--- Torna a interface inacessível IsLockedGUI(false); //--- Inicializa o gerador de quadros m_frame_gen.OnTesterInitEvent(m_graph1.GetGraphicPointer(),m_graph2.GetGraphicPointer()); } //+------------------------------------------------------------------+ //| Disponibilidade da interface | //+------------------------------------------------------------------+ void CProgram::IsAvailableGUI(const bool state) { m_window1.IsAvailable(state); m_sleep_ms.IsAvailable(state); m_curves_total.IsAvailable(state); m_reply_frames.IsAvailable(state); } //+------------------------------------------------------------------+ //| Bloqueia a interface | //+------------------------------------------------------------------+ void CProgram::IsLockedGUI(const bool state) { m_window1.IsAvailable(state); m_sleep_ms.IsLocked(!state); m_curves_total.IsLocked(!state); m_reply_frames.IsLocked(!state); }
Nós já mencionamos que os dados nas tabelas devem ser atualizados na classe do aplicativo usando os métodos CProgram::UpdateStatTable() e CProgram::UpdateParamTable(). O código de ambas as tabelas é idêntico, então nós vamos dar um exemplo de apenas um deles. Os nomes dos parâmetros e valores na mesma linha são exibidos usando '=' como um separador. Portanto, nós passamos por eles em um loop e dividimos em um array separado, dividindo-o em dois elementos. Em seguida, nós inserimos esses valores nas células da tabela.
class CProgram : public CWndEvents { private: //--- Atualiza a tabela de estatísticas void UpdateStatTable(void); //--- Atualiza a tabela de parâmetros void UpdateParamTable(void); } //+------------------------------------------------------------------+ //| Atualiza a tabela da estatística | //+------------------------------------------------------------------+ void CProgram::UpdateStatTable(void) { //--- Obtém o array de dados para a tabela da estatística string stat_data[]; int total=m_frame_gen.CopyStatData(stat_data); for(int i=0; i<total; i++) { //--- Divide em duas linhas e entra na tabela string array[]; if(::StringSplit(stat_data[i],'=',array)==2) { if(m_frame_gen.CurrentFrame()>1) m_table_stat.SetValue(1,i,array[1],0,true); else { m_table_stat.SetValue(0,i,array[0],0,true); m_table_stat.SetValue(1,i,array[1],0,true); } } } //--- Atualiza a tabela m_table_stat.Update(); }
Ambos os métodos para atualizar os dados nas tabelas são chamados no método CProgram::OnTesterPassEvent() por uma resposta positiva do método do mesmo nome CFrameGenerator::OnTesterPassEvent():
//+------------------------------------------------------------------+ //| Evento de processamento do passo da otimização | //+------------------------------------------------------------------+ void CProgram::OnTesterPassEvent(void) { //--- Processa os resultados dos testes obtidos e exibe o gráfico if(m_frame_gen.OnTesterPassEvent()) { UpdateStatTable(); UpdateParamTable(); } }
Depois de concluir a otimização, o método CProgram::CalculateProfitsAndLosses() calcula a razão percentual dos resultados positivos e negativos e exibe os dados na barra de status:
class CProgram : public CWndEvents { private: //--- Calcula a razão de resultados positivos e negativos void CalculateProfitsAndLosses(void); } //+------------------------------------------------------------------+ //| Calcula a razão de resultados positivos e negativos | //+------------------------------------------------------------------+ void CProgram::CalculateProfitsAndLosses(void) { //--- Sai se não houver quadros if(m_frame_gen.FramesTotal()<1) return; //--- Número de resultados negativos e positivos int losses =m_frame_gen.LossesTotal(); int profits =m_frame_gen.ProfitsTotal(); //--- Razão percentual string pl =::DoubleToString(((double)losses/(double)m_frame_gen.FramesTotal())*100,2); string pp =::DoubleToString(((double)profits/(double)m_frame_gen.FramesTotal())*100,2);; //--- Exibir na barra de status m_status_bar.SetValue(1,"Profits: "+(string)profits+" ("+pp+"%)"+" / Losses: "+(string)losses+" ("+pl+"%)"); m_status_bar.GetItemPointer(1).Update(true); }
O código do método para processamento do evento TesterDeinit é exibido abaixo. Inicializando o núcleo gráfico significa que o movimento do cursor do mouse deve ser rastreado e o timer deve ser ligado. Infelizmente, na versão atual da MetaTrader 5, o cronômetro não liga quando a otimização é concluída. Vamos esperar que esta oportunidade apareça no futuro.
//+------------------------------------------------------------------+ //| Evento de conclusão da otimização | //+------------------------------------------------------------------+ void CProgram::OnTesterDeinitEvent(void) { //--- Conclusão da otimização m_frame_gen.OnTesterDeinitEvent(); //--- Torna a interface acessível IsLockedGUI(true); //--- Calcula a razão de resultados positivos e negativos CalculateProfitsAndLosses(); //--- inicializa o núcleo da GUI CWndEvents::InitializeCore(); }
Agora nós também podemos trabalhar com os dados dos quadros após a conclusão da otimização. O EA é colocado no gráfico do terminal e os quadros podem ser acessados para analisar os resultados. A interface gráfica torna tudo intuitivo. Dentro do método do manipulador do evento CProgram::OnEvent(), nós rastreamos:
- mudanças no campo de entrada para definir o número de séries de saldo exibidas no gráfico;
- iniciando a visualização dos resultados da otimização.
O método CProgram::UpdateBalanceGraph() é usado para atualizar o gráfico depois de alterar o número de séries. Aqui, nós definimos o número de séries para trabalhar no gerador de quadros e depois reservamos esse número no gráfico.
class CProgram : public CWndEvents { private: //--- Atualiza o gráfico void UpdateBalanceGraph(void); }; //+------------------------------------------------------------------+ //| Atualiza o gráfico | //+------------------------------------------------------------------+ void CProgram::UpdateBalanceGraph(void) { //--- Define o número de séries para o trabalho int curves_total=(int)m_curves_total.GetValue(); m_frame_gen.SetCurvesTotal(curves_total); //--- Exclui as séries CGraphic *graph=m_graph1.GetGraphicPointer(); int total=graph.CurvesTotal(); for(int i=total-1; i>=0; i--) graph.CurveRemoveByIndex(i); //--- Adiciona as séries double data[]; for(int i=0; i<curves_total; i++) graph.CurveAdd(data,CURVE_LINES,""); //--- Atualiza o gráfico graph.CurvePlotAll(); graph.Update(); }
No manipulador de eventos, o método CProgram::UpdateBalanceGraph() é chamado quando alterna-se os botões no campo de entrada (ON_CLICK_BUTTON) e quando o é inserido um valor no campo do teclado (ON_END_EDIT):
//+------------------------------------------------------------------+ //| Manipulador de eventos | //+------------------------------------------------------------------+ void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- Botão pressionando eventos if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON) { //--- Altera o número de séries no gráfico if(lparam==m_curves_total.Id()) { UpdateBalanceGraph(); return; } return; } //--- Evento de inserção de um valor no campo de entrada if(id==CHARTEVENT_CUSTOM+ON_END_EDIT) { //--- Altera o número de séries no gráfico if(lparam==m_curves_total.Id()) { UpdateBalanceGraph(); return; } return; } }
Para ver os resultados após a otimização na classe CFrameGenerator, o método público CFrameGenerator::ReplayFrames() é implementado. Aqui, no início, nós definimos o seguinte pelo contador de quadros: se o processo acabou de ser iniciado, os arrays são definidos como zero, e o ponteiro de quadros é deslocado para o início da lista. Posteriormente, os quadros são percorridos e as mesmas ações, como descritas anteriormente no método CFrameGenerator::OnTesterPassEvent() são executados. Se um quadro for recebido, o método retorna true. Após a conclusão, os contadores de quadro e série são definidos como zero e o método retorna false.
class CFrameGenerator { public: //--- Percorre os quadros bool ReplayFrames(void); }; //+------------------------------------------------------------------+ //| Reproduz os quadros após a conclusão da otimização | //+------------------------------------------------------------------+ bool CFrameGenerator::ReplayFrames(void) { //--- Define o ponteiro do quadro para o início if(m_frames_counter<1) { ArraysFree(); ::FrameFirst(); } //--- Inicia o ciclo pelos quadros if(::FrameNext(m_pass,m_name,m_id,m_value,m_data)) { //--- Obtém as entradas do EA, para as quais um quadro foi formado ::FrameInputs(m_pass,m_param_data,m_par_count); //--- Salva os parâmetros do resultado estatístico para o array SaveStatData(); //--- Atualiza o gráfico de resultados e balanceamento UpdateResultsGraph(); UpdateBalanceGraph(); //--- Aumenta o contador de quadros processados m_frames_counter++; return(true); } //--- Ciclo completo m_frames_counter =0; m_last_serie_index =0; return(false); }
O método CFrameGenerator::ReplayFrames() é chamado na classe CProgram do método ViewOptimizationResults(). Antes de lançar os quadros, a interface gráfica fica indisponível. A velocidade de rolagem pode ser ajustada especificando uma pausa no campo de entrada Sleep. Enquanto isso, a barra de status exibe a barra de progresso mostrando o tempo antes do final do processo.
class CFrameGenerator { private: //--- Visualiza os resultados da otimização void ViewOptimizationResults(void); }; //+------------------------------------------------------------------+ //| Visualiza os resultados da otimização | //+------------------------------------------------------------------+ void CProgram::ViewOptimizationResults(void) { //--- Torna a interface indisponível IsAvailableGUI(false); //--- Pausa int pause=(int)m_sleep_ms.GetValue(); //--- Reproduz os quadros while(m_frame_gen.ReplayFrames() && !::IsStopped()) { //--- Atualiza as tabelas UpdateStatTable(); UpdateParamTable(); //--- Atualiza a barra de progresso m_progress_bar.Show(); m_progress_bar.LabelText("Replay frames: "+string(m_frame_gen.CurrentFrame())+"/"+string(m_frame_gen.FramesTotal())); m_progress_bar.Update((int)m_frame_gen.CurrentFrame(),(int)m_frame_gen.FramesTotal()); //--- Pausa ::Sleep(pause); } //--- Calcula a razão de resultados positivos e negativos CalculateProfitsAndLosses(); //--- Oculta a barra de progresso m_progress_bar.Hide(); //--- Disponibiliza a interface IsAvailableGUI(true); m_reply_frames.MouseFocus(false); m_reply_frames.Update(true); }
O método CProgram::ViewOptimizationResults() é chamado pressionando o botão Replay Frames na interface gráfica da aplicação. O evento ON_CLICK_BUTTON é gerado.
//+------------------------------------------------------------------+ //| Manipulador de eventos | //+------------------------------------------------------------------+ void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- Evento de pressionar os botões if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON) { //--- Visualiza os resultados da otimização if(lparam==m_reply_frames.Id()) { ViewOptimizationResults(); return; } //--- ... return; } }
Agora é hora de ver os resultados e definir o que um usuário realmente vê no gráfico durante a otimização ao trabalhar com quadros.
Exibindo os resultados obtidos
Para testes, nós usaremos o algoritmo de negociação padrão — Moving Average. Vamos implementá-lo como uma classe ("como está") sem adições e correções. Todos os arquivos do aplicativo desenvolvido devem estar localizados na mesma pasta. O arquivo da estratégia está incluso no arquivo Program.mqh.
O FormatString.mqh está incluído aqui como uma adição com funções para formatação de linhas. Eles ainda não fazem parte de nenhuma classe, então vamos marcar a flecha com a cor preta. A estrutura do aplicativo resultante é a seguinte:
Fig. 4. Incluindo a classe da estratégia de negociação e o arquivo com as funções adicionais
Vamos tentar otimizar os parâmetros e ver como fica o gráfico no terminal. Configurações do testador: EURUSD H1, intervalo de tempo 01.01.2017 - 01.01.2018.
Fig. 5. Demonstrando o resultado do EA Moving Average a partir do pacote padrão
Como podemos ver, ele acabou por ser bastante informativo. Quase todos os resultados para este algoritmo de negociação são negativos (95,23%). Se nós aumentarmos o intervalo de tempo, eles se tornarão ainda piores. No entanto, ao desenvolver um sistema de negociação, nós devemos nos certificar de que a maioria dos resultados seja positiva. Caso contrário, o algoritmo é deficitário e não deve ser usado. É necessário otimizar os parâmetros em mais dados e garantir que haja tantos negócios quanto possível.
Vamos tentar testar outro algoritmo de negociação a partir do pacote padrão — MACD Sample.mq5. Ele já está implementado como uma classe. Após pequenas melhorias, nós podemos simplesmente conectá-lo ao nosso aplicativo, como o anterior. Nós devemos testá-lo no mesmo símbolo e tempo gráfico. Embora devamos aumentar o intervalo de tempo para mais negociações nos testes (01.01.2010 - 01.01.2018). Abaixo está o resultado de otimização do EA de negociação:
Fig. 6. Mostrando o resultado da otimização do MACD Sample
Aqui nós vemos um resultado muito diferente: 90.89% de resultados positivos.
A otimização dos parâmetros pode demorar muito, dependendo da quantidade de dados usada. Você não precisa se sentar em frente ao seu PC durante todo o processo. Após a otimização, você pode iniciar a visualização repetida dos resultados no modo acelerado pressionando Replay frames. Vamos começar a reproduzir os quadros com o limite de exibição de 25 séries. É assim que ele se parece:
Fig. 7. Exibição do resultado do EA MACD Sample após a otimização
Conclusão
Neste artigo, nós apresentamos a versão moderna do programa para receber e analisar os quadros de otimização. Os dados são visualizados no ambiente de interface gráfica desenvolvido com base na biblioteca EasyAndFast.
Uma desvantagem dessa solução é que, ao concluir a otimização no modo de processamento de quadros, é impossível iniciar o cronômetro. Isso impõe algumas limitações ao trabalho com a mesma interface gráfica. A segunda questão é que a desinicialização na função OnDeinit() não é acionada ao remover o EA do gráfico. Isso interfere no processamento correto do evento. Talvez, essas questões serão resolvidas nas versões futuras da MetaTrader 5 .
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/4395
- 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