English Русский 中文 Español Deutsch 日本語
Os 100 melhores passes de otimização (parte 1). Desenvolvimento de um analisador de otimizações

Os 100 melhores passes de otimização (parte 1). Desenvolvimento de um analisador de otimizações

MetaTrader 5Testador | 18 dezembro 2018, 11:51
4 156 0
Andrey Azatskiy
Andrey Azatskiy

Introdução

A tecnologia moderna tornou-se tão profundamente arraigada no campo da negociação financeira que é quase impossível imaginar como nós poderíamos viver sem ela. No entanto, há pouco tempo atrás, as negociações eram conduzidas manualmente e havia um sistema complexo de linguagem de mão (cada vez mais esquecido nos dias de hoje), que descrevia a quantidade do ativo que se gostaria de comprar ou vender.

Os computadores pessoais rapidamente substituíram os métodos tradicionais de negociação, trazendo a negociação on-line literalmente para nossas casas. Agora nós podemos analisar as cotações de ativos em tempo real e tomar decisões apropriadas. Além disso, o advento das tecnologias on-line na indústria do mercado faz com que a categoria de traders manuais diminuíssem a uma velocidade crescente. Agora, mais da metade dos negócios são feitos por meio de algoritmos de negociação, e vale dizer que a MetaTrader 5 é o número um entre as plataformas mais convenientes para isso.

Mas, apesar de todas as vantagens dessa plataforma, ela tem várias desvantagens que eu tentei amenizar com o aplicativo descrito aqui. O artigo descreve o desenvolvimento do programa escrito inteiramente em MQL5 usando a biblioteca EasyAndFastGUI projetada para melhorar a seleção dos parâmetros de otimização dos algoritmos de negociação. Ele também adiciona novos recursos para a análise retrospectiva da negociação e a avaliação geral do EA.



Primeiro, a otimização de EAs leva muito tempo. Naturalmente, isso se deve ao fato de que o testador gera ticks de maior qualidade (mesmo quando o OHLC é selecionado, quatro ticks são gerados para cada vela), bem como outros acréscimos que permitem uma melhor avaliação do EA. No entanto, em PCs domésticos que não são tão poderosos, a otimização pode levar vários dias ou semanas. Muitas vezes isso acontece após a escolha dos parâmetros do EA, logo nós percebemos que eles estão incorretos, e não há nada na mão além as estatísticas do passes da otimização e algumas métricas de avaliação.

Seria bom ter uma estatística completa de cada passe de otimização e a capacidade de filtragem (incluindo filtros condicionais) de cada um deles por múltiplos parâmetros. Também seria bom comparar as estatísticas de negociação com uma estratégia de Buy And Hold e impor todas as estatísticas uma à outra. Além disso, às vezes é necessário carregar todos os dados do histórico de negociação em um arquivo para o processamento subsequente dos resultados de cada negócio.

Às vezes, nós também podemos querer ver que tipo de desvio o algoritmo é capaz de suportar e como o algoritmo se comporta em um determinado intervalo de tempo, uma vez que algumas estratégias dependem do tipo de mercado. Uma estratégia baseada em mercados lateralizados pode servir como exemplo. Ele perde durante períodos de tendência e lucra durante as lateralizações. Também seria bom visualizar determinados intervalos (por datas) como um conjunto completo de métricas e outras adições (em vez de um simples em um gráfico de preços) separadamente do gráfico de PL geral.

Nós também devemos prestar atenção aos forward tests (teste fora da amostra). Eles são muito informativos, mas seus gráficos são exibidos como uma continuação do gráfico anterior no relatório padrão do testador de estratégia. Traders iniciantes podem facilmente concluir que seu robô perdeu drasticamente todos o seu lucro e depois começou a se recuperar (ou pior, ficou negativo). No programa descrito aqui, todos os dados são revisados em termos do tipo de otimização (mesmo no forward ou no histórico).

Também é importante mencionar o Santo Graal, que muitos desenvolvedores de EAs tanto buscam. Alguns robôs fazem 1000% ou mais por mês. Pode parecer que eles "batem" o mercado (estratégia Buy and Hold), mas na prática real, tudo parece muito diferente. Como o programa descrito apresenta, esses robôs podem realmente fazer 1000%, mas não batem o mercado.

O programa caracteriza a separação de uma análise entre a negociação usando um robô com um lote cheio (aumentando/reduzindo, etc…), como também a imitação de uma negociação pelo robô que usa um lote único (lote mínimo disponível para a negociação). Ao construir o gráfico de negociação Buy and Hold, o programa descrito considera o gerenciamento de lotes realizado pelo robô (ou seja, ele compra mais o ativo quando o lote é aumentado e reduz a quantidade do ativo comprado quando o lote é reduzido). Se nós compararmos esses dois gráficos, o meu robô de teste, que mostrou resultados irreais em um de seus melhores passes de otimização, não conseguiu superar o mercado. Portanto, para uma avaliação mais objetiva das estratégias de negociação, nós devemos dar uma olhada no gráfico de negociação de um lote, no qual o PL do robô e da estratégia de Buy and Hold são exibidos como se negociassem com volume mínimo permitido para negociação (PL = Lucro/Perda - gráfico do lucro obtido pelo tempo).

Agora, vamos dar uma olhada mais detalhada em como o programa foi desenvolvido.


Estrutura do analisador de otimização

A estrutura do programa pode ser expressa graficamente da seguinte forma:


O analisador de otimização resultante não está vinculado a nenhum robô em particular, não fazendo parte dele. No entanto, devido às especificidades da construção de interfaces gráficas em MQL5, o modelo de desenvolvimento do EA em MQL5 foi usado como base do programa. Como o programa se verificou bem grande (milhares de linhas de código), para uma maior especificidade e consistência, ele foi dividido em vários blocos (exibidos no diagrama acima) que, por sua vez, foram divididos em classes. O modelo robot é apenas o ponto de partida para o início do aplicativo. Cada um dos blocos será considerado em mais detalhes abaixo. Aqui nós vamos descrever as relações entre eles. Para trabalhar com o aplicativo, nós precisaremos de:

O robô em si pode ser desenvolvido como você quiser (usando OOP, uma função dentro do modelo do robô, importando Dlls…). O mais importante é ele aplicar o modelo de desenvolvimento do robô fornecido pelo Assistente MQL5. Ele conecta um arquivo do bloco Database na qual a classe faz o upload dos dados necessários para o banco de dados após a localização de cada passe de otimização. Essa parte é independente e não depende do próprio aplicativo, já que o banco de dados é formado ao ativar o robô no testador de estratégia.

O bloco Calculation é uma melhoria continuada do meu artigo anterior "Apresentação personalizada do histórico de negociação e criação de gráficos para relatórios".

O bloco Database e Calculation são utilizados tanto no robô analisado quanto na aplicação descrita. Portanto, eles são colocados na pasta Include. Esses blocos executam a maior parte do trabalho e são conectados à interface gráfica por meio da classe presenter.

A classe presenter conecta os blocos separados do programa. Cada um dos blocos tem sua própria função na interface gráfica. Ele controla o pressionamento de botões e outros eventos, bem como o redirecionamento para outros blocos lógicos. Os dados obtidos a partir deles são retornados ao presenter, onde eles são processados e os gráficos apropriados são desenhados, as tabelas são preenchidas e ocorre outra interação com a parte gráfica.

A parte gráfica do programa não realiza nenhuma lógica conceitual. Em vez disso, ele cria apenas uma janela com a interface necessária e chama as funções apropriadas do presenter durante o evento de pressionamento do botão.

O programa em si está escrito como o Projeto MQL5, permitindo que você desenvolva de forma mais estruturada e coloque todos os arquivos necessários em um só lugar. O projeto apresenta ainda outra classe que será descrita no bloco Cálculo. Esta classe foi escrita especificamente para este programa. Ela ordena os passes de otimização usando o método que eu desenvolvi. Na verdade, ela serve para toda a guia "Optimisation selection", reduzindo a amostragem de dados por determinados critérios.

Classe de ordenação universal é uma adição independente ao programa. Ele não se encaixa em nenhum dos blocos, mas ainda continua sendo uma parte importante do programa. Portanto, nós consideraremos brevemente isso nesta parte do artigo.

Como o nome indica, a classe lida com a ordenação de dados. Seu algoritmo foi retirado de um site de terceiros - Ordenação por Seleção (em russo).

//+------------------------------------------------------------------+
//| E-num com um estilo de ordenação                                 |
//+------------------------------------------------------------------+
enum SortMethod
  {
   Sort_Ascending,// Ascendente
   Sort_Descendingly// Descendente
  };
//+------------------------------------------------------------------+
//| Classe que ordena o tipo de dados passado                        |
//+------------------------------------------------------------------+
class CGenericSorter
  {
public:
   // Construtor padrão
                     CGenericSorter(){method=Sort_Descendingly;}
   // Método de ordenação
   template<typename T>
   void              Sort(T &out[],ICustomComparer<T>*comparer);
   // Tipo de ordenação por seleção
   void Method(SortMethod _method){method=_method;}
   // Obtém o método de ordenação
   SortMethod Method(){return method;}
private:
   // Método de ordenação
   SortMethod        method;
  };

A classe contém o modelo Sort, que ordena os dados. O método do modelo permite ordenar quaisquer dados passados, incluindo classes e estruturas. O método de comparação de dados deve ser descrito em uma classe separada que implementa a interface IСustomComparer<T>. Eu tive que desenvolver minha própria interface do tipo IСomparer apenas porque na interface convencional do método IСomparer, os dados incluídos não são passados por referência, enquanto que a passagem por referência é uma das condições de passagem de estruturas para um método na linguagem MQL5.

O método de classe CGenericSorter::Method sobrecarrega o retorno e aceita o tipo de ordenação de dados (em ordem crescente ou decrescente). Esta classe é usada em todos os blocos do programa onde os dados são ordenados.


Gráficos

Aviso!


Ao desenvolver a interface gráfica, foi detectado um bug na biblioteca aplicada (EasyAndFastGUI) - o elemento gráfico ComboBox limpou algumas variáveis de forma incompleta durante o seu preenchimento. De acordo com as recomendações (em russo) do desenvolvedor da biblioteca, as seguintes alterações devem ser feitas para corrigir isso:

m_item_index_focus =WRONG_VALUE;
m_prev_selected_item =WRONG_VALUE;
m_prev_item_index_focus =WRONG_VALUE;

ao método CListView::Clear(const bool redraw=false).

O método está localizado na linha 600 do arquivo ListView.mqh. O caminho do arquivo:
Include\EasyAndFastGUI\Controls.

Se você não adicionar essas edições, o erro "Array out of range" será exibido algumas vezes durante a abertura do ComboBox e o aplicativo será fechado de forma anormal.


Para criar uma janela em MQL5 com base na biblioteca EasyAndFastGUI, é necessário uma classe que servirá como um contêiner para todo o preenchimento da janela subsequente. A classe deve ser derivada da classe CwindEvents. Os métodos devem ser redefinidos dentro da classe:

 //--- Inicialização/desinicialização
   void              OnDeinitEvent(const int reason){CWndEvents::Destroy();};
   //--- Manipulador de eventos do gráfico
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);//

O espaço em branco para a criação da janela deve ser o seguinte:

class CWindowManager : public CWndEvents
  {
public:
                     CWindowManager(void){presenter = NULL;};
                    ~CWindowManager(void){};
   //===============================================================================   
   // Chamando métodos e eventos:
   //===============================================================================
   //--- Inicialização/desinicialização
   void              OnDeinitEvent(const int reason){CWndEvents::Destroy();};
   //--- Manipulador de eventos do gráfico
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);

//--- Cria a interface gráfica do programa
   bool              CreateGUI(void);


private:
 //--- Janela principal
   CWindow           m_window;
  }

A janela em si é criada com o tipo Cwindow dentro da classe. No entanto, várias propriedades da janela devem ser definidas antes de exibir a janela. Neste caso específico, o método de criação de janelas é o seguinte:

bool CWindowManager::CreateWindow(const string text)
  {
//--- Adiciona o ponteiro da janela para o array de janela
   CWndContainer::AddWindow(m_window);
//--- Coordenadas
   int x=(m_window.X()>0) ? m_window.X() : 1;
   int y=(m_window.Y()>0) ? m_window.Y() : 1;
//--- Propriedades
   m_window.XSize(WINDOW_X_SIZE+25);
   m_window.YSize(WINDOW_Y_SIZE);
   m_window.Alpha(200);
   m_window.IconXGap(3);
   m_window.IconYGap(2);
   m_window.IsMovable(true);
   m_window.ResizeMode(false);
   m_window.CloseButtonIsUsed(true);
   m_window.FullscreenButtonIsUsed(false);
   m_window.CollapseButtonIsUsed(true);
   m_window.TooltipsButtonIsUsed(false);
   m_window.RollUpSubwindowMode(true,true);
   m_window.TransparentOnlyCaption(true);

//--- Define as dicas de ferramentas
   m_window.GetCloseButtonPointer().Tooltip("Close");
   m_window.GetFullscreenButtonPointer().Tooltip("Fullscreen/Minimize");
   m_window.GetCollapseButtonPointer().Tooltip("Collapse/Expand");
   m_window.GetTooltipButtonPointer().Tooltip("Tooltips");
//--- Cria o formulário
   if(!m_window.CreateWindow(m_chart_id,m_subwin,text,x,y))
      return(false);
//---
   return(true);
  }

Os pré-requisitos para esse método são uma string que adiciona a janela à matriz de janelas do aplicativo e cria o formulário. Posteriormente, quando o aplicativo estiver em execução e o evento OnEvent for acionado, um dos métodos da biblioteca será executado em um loop sobre todas as janelas listadas no array de janelas. Em seguida, ele passa por todos os elementos dentro da janela e procura por um evento relacionado ao clique em qualquer interface de gerenciamento ou o destaque de uma linha da tabela, etc. Portanto, ao criar cada nova janela do aplicativo, uma referência deve ser adicionada a essa janela no array de referência.

O aplicativo desenvolvido apresenta a interface dividida por guias. Existem 4 contêineres de guia:

//--- Guias
   CTabs             main_tab; // Guias principais
   CTabs             tab_up_1; // Guias com configurações e tabela de resultados
   CTabs             tab_up_2; // Guias com estatísticas e seleção de parâmetros, além de gráficos comuns
   CTabs             tab_down; // Guias com estatísticas e upload para um arquivo

Eles se parecem como segue no formulário (descrito em vermelho na captura de tela):

  • main_tab divide a tabela com todos os passes de otimização selecionados ("Optimisation Data") do resto da interface do programa. Esta tabela contém todos os resultados que satisfazem as condições do filtro na guia settings. Os resultados são ordenados pela métrica selecionada no ComboBox — Sort by. Os dados obtidos são transferidos para a tabela descrita no formulário ordenado. A guia com o restante da interface do programa contém outros 3 contêineres de guia.
  • tab_up_1 contém uma divisão nas configurações iniciais do programa e uma tabela com os resultados ordenados. Além dos filtros condicionais mencionados, a guia Settings serve para selecionar o banco de dados e inserir os dados adicionais. Por exemplo, você pode selecionar se deseja inserir todos os dados já adicionados à guia Optimisation Data da tabela para a tabela de resultados de seleção de dados ou apenas um determinado número dos melhores parâmetros (filtragem em ordem decrescente pela métrica selecionada) será suficiente.
  • tab_up_2 contém 3 guias. Cada um deles contém a interface executando três tipos diferentes de tarefas. A primeira guia contém o relatório completo em um passe de otimização selecionado e permite simular o desvio, além de considerar o histórico de negociações para um determinado período de tempo. O segundo serve como filtro para os passes de otimização e ajuda a definir a sensibilidade da estratégia para diferentes parâmetros e diminuir o número de resultados da otimização ao selecionar os intervalos mais adequados dos parâmetros de interesse. A última guia serve como uma representação gráfica da tabela de resultados da otimização e mostra o número total de parâmetros de otimização selecionados.
  • O tab_down apresenta cinco guias, quatro das quais são a apresentação de um relatório de negociação do EA durante a otimização com os parâmetros selecionados, enquanto a última guia está carregando os dados para um arquivo. A primeira guia apresenta uma tabela com as métricas estimadas. A segunda guia fornece a distribuição de lucros/perdas pelos dias de negociação. A terceira guia representa o gráfico de lucros e perdas imposta na estratégia de Buy and Hold (gráfico preto), enquanto a quarta guia representa as alterações em algumas métricas selecionadas ao longo do tempo, bem como alguns tipos interessantes e informativos de gráficos que podem ser obtidos pela análise dos resultados de negociação do EA.

O processo de criação das guias é semelhante — a única diferença é o conteúdo. Como exemplo, eu vou fornecer o método de criação da guia principal:

//+------------------------------------------------------------------+
//| Main Tab                                                         |
//+------------------------------------------------------------------+
bool CWindowManager::CreateTab_main(const int x_gap,const int y_gap)
  {
//--- Salva o ponteiro para o elemento principal
   main_tab.MainPointer(m_window);

//--- Array de largura da guia
   int tabs_width[TAB_MAIN_TOTAL];
   ::ArrayInitialize(tabs_width,45);
   tabs_width[0]=120;
   tabs_width[1]=120;
//---
   string tabs_names[TAB_UP_1_TOTAL]={"Analysis","Optimisation Data"};
//--- Propriedades
   main_tab.XSize(WINDOW_X_SIZE-23);
   main_tab.YSize(WINDOW_Y_SIZE);
   main_tab.TabsYSize(TABS_Y_SIZE);
   main_tab.IsCenterText(true);
   main_tab.PositionMode(TABS_LEFT);
   main_tab.AutoXResizeMode(true);
   main_tab.AutoYResizeMode(true);
   main_tab.AutoXResizeRightOffset(3);
   main_tab.AutoYResizeBottomOffset(3);
//---
   main_tab.SelectedTab((main_tab.SelectedTab()==WRONG_VALUE)? 0 : main_tab.SelectedTab());
//--- Adiciona as guias com as propriedades especificadas
   for(int i=0; i<TAB_MAIN_TOTAL; i++)
      main_tab.AddTab((tabs_names[i]!="")? tabs_names[i]: "Tab "+string(i+1),tabs_width[i]);
//--- Cria um elemento de controle
   if(!main_tab.CreateTabs(x_gap,y_gap))
      return(false);
//--- Adiciona um objeto ao array comum de grupos de objetos
   CWndContainer::AddToElementsArray(0,main_tab);
   return(true);
  }

Além do conteúdo que pode variar, as strings do código principal são as seguintes:

  1. Adicionar um ponteiro ao elemento principal — o contêiner de guias deve conhecer o elemento ao qual ele está atribuído
  2. String de criação do elemento de controle
  3. Adicionar um elemento à lista geral de controles.

Os elementos de controle são os próximos de acordo com a hierarquia. 11 tipos de elementos de controle foram utilizados na aplicação. Eles são todos criados de maneira semelhante, portanto os métodos que adicionam os elementos de controle foram escritos para criar cada um deles. Vamos considerar a implementação de apenas um deles:

bool CWindowManager::CreateLable(const string text,
                                 const int x_gap,
                                 const int y_gap,
                                 CTabs &tab_link,
                                 CTextLabel &lable_link,
                                 int tabIndex,
                                 int lable_x_size)
  {
//--- Salva o ponteiro para o elemento principal
   lable_link.MainPointer(tab_link);
//--- Atribui para a guia
   tab_link.AddToElementsArray(tabIndex,lable_link);

//--- Definições
   lable_link.XSize(lable_x_size);

//--- Criação
   if(!lable_link.CreateTextLabel(text,x_gap,y_gap))
      return false;

//--- Adiciona um objeto ao array de grupos de objetos gerais
   CWndContainer::AddToElementsArray(0,lable_link);
   return true;
  }

O elemento de controle passado (CTextLabel), junto com as guias, deve lembrar o elemento ao qual está designado como um contêiner. Por sua vez, o contêiner de guias lembra a guia em que o elemento está localizado. Depois disso, o elemento é preenchido com as configurações necessárias e os dados iniciais. Eventualmente, o objeto é adicionado ao array geral de objetos.

Semelhante aos rótulos, são adicionados outros elementos definidos dentro do contêiner como campos. Eu separei certos elementos e coloquei alguns deles na área 'protected' da classe. Estes são os elementos que não requerem acesso através do presenter. Alguns outros elementos foram colocados como 'public'. Estes são os elementos que definem algumas condições ou botões de opção, cujo estado deve ser verificado pelo presenter. Em outras palavras, todos os elementos e métodos, cujo acesso não é desejável, têm seus cabeçalhos nas partes "protected" ou "private" da classe, juntamente com a referência para o presenter. A adição da referência do presenter é feita na forma de um método público, em que a presença de um presenter já adicionado é verificada primeiro e, se a referência a ele ainda não tiver sido adicionada, o presenter será salvo. Isso é feito para evitar a substituição dinâmica do presenter durante a execução do programa.

A janela em si é criada no método CreateGUI:

bool CWindowManager::CreateGUI(void)
  {
//--- Cria a janela
   if(!CreateWindow("Optimisation Selection"))
      return(false);

//--- Cria as abas
   if(!CreateTab_main(120,20))
      return false;
   if(!CreateTab_up_1(3,44))
      return(false);
   int indent=WINDOW_Y_SIZE-(TAB_UP_1_BOTTOM_OFFSET+TABS_Y_SIZE-TABS_Y_SIZE);
   if(!CreateTab_up_2(3,indent))
      return(false);
   if(!CreateTab_down(3,33))
      return false;

//--- Cria os controles 
   if(!Create_all_lables())
      return false;
   if(!Create_all_buttons())
      return false;
   if(!Create_all_comboBoxies())
      return false;
   if(!Create_all_dropCalendars())
      return false;
   if(!Create_all_textEdits())
      return false;
   if(!Create_all_textBoxies())
      return false;
   if(!Create_all_tables())
      return false;
   if(!Create_all_radioButtons())
      return false;
   if(!Create_all_SepLines())
      return false;
   if(!Create_all_Charts())
      return false;
   if(!Create_all_CheckBoxies())
      return false;

// exibe a janela
   CWndEvents::CompletedGUI();

   return(true);
  }

Como pode ser visto em sua implementação, ele não cria diretamente nenhum elemento de controle em si, mas apenas chama outros métodos para a criação desses elementos. A sequência de código principal que deve ser incluída como final neste método é a CWndEvents::CompletedGUI();

Essas linhas completam a criação dos gráficos e plota na tela do usuário. A criação de cada elemento de controle (seja linhas de separação, rótulos ou botões) é implementada em métodos com um conteúdo semelhante e aplicando as abordagens mencionadas acima para a criação de elementos de controle gráficos. Os cabeçalhos do método podem ser encontrados na parte 'private' da classe:

//===============================================================================   
// Criação dos controles:
//===============================================================================
//--- Todos os rótulos
   bool              Create_all_lables();
   bool              Create_all_buttons();
   bool              Create_all_comboBoxies();
   bool              Create_all_dropCalendars();
   bool              Create_all_textEdits();
   bool              Create_all_textBoxies();
   bool              Create_all_tables();
   bool              Create_all_radioButtons();
   bool              Create_all_SepLines();
   bool              Create_all_Charts();
   bool              Create_all_CheckBoxies();

Falando de gráficos, é impossível pular a parte do modelo de evento. Para o processamento correto nos aplicativos gráficos desenvolvidos usando a EasyAndFastGUI, você precisará executar as seguintes etapas:

Criar o método do manipulador de eventos (por exemplo, pressionando o botão). Este método deve aceitar o 'id' e 'lparam' como parâmetros. O primeiro parâmetro indica o tipo de um evento gráfico, enquanto o segundo indica o ID de um objeto com o qual a interação ocorreu. A implementação dos métodos é semelhante em todos os casos:

//+------------------------------------------------------------------+
//| Btn_Update_Click                                                 |
//+------------------------------------------------------------------+
void CWindowManager::Btn_Update_Click(const int id,const long &lparam)
  {
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON && lparam==Btn_update.Id())
     {
      presenter.Btn_Update_Click();
     }
  }

Primeiro, é verificado a condição (se o botão foi pressionado ou o elemento da lista foi selecionado…). Em seguida, é verificado o lparam onde o ID passado ao método é comparado com o ID do elemento da lista solicitado.

Todas as declarações de eventos de pressionamento de botões estão localizados na parte "private" da classe. O evento deve ser chamado para obter uma resposta a ele. Os eventos declarados são chamados no método OnEvent sobrecarregado:

//+------------------------------------------------------------------+
//| OnEvent                                                          |
//+------------------------------------------------------------------+
void CWindowManager::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   Btn_Update_Click(id,lparam);
   Btn_Load_Click(id,lparam);
   OptimisationData_inMainTable_selected(id,lparam);
   OptimisationData_inResults_selected(id,lparam);
   Update_PLByDays(id,lparam);
   RealPL_pressed(id,lparam);
   OneLotPL_pressed(id,lparam);
   CoverPL_pressed(id,lparam);
   RealPL_pressed_2(id,lparam);
   OneLotPL_pressed_2(id,lparam);
   RealPL_pressed_4(id,lparam);
   OneLotPL_pressed_4(id,lparam);
   SelectHistogrameType(id,lparam);
   SaveToFile_Click(id,lparam);
   Deals_passed(id,lparam);
   BuyAndHold_passed(id,lparam);
   Optimisation_passed(id,lparam);
   OptimisationParam_selected(id,lparam);
   isCover_clicked(id,lparam);
   ChartFlag(id,lparam);
   show_FriquencyChart(id,lparam);
   FriquencyChart_click(id,lparam);
   Filtre_click(id,lparam);
   Reset_click(id,lparam);
   RealPL_pressed_3(id,lparam);
   OneLotPL_pressed_3(id,lparam);
   ShowAll_Click(id,lparam);
   DaySelect(id,lparam);
  }

O método, por sua vez, é chamado a partir do modelo robot. Assim, o modelo de evento se estende do modelo robot (fornecido abaixo) para a interface gráfica. A GUI executa todo o processamento, ordenação e redirecionamento para a manipulação subsequente no presenter. O modelo robot em si é um ponto de partida do programa. Ele parece como se segue:

#include "Presenter.mqh"

CWindowManager _window;
CPresenter Presenter(&_window);
//+------------------------------------------------------------------+
//| Função de inicialização do Expert                                |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   if(!_window.CreateGUI())
     {
      Print(__FUNCTION__," > Failed to create the graphical interface!");
      return(INIT_FAILED);
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Função de desinicialização do Expert                             |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   _window.OnDeinitEvent(reason);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnChartEvent(const int    id,
                  const long   &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   _window.ChartEvent(id,lparam,dparam,sparam);
  }
//+------------------------------------------------------------------+

Trabalhando com o banco de dados

Antes de considerar esta parte bastante extensa do projeto, vale a pena dizer algumas palavras sobre a escolha feita. Um dos objetivos iniciais do projeto era fornecer a capacidade de trabalhar com os resultados da otimização depois de concluir a otimização em si, bem como a disponibilidade desses resultados a qualquer momento. Salvar os dados em um arquivo foi descartado imediatamente como sendo inadequado. Isso exigiria a criação de várias tabelas (formando, de fato, uma única tabela grande, mas com um número diferente de linhas) ou arquivos.

Nem sendo muito conveniente. Além disso, o método é mais difícil de implementar. O segundo método é a criação dos quadros de otimização. O kit de ferramentas em si é bom, mas não vamos trabalhar com as otimizações durante o processo de otimização. Além disso, a funcionalidade de quadros não é tão boa quanto a do banco de dados. Além disso, os quadros são projetados para a MetaTrader, enquanto que o banco de dados pode ser usado em qualquer programa analítico de terceiros, se necessário.

A seleção do banco de dados correto foi fácil. Nós precisávamos de um banco de dados rápido e popular que fosse conveniente para se conectar e não exigir nenhum software adicional. O banco de dados Sqlite atende a todos os critérios. As características mencionadas torna-o tão popular. Para usá-lo, conecte os bancos de dados fornecidos pelo provedor ao projeto Dll. Os dados da DLL são escritos em C e são facilmente vinculados aos aplicativos em MQL5, o que é uma boa adição, já que você não precisa escrever uma única linha de código em uma linguagem de terceiros que complique o projeto. Entre as desvantagens dessa abordagem é que a Dll Sqlite não fornece uma API conveniente para trabalhar com o banco de dados e, portanto, é necessário descrever pelo menos o wrapper mínimo para trabalhar com o banco de dados. Um exemplo de escrita desta funcionalidade foi eficientemente apresentado no artigo "SQL e MQL5: Trabalhando com Banco de Dados SQLite". Para este projeto, foi usado parte do código relacionado à interação com o WinApi e a importação de algumas funções da dll para a MQL5, que são mencionados no artigo. Quanto ao wrapper, eu decidi escrevê-lo sozinho.

Como resultado, o bloco de manipulação do banco de dados consiste na pasta Sqlite3, onde é descrito um wrapper conveniente para trabalhar com o banco de dados e a pasta OptimisationSelector foi criada especificamente para o programa desenvolvido. Ambas as pastas estão localizadas na pasta MQL5/Include. Como mencionado anteriormente, várias funções da biblioteca padrão do Windows são usadas para trabalhar com o banco de dados. Todas as funções desta parte do aplicativo estão localizadas na pasta WinApi. Além dos empréstimos mencionados, eu também usei o código para criar um recurso compartilhado (Mutex) da CodeBase. Ao trabalhar com o banco de dados a partir de duas fontes (ou seja, se o analisador de otimização abrir o banco de dados usado durante a otimização), os dados obtidos pelo programa devem estar sempre completos. É por isso que um recurso compartilhado é necessário. Acontece que, se um dos lados (processo de otimização ou analisador) ativar o banco de dados, o segundo aguarda até que sua contraparte conclua seu trabalho. O banco de dados Sqlite permite lê-lo de várias threads. Devido ao assunto do artigo, nós não consideraremos em detalhes o wrapper resultante para trabalhar com o banco de dados sqlite3 do MQL5. Em vez disso, nós descrevemos apenas alguns pontos de seus métodos de implementação e aplicação. Como já mencionado, o wrapper para trabalhar com o banco de dados está localizado na pasta Sqlite3. Existem três arquivos nele. Vamos analisá-los na ordem dos arquivos.

  • A primeira coisa que precisamos é importar da DLL as funções necessárias para trabalhar com o banco de dados. Como o objetivo era criar um wrapper contendo a funcionalidade mínima necessária, não importei nem 1% do número total de funções fornecidas pelos desenvolvedores de banco de dados. Todas as funções necessárias são importadas no arquivo sqlite_amalgmation.mqh. Essas funções são bem comentadas no site do desenvolvedor e também são rotuladas no arquivo acima. Se desejar, você pode importar o arquivo de cabeçalho inteiro da mesma maneira. O resultado será uma lista completa de todas as funções e, consequentemente, a possibilidade de acessá-las. A lista das funções importadas é a seguinte:

#import "Sqlite3_32.dll"
int sqlite3_open(const uchar &filename[],sqlite3_p32 &paDb);// Open the database
int sqlite3_close(sqlite3_p32 aDb); // Fecha o banco de dados
int sqlite3_finalize(sqlite3_stmt_p32 pStmt);// Completa a instrução
int sqlite3_reset(sqlite3_stmt_p32 pStmt); // Redefine a instrução
int sqlite3_step(sqlite3_stmt_p32 pStmt); // Move para a próxima linha ao ler a instrução
int sqlite3_column_count(sqlite3_stmt_p32 pStmt); // Calcula o número de colunas
int sqlite3_column_type(sqlite3_stmt_p32 pStmt,int iCol); // Obtém o tipo da coluna selecionada
int sqlite3_column_int(sqlite3_stmt_p32 pStmt,int iCol);// Converte o valor em int
long sqlite3_column_int64(sqlite3_stmt_p32 pStmt,int iCol); // Converter o valor em int64
double sqlite3_column_double(sqlite3_stmt_p32 pStmt,int iCol); // Converter o valor em double
const PTR32 sqlite3_column_text(sqlite3_stmt_p32 pStmt,int iCol);// Obtém o valor do texto
int sqlite3_column_bytes(sqlite3_stmt_p32 apstmt,int iCol); // Obtém o número de bytes ocupados pela linha da célula passada
int sqlite3_bind_int64(sqlite3_stmt_p32 apstmt,int icol,long a);// Combina a solicitação com um valor (do tipo int64)
int sqlite3_bind_double(sqlite3_stmt_p32 apstmt,int icol,double a);// Combine a solicitação com um valor (do tipo double)
int sqlite3_bind_text(sqlite3_stmt_p32 apstmt,int icol,char &a[],int len,PTRPTR32 destr);// Combina a solicitação tendo um valor (tipo string (char* - em C++))
int sqlite3_prepare_v2(sqlite3_p32 db,const uchar &zSql[],int nByte,PTRPTR32 &ppStmt,PTRPTR32 &pzTail);// Prepara a solicitação
int sqlite3_exec(sqlite3_p32 aDb,const char &sql[],PTR32 acallback,PTR32 avoid,PTRPTR32 &errmsg);// Execução em Sql
int sqlite3_open_v2(const uchar &filename[],sqlite3_p32 &ppDb,int flags,const char &zVfs[]); // Abra o banco de dados com os parâmetros
#import

Os bancos de dados fornecidos pelos desenvolvedores devem ser colocados na pasta Libraries e nomeados Sqlite3_32.dll e Sqlite3_64.dll de acordo com a contagem de bits para que o wrapper do banco de dados dll funcione. Você pode pegar os dados da Dll dos arquivos anexados ao artigo, compilá-los do Sqlite Amalgmation ou obter do site dos desenvolvedores do Sqlite. A presença deles é um pré-requisito para o programa. Você também precisa permitir que o EA importe a Dll.  

  • A segunda coisa é escrever um wrapper funcional para se conectar ao banco de dados. Essa deve ser uma classe que cria uma conexão com o banco de dados e a libera (desconecta do banco de dados) no destruidor. Além disso, ele deve ser capaz de executar os comandos em Sql de string simples, gerenciar transações e criar consultas (instruções). Toda a funcionalidade descrita foi implementada na classe CsqliteManager - é de sua criação que o processo de interação com o banco de dados é iniciado.

//+------------------------------------------------------------------+
//| Conexão do banco de dados e classe de gerenciamento              |
//+------------------------------------------------------------------+
class CSqliteManager
  {
public:
                     CSqliteManager(){db=NULL;} // Construtor vazio
                     CSqliteManager(string dbName); // Passa o nome
                     CSqliteManager(string dbName,int flags,string zVfs); // Passa o nome e as flags de conexão
                     CSqliteManager(CSqliteManager  &other) { db=other.db; } // Copia o construtor
                    ~CSqliteManager(){Disconnect();};// Destruidor

   void              Disconnect(); // Desconecta do banco de dados
   bool              Connect(string dbName,int flags,string zVfs); // Conexão paramétrica ao banco de dados
   bool              Connect(string dbName); // Conecta ao banco de dados pelo nome

   void operator=(CSqliteManager  &other){db=other.db;}// Operador de atribuição

   sqlite3_p64 DB() { return db; }; // Obtém o ponteiro para o banco de dados

   sqlite3_stmt_p64  Create_statement(const string sql); // Cria a instrução
   bool              Execute(string sql); // Executa o comando
   void              Execute(string  sql,int &result_code,string &errMsg); // Executa o comando e forneçe o código de erro e a mensagem

   void              BeginTransaction(); // Início da transação
   void              RollbackTransaction(); // Reversão da transação
   void              CommitTransaction(); // Confirma uma transação

private:
   sqlite3_p64       db; // Banco de dados

   void stringToUtf8(const string strToConvert,// String a ser convertida em um array na codificação utf-8
                     uchar &utf8[],// Array na codificação utf-8 da string strToConvert convertida deve ser colocada em
                     const bool untilTerminator=true)
     {    // Número de símbolos convertidos em codificação utf-8 e copiados para o array utf-8
      //---
      int count=untilTerminator ? -1 : StringLen(strToConvert);
      StringToCharArray(strToConvert,utf8,0,count,CP_UTF8);
     }
  };

Como pode ser visto no código, a classe resultante tem a capacidade de criar dois tipos de conexões no banco de dados (parâmetros textuais e de especificação). O método Create_sttement forma uma solicitação para o banco de dados e retorna um ponteiro para ele. As sobrecargas do método Exequte executam consultas de cadeia simples, enquanto os métodos de transação criam e aceitam/cancelam transações. A conexão com o próprio banco de dados é armazenada na variável db. Se aplicássemos o método Disconnect ou apenas criamos a classe usando o construtor padrão (ainda não tivemos tempo de conectar ao banco de dados), a variável é NULL. Ao chamar repetidamente o método Connect, nós desconectamos do banco de dados conectado anteriormente e nos conectamos ao novo. Como a conexão com o banco de dados requer a passagem de uma string no formato UTF-8, a classe tem um método 'private' especial que converte a string para o formato de dados necessário.

  • A próxima tarefa é criar um wrapper para o trabalho conveniente com as consultas (instrução). Uma solicitação para o banco de dados deve ser criada e destruída. Uma solicitação é criada pelo CsqliteManager, enquanto a memória não é gerenciada por nada. Em outras palavras, depois de criar uma solicitação, ele precisa ser destruído quando não for mais necessário, caso contrário, ele não permitirá a desconexão do banco de dados e, ao tentar concluir o trabalho com o banco de dados, nós obteremos uma exceção indicando que o banco de dados está ocupado. Além disso, uma classe wrapper de instrução deve ser capaz de preencher a solicitação com os parâmetros passados (quando ela é formada como "INSERT INTO table_1 VALUES(@ID,@Param_1,@Param_2);"). Além disso, uma determinada classe deve ser capaz de executar uma consulta colocada nele (método Exequte).

typedef bool(*statement_callback)(sqlite3_stmt_p64); // chamada de retorno realizada ao executar uma consulta. Se bem sucedido, é executado um 'true'
//+------------------------------------------------------------------+
//| Classe de uma consulta ao banco de dados                         |
//+------------------------------------------------------------------+
class CStatement
  {
public:
                     CStatement(){stmt=NULL;} // construtor vazio
                     CStatement(sqlite3_stmt_p64 _stmt){this.stmt=_stmt;} // Construtor com o parâmetro - ponteiro para a instrução
                    ~CStatement(void){if(stmt!=NULL)Sqlite3_finalize(stmt);} // Destruidor
   sqlite3_stmt_p64 get(){return stmt;} // Obtém o ponteiro para a instrução
   void              set(sqlite3_stmt_p64 _stmt); // Define o ponteiro para a instrução

   bool              Execute(statement_callback callback=NULL); // Executa a instrução
   bool              Parameter(int index,const long value); // Adiciona o parâmetro
   bool              Parameter(int index,const double value); // Adicione o parâmetro
   bool              Parameter(int index,const string value); // Adicione o parâmetro

private:
   sqlite3_stmt_p64  stmt;
  };

As sobrecargas do método Parameter preenchem os parâmetros da solicitação. O método 'set' salva a instrução passada para a variável 'stmt': se for descoberto que uma solicitação antiga já foi salva na classe antes de salvar a nova, o método Sqlite3_finalize é chamado para a solicitação salva anteriormente.

  • A classe final no wrapper de tratamento do banco de dados é a CSqliteReader, que é capaz de ler uma resposta do banco de dados. Semelhante às classes anteriores, a classe chama o método sqlite3_reset em seu destrutor — ele descarta a solicitação e permite que você trabalhe com ela novamente. Nas novas versões do banco de dados, a chamada dessa função não é necessária, mas foi deixado pelos desenvolvedores. Eu usei no wrapper apenas no caso de necessidade. Além disso, essa classe deve cumprir suas principais funções, ou seja, ler uma resposta de uma string do banco de dados pela string com a possibilidade de converter os dados lidos no formato apropriado.
//+------------------------------------------------------------------+
//| Classe de leitura das respostas dos bancos de dados              |
//+------------------------------------------------------------------+
class CSqliteReader
  {
public:
                     CSqliteReader(){statement=NULL;} // construtor vazio
                     CSqliteReader(sqlite3_stmt_p64 _statement) { this.statement=_statement; }; // Construtor aceitando o ponteiro para a instrução
                     CSqliteReader(CSqliteReader  &other) : statement(other.statement) {} // Copia do construtor
                    ~CSqliteReader() { Sqlite3_reset(statement); } // Destruidor

   void              set(sqlite3_stmt_p64 _statement); // Adiciona uma referência à instrução
   void operator=(CSqliteReader  &other){statement=other.statement;}// Operador de atribuição do leitor
   void operator=(sqlite3_stmt_p64 _statement) {set(_statement);}// Operador de atribuição da instrução

   bool              Read(); // Lê a string
   int               FieldsCount(); // Conta o número de colunas
   int               ColumnType(int col); // Obtém o tipo de coluna

   bool              IsNull(int col); // Verifica se o valor == SQLITE_NULL
   long              GetInt64(int col); // Converte em 'int'
   double            GetDouble(int col);// Converte em 'double'
   string            GetText(int col);// Converte em 'string'

private:
   sqlite3_stmt_p64  statement; // ponteiro para a instrução
  };

Agora que implementamos as classes descritas usando as funções para trabalhar com o banco de dados carregado do Sqlite3.dll, é hora de descrever as classes que trabalham com o banco de dados a partir do programa descrito.

A estrutura do banco de dados criado é a seguinte:

Tabela Buy And Hold:

  1. Time — eixo X (rótulo de intervalo de tempo)
  2. PL_total — lucro/perda se aumentarmos o lote em proporção ao robô
  3. PL_oneLot — lucro/perda se negociar um único lote constantemente
  4. DD_total — rebaixamento se negociar um lote da mesma forma que o EA negociou
  5. DD_oneLot — rebaixamento se estiver negociando um único lote
  6. isForvard — propriedade do gráfico de forward

Tabela OptimisationParams:

  1. ID — índice de entrada de preenchimento automático exclusivo no banco de dados
  2. HistoryBorder — histórico da data de conclusão da otimização
  3. TF — tempo gráfico
  4. Param_1...Param_n — parâmetro
  5. InitalBalance — valor do saldo inicial

Tabela ParamsCoefitients:

  1. ID — chave externa, referência ao OptimisationParams(ID)
  2. isForvard — propriedade da otimização de forward
  3. isOneLot — propriedade do gráfico em que a métrica foi baseada
  4. DD — rebaixamento
  5. averagePL — lucro/perda média pelo gráfico PL
  6. averageDD — rebaixamento médio
  7. averageProfit — lucro médio
  8. profitFactor — fator de lucro
  9. recoveryFactor — fator de recuperação
  10. sharpRatio — Sharpe ratio
  11. altman_Z_Score — Altman Z score
  12. VaR_absolute_90 — VaR 90
  13. VaR_absolute_95 — VaR 95
  14. VaR_absolute_99 — VaR 99
  15. VaR_growth_90 — VaR 90
  16. VaR_growth_95 — VaR 95
  17. VaR_growth_99 — VaR 99
  18. winCoef — taxa de acerto
  19. customCoef — métrica personalizada

Tabela ParamType:

  1. ParamName — nome do parâmetro do robô
  2. ParamType — tipo do parâmetro do robô (int/double/string)

Tabela TradingHistory

  1. ID — chave externa, referência ao OptimisationParams(ID)
  2. isForvard — flag do forward test
  3. Symbol — símbolo
  4. DT_open — data de abertura
  5. Day_open — dia de abertura
  6. DT_close — data de fechamento
  7. Day_close — dia de fechamento
  8. Volume — número de lotes
  9. isLong — propriedade comprado/vendido
  10. Price_in — preço de entrada
  11. Price_out — preço de saída
  12. PL_oneLot — lucro ao negociar um único lote
  13. PL_forDeal — lucro ao negociar como nós fizemos anteriormente
  14. OpenComment — comentário de entrada
  15. CloseComment — comentário de saída

Com base na estrutura do banco de dados fornecido, nós podemos ver que algumas tabelas usam a chave externa para se referir à tabela OptimisationParams onde armazenamos os parâmetros do EA. Cada coluna de um parâmetro de entrada leva seu nome (por exemplo, Fast/Slow — fast/slow moving average). Além disso, cada coluna deve ter um formato de dados específico. Muitos bancos de dados Sqlite são criados sem definir o formato de dados da coluna da tabela. Nesse caso, todos os dados são armazenados como linhas. No entanto, nós precisamos saber o formato exato dos dados, já que devemos ordenar as métricas por uma determinada propriedade, o que significa a conversão dos dados carregados do banco de dados para o formato original.

Para fazer isso, nós devemos saber seu formato antes de inserir os dados no banco de dados. Várias opções são possíveis: criar um método modelo e transferir o conversor para ele ou criar uma classe, que, de fato, é um armazenamento universal de vários tipos de dados (qualquer tipo de dado pode ser convertido) combinado com o nome da variável do EA. Eu selecionei a segunda opção e criei a classe CDataKeeper. A classe descrita pode armazenar 3 tipos de dados [int, double, string], enquanto que todos os outros tipos de dados que podem ser usados como os formatos de entrada do EA podem ser convertidos para eles de uma forma ou de outra.

//+------------------------------------------------------------------+
//| ipos de dados de entrada do parâmetro do EA                      |
//+------------------------------------------------------------------+
enum DataTypes
  {
   Type_INTEGER,// int
   Type_REAL,// double, float
   Type_Text // string
  };
//+------------------------------------------------------------------+
//| Resultado da comparação de dois CDataKeeper                      |
//+------------------------------------------------------------------+
enum CoefCompareResult
  {
   Coef_Different,// diferentes tipos de dados ou nomes de variáveis
   Coef_Equal,// variáveis são iguais
   Coef_Less, // variável atual é menor que uma passada
   Coef_More // variável atual excede a passada
  };
//+---------------------------------------------------------------------+
//| Classe para armazenar uma entrada específica do robô                |                             
//| Ele pode armazenar os dados dos seguintes tipos: [int, double, string] 
//+---------------------------------------------------------------------+
class CDataKeeper
  {
public:
                     CDataKeeper(); // Construtor
                     CDataKeeper(const CDataKeeper&other); // Copia o construtor
                     CDataKeeper(string _variable_name,int _value); // Construtor paramétrico
                     CDataKeeper(string _variable_name,double _value); // Construtor paramétrico
                     CDataKeeper(string _variable_name,string _value); // Construtor paramétrico

   CoefCompareResult Compare(CDataKeeper &data); // Método de comparação

   DataTypes         getType(){return variable_type;}; // Obtém o tipo de dados
   string            getName(){return variable_name;}; // Obtém o nome do parâmetro
   string            valueString(){return value_string;}; // Obtém o parâmetro
   int               valueInteger(){return value_int;}; // Obtém o parâmetro
   double            valueDouble(){return value_double;}; // Obtém o parâmetro
   string            ToString(); // Converta qualquer parâmetro em uma string. Se este for um parâmetro de string, as aspas simples serão adicionadas à string de ambos os lados <<'>>

private:
   string            variable_name,value_string; // nome da variável e variável de string
   int               value_int; // Variável int
   double            value_double; // Variável double
   DataTypes         variable_type; // Tipo da variável

   int compareDouble(double x,double y) // Comparando a acurácia dos tipos double até 10 casas decimais
     {
      double diff=NormalizeDouble(x-y,10);
      if(diff>0) return 1;
      else if(diff<0) return -1;
      else return 0;
     }
  };

Três sobrecargas de construtor aceitam o nome da variável como o primeiro parâmetro, enquanto que o valor convertido em um dos tipos mencionados é aceito como o segundo. Esses valores são salvos nas variáveis globais da classe, iniciando com 'value_' seguido por uma indicação do tipo. O método getType() retorna o tipo como uma enumeração fornecida acima, enquanto o método getName() retorna o nome da variável. Os métodos que começam com 'value' retornam a variável do tipo solicitado, mas se o método valueDouble() é chamado, enquanto a variável armazenada na classe é do tipo 'int', é retornado NULL. O método ToString() converte o valor de qualquer uma das variáveis para o formato string. No entanto, se a variável era inicialmente uma string, as aspas simples são adicionadas a ela (para formar as solicitações em SQL de forma mais conveniente). O método Compare(CDataKeeper &ther) permite a comparação de dois objetos do tipo CDataKeeper, ao comparar:

  1. O nome da variável do EA
  2. O tipo da variável
  3. O valor da variável

Se as duas primeiras comparações não passarem, então nós estamos tentando comparar dois parâmetros diferentes (por exemplo, o período da média móvel rápida com o período da lenta) e, consequentemente, não podemos fazer isso porque nós só precisamos comparar os dados do mesmo tipo. Portanto, nós retornamos o valor Coef_Different do tipo CoefCompareResult. Em outros casos, uma comparação é feita e o resultado solicitado é retornado. O método de comparação em si é implementado da seguinte forma:

//+------------------------------------------------------------------+
//| Compara o parâmetro atual com o passado                          |
//+------------------------------------------------------------------+
CoefCompareResult CDataKeeper::Compare(CDataKeeper &data)
  {
   CoefCompareResult ans=Coef_Different;

   if(StringCompare(this. variable_name,data.getName())==0 && 
      this.variable_type==data.getType()) // Compara nomes e tipos
     {
      switch(this.variable_type) // Compare os valores
        {
         case Type_INTEGER :
            ans=(this.value_int==data.valueInteger() ? Coef_Equal :(this.value_int>data.valueInteger() ? Coef_More : Coef_Less));
            break;
         case Type_REAL :
            ans=(compareDouble(this.value_double,data.valueDouble())==0 ? Coef_Equal :(compareDouble(this.value_double,data.valueDouble())>0 ? Coef_More : Coef_Less));
            break;
         case Type_Text :
            ans=(StringCompare(this.value_string,data.valueString())==0 ? Coef_Equal :(StringCompare(this.value_string,data.valueString())>0 ? Coef_More : Coef_Less));
            break;
        }
     }
   return ans;
  }

A representação do tipo independente das variáveis permite usá-las de uma forma mais conveniente, levando em consideração o nome, o tipo de dados da variável e seu valor.

A próxima tarefa é criar o banco de dados descrito acima. A classe CDatabaseWriter é usada para isso.

//+---------------------------------------------------------------------------------+
//| Call-back calculando uma métrica do usuário                                     |
//| Dados do histórico e a flag do tipo history, 	em que o cálculo da métrica é necessário 
//| são passados para a entrada                                                     |
//+---------------------------------------------------------------------------------+
typedef double(*customScoring_1)(const DealDetales &history[],bool isOneLot);
//+---------------------------------------------------------------------------------+
//| Call-back calculando uma métrica do usuário                                     |
//| Conexão ao banco de dados (somente leitura), flag do tipo history e requested ratio  
//| são passados para a entrada                                                     |
//+---------------------------------------------------------------------------------+
typedef double(*customScoring_2)(CSqliteManager *dbManager,const DealDetales &history[],bool isOneLot);
//+---------------------------------------------------------------------------------+
//| Classe que salva os dados no banco de dados e cria o banco de dados antes disso |
//+---------------------------------------------------------------------------------+
class CDBWriter
  {
public:
   // Chamada de uma das redefinições para a OnInit
   void              OnInitEvent(const string DBPath,const CDataKeeper &inputData_array[],customScoring_1 scoringFunction,double r,ENUM_TIMEFRAMES TF=PERIOD_CURRENT); // call-back 1
   void              OnInitEvent(const string DBPath,const CDataKeeper &inputData_array[],customScoring_2 scoringFunction,double r,ENUM_TIMEFRAMES TF=PERIOD_CURRENT); // call-back 2
   void              OnInitEvent(const string DBPath,const CDataKeeper &inputData_array[],double r,ENUM_TIMEFRAMES TF=PERIOD_CURRENT);// Sem call-back e sem métrica do usuário (igual a zero)
   double            OnTesterEvent();// Chamada no OnTester
   void              OnTickEvent();// Chamada no OnTick

private:
   CSqliteManager    dbManager; // Conector para o banco de dados
   CDataKeeper       coef_array[]; // Parâmetros de entrada
   datetime          DT_Border; // A data da última vela (calculada em OnTickEvent)
   double            r; // Taxa livre de risco

   customScoring_1   scoring_1; // Call-back
   customScoring_2   scoring_2; // Call-back
   int               scoring_type; // Call-back tipo [1,2]
   string            DBPath; // Caminho para o banco de dados 
   double            balance; // Saldo
   ENUM_TIMEFRAMES   TF; // Tempo gráfico

   void              CreateDB(const string DBPath,const CDataKeeper &inputData_array[],double r,ENUM_TIMEFRAMES TF);// Cria o banco de dados e tudo que o acompanha
   bool              isForvard();// Definir o tipo da otimização atual (history/forward)
   void              WriteLog(string s,string where);// Entrada do arquivo de log

   int               setParams(bool IsForvard,CReportCreator *reportCreator,DealDetales &history[],double &customCoef);// Preenche a tabela de entradas
   void              setBuyAndHold(bool IsForvard,CReportCreator *reportCreator);// Preenche o histórico de Buy And Hold
   bool              setTraidingHistory(bool IsForvard,DealDetales &history[],int ID);// Preenche o histórico de negociaçãoy
   bool              setTotalResult(TotalResult &coefData,bool isOneLot,long ID,bool IsForvard,double customCoef);// Preenche as tabelas com as métricas
   bool              isHistoryItem(bool IsForvard,DealDetales &item,int ID); // Verifica se esses parâmetros já existem na tabela do histórico de negociação
  };

A classe é usada apenas no próprio robô personalizado. Seu objetivo é criar um parâmetro de entrada para o programa descrito, ou seja, o banco de dados com uma estrutura e conteúdo solicitados. Como nós podemos ver, ele tem 3 métodos públicos (o método de sobrecarga é considerado como um):

  • OnInitEvent
  • OnTesterEvent
  • OnTickEvent

Cada um deles é chamado nas call-backs correspondentes do modelo robot, onde os parâmetros necessários são passados para eles. O método OnInitEvent é projetado para preparar a classe para trabalhar com o banco de dados. Suas sobrecargas são implementadas da seguinte forma:

//+------------------------------------------------------------------+
//| Cria o banco de dados e a conexão                                |
//+------------------------------------------------------------------+
void CDBWriter::OnInitEvent(const string _DBPath,const CDataKeeper &inputData_array[],customScoring_2 scoringFunction,double _r,ENUM_TIMEFRAMES _TF)
  {
   CreateDB(_DBPath,inputData_array,_r,_TF);
   scoring_2=scoringFunction;
   scoring_type=2;
  }
//+------------------------------------------------------------------+
//| Cria o banco de dados e a conexão                                |
//+------------------------------------------------------------------+
void CDBWriter::OnInitEvent(const string _DBPath,const CDataKeeper &inputData_array[],customScoring_1 scoringFunction,double _r,ENUM_TIMEFRAMES _TF)
  {
   CreateDB(_DBPath,inputData_array,_r,_TF);
   scoring_1=scoringFunction;
   scoring_type=1;
  }
//+------------------------------------------------------------------+
//| Cria o banco de dados e a conexão                                |
//+------------------------------------------------------------------+
void CDBWriter::OnInitEvent(const string _DBPath,const CDataKeeper &inputData_array[],double _r,ENUM_TIMEFRAMES _TF)
  {
   CreateDB(_DBPath,inputData_array,_r,_TF);
   scoring_type=0;
  }

Como nós podemos ver na implementação do método, ele atribui valores obrigatórios aos campos da classe e cria o banco de dados. Os métodos de call-back devem ser implementados pelo usuário pessoalmente (se uma métrica personalizada deve ser calculada) ou uma sobrecarga sem um retorno de chamada é usada — nesse caso, uma taxa personalizada é igual a zero. A métrica de um usuário é um método personalizado para avaliar o passe de otimização do EA. Para implementá-lo, são criados os ponteiros para as duas funções com os dois tipos de dados possíveis necessários.

  • O primeiro (customScoring_1) recebe o histórico de negociação e a flag que define o passe da otimização que o cálculo é solicitado (lote negociado ou negociando um único lote - todos os dados para cálculos estão presentes no array passado).
  • O segundo tipo de call-back (customScoring_2) obtém o acesso ao banco de dados na qual o trabalho é executado, mas apenas com direitos de somente leitura para evitar edições inesperadas pelo usuário.
O método CreateDB é um dos principais métodos da classe. Ele executa a preparação completa para o trabalho:

  • Atribui o saldo, tempo gráfico e valores da taxa livre de risco.
  • Estabelece a conexão com o banco de dados e ocupa um recurso compartilhado (Mutex)
  • Cria o banco de dados da tabela, se ainda não foi criado.

O tick público OnTickEvent salva a data da vela de minuto em cada tick. Ao testar uma estratégia, é impossível definir se o passe atual é de forward ou não, enquanto o banco de dados tem um parâmetro semelhante. Mas nós sabemos que o testador executa os passes depois dos históricos. Assim, enquanto sobrescrevemos a variável com uma data em cada tick, nós descobrimos a última data no final do processo de otimização. A tabela OptimisationParams apresenta o parâmetro HistoryBorder. Ele é igual à data salva. As linhas são adicionadas a essa tabela somente durante a otimização histórica. Durante a primeira passagem com esses parâmetros (o mesmo que o passe de otimização histórica), a data é incluída no campo obrigatório no banco de dados. Se durante um dos próximos passes, nós vemos que a entrada com esses parâmetros já está presente no banco de dados, existem duas opções:

  1. ou um usuário, por alguns motivos, interrompeu a otimização histórica e depois iniciou ela novamente,
  2. ou esta é uma otimização de forward.

Para filtrar um do outro, nós comparamos a última data armazenada no passe atual com a data do banco de dados. Se a data atual for maior que a do banco de dados, então é um passe de forward, se for menor ou igual, você está lidando com um histórico. Considerando que a otimização deve ser lançada duas vezes com as mesmas métricas, nós inserimos apenas os novos dados no banco de dados ou cancelamos todas as alterações feitas durante o passe atual. O método OnTesterEvent() salva os dados no banco de dados. Ele é implementado da seguinte maneira:

//+------------------------------------------------------------------+
//| Salva todos os dados no banco de dados e retorna                 |
//| uma métrica personalizada                                        |
//+------------------------------------------------------------------+
double CDBWriter::OnTesterEvent()
  {

   DealDetales history[];

   CDealHistoryGetter historyGetter;
   historyGetter.getDealsDetales(history,0,TimeCurrent()); // Obtém o histórico de negociação

   CMutexSync sync; // objeto de sincronização 
   if(!sync.Create(getMutexName(DBPath))) { Print(Symbol()+" MutexSync create ERROR!"); return 0; }
   CMutexLock lock(sync,(DWORD)INFINITE); // bloqueia o segmento dentro dos colchetes

   bool IsForvard=isForvard(); // Descobre se a iteração atual do testador é um forward
   CReportCreator rc;
   string Symb[];
   rc.Get_Symb(history,Symb); // Obtém a lista de símbolos
   rc.Create(history,Symb,balance,r); // Cria um relatório (o relatório Buy And Hold é criado automaticamente)

   double ans=0;
   dbManager.BeginTransaction(); // Início da transação

   CStatement stmt(dbManager.Create_statement("INSERT OR IGNORE INTO ParamsType VALUES(@ParamName,@ParamType);")); // Solicitação para salvar a lista de tipos de parâmetros do EA
   if(stmt.get()!=NULL)
     {
      for(int i=0;i<ArraySize(coef_array);i++)
        {
         stmt.Parameter(1,coef_array[i].getName());
         stmt.Parameter(2,(int)coef_array[i].getType());
         stmt.Execute(); // salvar os tipos de parâmetro e seus nomes
        }
     }

   int ID=setParams(IsForvard,&rc,history,ans); // Salva os parâmetros do EA, bem como as taxas de avaliação e obtém o ID
   if(ID>0)// Se ID > 0, os parâmetros são salvos com sucesso 
     {
      if(setTraidingHistory(IsForvard,history,ID)) // Salva o histórico de negociação e verifica se ele está salvo
        {
         setBuyAndHold(IsForvard,&rc); // Salva o histórico de Buy And Hold (salvo apenas uma vez - durante a primeira gravação)
         dbManager.CommitTransaction(); // Confirma o final de uma transação
        }
      else dbManager.RollbackTransaction(); // Caso contrário, cancela a transação
     }
   else dbManager.RollbackTransaction(); // Caso contrário, cancela a transação

   return ans;
  }

A primeira coisa que o método faz é formar o histórico de negociação usando a classe descrita em meu artigo anterior. Em seguida, ele pega o recurso compartilhado (Mutex) e salva os dados. Para conseguir isso, primeiro defina se o passe de otimização atual é de forward (de acordo com o método descrito acima), então obtenha a lista de símbolos (todos os símbolos que foram negociados).

Consequentemente, se um EA de negociação de spread foi testada, por exemplo, o histórico de negociação é carregado em ambos os símbolos em que a negociação foi realizada. Depois disso, um relatório é gerado (usando a classe revisada abaixo) e gravado no banco de dados. Uma transação é criada para o registro correto. A transação é cancelada se ocorrer um erro ao preencher qualquer uma das tabelas ou se os dados incorretos forem obtidos. Primeiro, as métricas são salvas e, em seguida, se tudo correr bem, nós salvamos o histórico de negociação seguido pelo histórico de Buy and Hold. Este último é salvo apenas uma vez durante a primeira entrada de dados. No caso de um erro de salvamento de dados, o arquivo de log é gerado na pasta Common/Files.

Depois de criar o banco de dados, ele deve ser lido. A classe de leitura do banco de dados já é usada no programa descrito. Ele é mais simples e se parece com o seguinte:

//+------------------------------------------------------------------+
//| Classe de leiturda dos dados do banco de dados                   |
//+------------------------------------------------------------------+
class CDBReader
  {
public:
   void              Connect(string DBPath);// Método de conexão ao banco de dados

   bool              getBuyAndHold(BuyAndHoldChart_item &data[],bool isForvard);// Método que calcula o histórico de Buy And Hold
   bool              getTraidingHistory(DealDetales &data[],long ID,bool isForvard);// Método de cálculo do histórico negociado pelo EA 
   bool              getRobotParams(CoefData_item &data[],bool isForvard);// Método de cálculo dos parâmetros e métricas do EA

private:
   CSqliteManager    dbManager; // Gerenciador do banco de dados 
   string            DBPath; // Caminho para o banco de dados

   bool              getParamTypes(ParamType_item &data[]);// Calcula os tipos de entrada e seus nomes.
  };

Ele implementa 3 métodos públicos de leitura de 4 tabelas nas quais nós estamos interessados e cria arrays de estruturas com os dados dessas tabelas.

  • O primeiro método (getBuyAndHold) retorna o histórico de BuyAndHold por referência para os períodos de forward e histórico, dependendo da flag passada. Se o upload for bem sucedido, o método retornará 'true', caso contrário, 'false'. O upload é realizado a partir da tabela Buy And Hold.
  • O método getTradingHistory também retorna o histórico de negociações correspondente ao ID passado e a flag isForvard. O upload é realizado a partir da tabela TradingHistory.
  • O método getRobotParams combina os carregamentos das duas tabelas: ParamsCoefitients — de onde os parâmetros do robô são obtidos e OptimisationParams onde as métricas de avaliação calculadas estão localizadas.

Assim, as classes escritas permitem que você não trabalhe mais diretamente com o banco de dados, mas com as classes que fornecem os dados necessários, ocultando todo o algoritmo para trabalhar com o banco de dados. Essas classes, por sua vez, trabalham com o wrapper escrito para o banco de dados, o que também simplifica o trabalho. O wrapper mencionado trabalha com o banco de dados via DLL fornecido pelos desenvolvedores do banco de dados. O próprio banco de dados atende a todas as condições exigidas e, na verdade, é um arquivo que o torna conveniente para transporte e processamento, tanto neste programa quanto em outras aplicações analíticas. Outra vantagem dessa abordagem é o fato de que a operação de longo prazo de um único algoritmo permite coletar bancos de dados de cada otimização, acumulando, assim, o histórico e monitorando os padrões de alteração dos parâmetros.


Cálculos

O bloco consiste em duas classes. O primeiro destina-se a gerar um relatório de negociação e é uma versão melhorada da classe gerando um relatório de negociação descrito no artigo anterior.

O segundo é uma classe de filtro. Ele classifica as amostras de otimização em um intervalo passado e é capaz de criar um gráfico exibindo uma frequência de negociações lucrativas e com prejuízo para cada valor da métrica de otimização individual. Outro objetivo dessa classe é criar um gráfico de distribuição normal para o PL efetivamente negociado no final da otimização (ou seja, PL para todo o período de otimização). Em outras palavras, se houver 1000 entradas de otimização, nós teremos 1000 resultados de otimização (PL como no final da otimização). A distribuição que nós estamos interessados é baseada neles.

Esta distribuição mostra em qual direção a assimetria dos valores obtidos é alterada. Se a cauda maior e o centro da distribuição estiverem na zona de lucro, o robô gera principalmente otimizações lucrativas e, consequentemente, é bom, caso contrário, gera muitos passes não lucrativos. Se a assimetria da definição for transferida para a zona de prejuízo, isso também significa que os parâmetros selecionados causam principalmente perdas em vez de lucros.

Vamos dar uma olhada neste bloco começando com a classe gerando um relatório de negociação. A classe descrita está localizada na pasta Include da pasta "History manager" e possui o seguinte cabeçalho:

//+------------------------------------------------------------------+
//| Classe para gerar as estatísticas do histórico de negociação     |
//+------------------------------------------------------------------+
class CReportCreator
  {
public:

   //=============================================================================================================================================
   // Cálculo/ recálculo:
   //=============================================================================================================================================

   void              Create(DealDetales &history[],DealDetales &BH_history[],const double balance,const string &Symb[],double r);
   void              Create(DealDetales &history[],DealDetales &BH_history[],const string &Symb[],double r);
   void              Create(DealDetales &history[],const string &Symb[],const double balance,double r);
   void              Create(DealDetales &history[],double r);
   void              Create(const string &Symb[],double r);
   void              Create(double r=0);

   //=============================================================================================================================================
   // Getters:
   //=============================================================================================================================================

   bool              GetChart(ChartType chart_type,CalcType calc_type,PLChart_item &out[]); // Obtém gráficos PL
   bool              GetDistributionChart(bool isOneLot,DistributionChart &out); // Obtém os gráficos de distribuição
   bool              GetCoefChart(bool isOneLot,CoefChartType type,CoefChart_item &out[]); // Obtém os gráficos de métricas
   bool              GetDailyPL(DailyPL_calcBy calcBy,DailyPL_calcType calcType,DailyPL &out); // Obtém o gráfico PL por dias
   bool              GetRatioTable(bool isOneLot,ProfitDrawdownType type,ProfitDrawdown &out); // Obtém a tabela de pontos extremos
   bool              GetTotalResult(TotalResult &out); // Obtém a tabela TotalResult
   bool              GetPL_detales(PL_detales &out); // Obtém a tabela PL_detales
   void              Get_Symb(const DealDetales &history[],string &Symb[]); // Obtém o array de símbolos que foram negociados
   void              Clear(); // Limpa as estatísticas

private:
   //=============================================================================================================================================
   // Tipos de dados privados:
   //=============================================================================================================================================
   // Estrutura dos tipos de gráfico PL
   struct PL_keeper
     {
      PLChart_item      PL_total[];
      PLChart_item      PL_oneLot[];
      PLChart_item      PL_Indicative[];
     };
   // Estrutura de tipos do gráfico diário de lucros/perdas
   struct DailyPL_keeper
     {
      DailyPL           avarage_open,avarage_close,absolute_open,absolute_close;
     };
   // Estrutura da tabela de pontos extremos
   struct RatioTable_keeper
     {
      ProfitDrawdown    Total_max,Total_absolute,Total_percent;
      ProfitDrawdown    OneLot_max,OneLot_absolute,OneLot_percent;
     };
   // Estruturas para o cálculo do montante dos lucros e perdas consecutivos
   struct S_dealsCounter
     {
      int               Profit,DD;
     };
   struct S_dealsInARow : public S_dealsCounter
     {
      S_dealsCounter    Counter;
     };
   // Estruturas para o cálculo de dados auxiliares
   struct CalculationData_item
     {
      S_dealsInARow     dealsCounter;
      int               R_arr[];
      double            DD_percent;
      double            Accomulated_DD,Accomulated_Profit;
      double            PL;
      double            Max_DD_forDeal,Max_Profit_forDeal;
      double            Max_DD_byPL,Max_Profit_byPL;
      datetime          DT_Max_DD_byPL,DT_Max_Profit_byPL;
      datetime          DT_Max_DD_forDeal,DT_Max_Profit_forDeal;
      int               Total_DD_numDeals,Total_Profit_numDeals;
     };
   struct CalculationData
     {
      CalculationData_item total,oneLot;
      int               num_deals;
      bool              isNot_firstDeal;
     };
   // Estrutura para a criação dos gráficos de métrica
   struct CoefChart_keeper
     {
      CoefChart_item    OneLot_ShartRatio_chart[],Total_ShartRatio_chart[];
      CoefChart_item    OneLot_WinCoef_chart[],Total_WinCoef_chart[];
      CoefChart_item    OneLot_RecoveryFactor_chart[],Total_RecoveryFactor_chart[];
      CoefChart_item    OneLot_ProfitFactor_chart[],Total_ProfitFactor_chart[];
      CoefChart_item    OneLot_AltmanZScore_chart[],Total_AltmanZScore_chart[];
     };
   // Classe que participa da ordenação do histórico de negociação pela data de fechamento.
   class CHistoryComparer : public ICustomComparer<DealDetales>
     {
   public:
      int               Compare(DealDetales &x,DealDetales &y);
     };
   //=============================================================================================================================================
   // Keepers:
   //=============================================================================================================================================
   CHistoryComparer  historyComparer; // Classe Comparing
   CChartComparer    chartComparer;   // Classe Comparing

                                      // Estruturas auxiliares
   PL_keeper         PL,PL_hist,BH,BH_hist;
   DailyPL_keeper    DailyPL_data;
   RatioTable_keeper RatioTable_data;
   TotalResult       TotalResult_data;
   PL_detales        PL_detales_data;
   DistributionChart OneLot_PDF_chart,Total_PDF_chart;
   CoefChart_keeper  CoefChart_data;

   double            balance,r; // Depósito inicial e taxa sem risco
                                // Classe de ordenação
   CGenericSorter    sorter;

   //=============================================================================================================================================
   // Cálculos:
   //=============================================================================================================================================
   // Cálculo do PL
   void              CalcPL(const DealDetales &deal,CalculationData &data,PLChart_item &pl_out[],CalcType type);
   // Cálculo dos histogramas de PL
   void              CalcPLHist(const DealDetales &deal,CalculationData &data,PLChart_item &pl_out[],CalcType type);
   // Calcula as estruturas auxiliares usadas para plotagem
   void              CalcData(const DealDetales &deal,CalculationData &out,bool isBH);
   void              CalcData_item(const DealDetales &deal,CalculationData_item &out,bool isOneLot);
   // Calcula o lucro/perda diário
   void              CalcDailyPL(DailyPL &out,DailyPL_calcBy calcBy,const DealDetales &deal);
   void              cmpDay(const DealDetales &deal,ENUM_DAY_OF_WEEK etalone,PLDrawdown &ans,DailyPL_calcBy calcBy);
   void              avarageDay(PLDrawdown &day);
   // Compara os símbolos
   bool              isSymb(const string &Symb[],string symbol);
   // Calcula o fator de lucro
   void              ProfitFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot);
   // Calcula o fator de recuperação
   void              RecoveryFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot);
   // Calcula a taxa de acerto
   void              WinCoef_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot);
   // Calcula o Sharpe ratio
   double            ShartRatio_calc(PLChart_item &data[]);
   void              ShartRatio_chart_calc(CoefChart_item &out[],PLChart_item &data[],const DealDetales &deal);
   // Calcula a distribuição
   void              NormalPDF_chart_calc(DistributionChart &out,PLChart_item &data[]);
   double            PDF_calc(double Mx,double Std,double x);
   // Calcula o VaR
   double            VaR(double quantile,double Mx,double Std);
   // Calcula o Z score 
   void              AltmanZScore_chart_calc(CoefChart_item &out[],double N,double R,double W,double L,const DealDetales &deal);
   // Calcular a estrutura TotalResult_item
   void              CalcTotalResult(CalculationData &data,bool isOneLot,TotalResult_item &out);
   // Calcular a estrutura PL_detales_item
   void              CalcPL_detales(CalculationData_item &data,int deals_num,PL_detales_item &out);
   // Obtém o dia a partir da data
   ENUM_DAY_OF_WEEK  getDay(datetime DT);
   // Apaga os dados
   void              Clear_PL_keeper(PL_keeper &data);
   void              Clear_DailyPL(DailyPL &data);
   void              Clear_RatioTable(RatioTable_keeper &data);
   void              Clear_TotalResult_item(TotalResult_item &data);
   void              Clear_PL_detales(PL_detales &data);
   void              Clear_DistributionChart(DistributionChart &data);
   void              Clear_CoefChart_keeper(CoefChart_keeper &data);

   //=============================================================================================================================================
   // Cópia:
   //=============================================================================================================================================
   void              CopyPL(const PLChart_item &src[],PLChart_item &out[]); // Copiar gráficos de PL
   void              CopyCoefChart(const CoefChart_item &src[],CoefChart_item &out[]); // Copia os gráficos de métricas

  };

Esta classe, ao contrário da versão anterior, calcula duas vezes mais dados e cria mais tipos de gráficos. As sobrecargas do método 'Create' também calculam o relatório.

Na verdade, o relatório é gerado apenas uma vez — no momento da chamada do método Create. Mais tarde, somente os dados calculados anteriormente são obtidos nos métodos que começam com a palavra Get. O loop principal, iterando uma vez sobre os parâmetros de entrada, está localizado no método Create com a maioria dos argumentos. Esse método itera sobre os argumentos e calcula imediatamente uma série de dados, com base nos quais todos os dados necessários são criados na mesma iteração.

Isso permite construir tudo o que nos interessa em um único passe, enquanto a versão anterior desta classe itera novamente sobre os dados para obter o gráfico. Como resultado, o cálculo de todas as métricas dura milésimos de segundo, enquanto a obtenção dos dados necessários leva ainda menos tempo. Na área 'private' da classe, há uma série de estruturas usadas somente dentro dessa classe como contêiner de dados mais convenientes. A ordenação do histórico de negociações é realizada usando o método de ordenação Generic descrito acima.

Vamos descrever os dados obtidos ao chamar cada um dos getters:

Método Parâmetros Tipo de gráfico
GetChart chart_type = _PL, calc_type = _Total Gráfico PL — de acordo com o histórico real de negociação
GetChart chart_type = _PL, calc_type = _OneLot Gráfico PL — ao negociar um único lote
GetChart chart_type = _PL, calc_type = _Indicative Gráfico PL — indicativo
GetChart chart_type = _BH, calc_type = _Total Gráfico BH — se gerenciando um lote como um robô
GetChart chart_type = _BH, calc_type = _OneLot Gráfico BH — se estiver negociando um único lote
GetChart chart_type = _BH, calc_type = _Indicative Gráfico BH — indicativo
GetChart chart_type = _Hist_PL, calc_type = _Total Histograma PL — de acordo com o histórico real negociado
GetChart chart_type = _Hist_PL, calc_type = _OneLot Histograma PL — se estiver negociando um único lote
GetChart chart_type = _Hist_PL, calc_type = _Indicative Histograma PL — indicativo
GetChart chart_type = _Hist_BH, calc_type = _Total Histograma BH — se gerenciando um lote como um robô
GetChart chart_type = _Hist_BH, calc_type = _OneLot Histograma BH — se estiver negociando um único lote
GetChart chart_type = _Hist_BH, calc_type = _Indicative Histograma BH — indicativo
GetDistributionChart isOneLot = true Distribuições e VaR ao negociar um único lote
GetDistributionChart isOneLot = false Distribuições e VaR ao negociar como nós fizemos anteriormente
GetCoefChart isOneLot = true, type=_ShartRatio_chart Sharpe ratio por tempo ao negociar um único lote
GetCoefChart isOneLot = true, type=_WinCoef_chart Taxa de ganho por tempo ao negociar um único lote
GetCoefChart isOneLot = true, type=_RecoveryFactor_chart Fator de recuperação por tempo ao negociar um único lote
GetCoefChart isOneLot = true, type=_ProfitFactor_chart Fator de lucro por tempo ao negociar um único lote
GetCoefChart isOneLot = true, type=_AltmanZScore_chart Z — Altman score por tempo ao negociar um único lote
GetCoefChart isOneLot = false, type=_ShartRatio_chart Sharpe ratio por tempo ao negociar como nós fizemos anteriormente
GetCoefChart isOneLot = false, type=_WinCoef_chart Taxa de ganho por tempo ao negociar como nós fizemos anteriormente
GetCoefChart isOneLot = false, type=_RecoveryFactor_chart Fator de recuperação por tempo ao negociar como nós fizemos anteriormente
GetCoefChart isOneLot = false, type=_ProfitFactor_chart Fator de lucro por tempo ao negociar como nós fizemos anteriormente
GetCoefChart isOneLot = false, type=_AltmanZScore_chart Z — Altman score por tempo ao negociar como nós fizemos anteriormente
GetDailyPL calcBy=CALC_FOR_CLOSE, calcType=AVERAGE_DATA PL médio por dias a partir do horário de fechamento
GetDailyPL calcBy=CALC_FOR_CLOSE, calcType=ABSOLUTE_DATA PL total por dias a partir do horário de fechamento
GetDailyPL calcBy=CALC_FOR_OPEN, calcType=AVERAGE_DATA PL médio por dias a partir do horário de abertura
GetDailyPL calcBy=CALC_FOR_OPEN, calcType=ABSOLUTE_DATA PL total por dias a partir do horário de abertura
GetRatioTable isOneLot = true, type = _Max Se negociar um lote — lucro/prejuízo máximo obtido por negociação
GetRatioTable isOneLot = true, type = _Absolute Se negociar um lote — lucro/prejuízo total
GetRatioTable isOneLot = true, type = _Percent Se negociar um lote — quantidade de lucro/prejuízo em %
GetRatioTable isOneLot = false, type = _Max Se negociando como fizemos anteriormente — lucro/prejuízo máximo obtido por negociação
GetRatioTable isOneLot = false, type = _Absolute Se negociando como fizemos anteriormente — lucro/prejuízo total
GetRatioTable isOneLot = false, type = _Percent Se negociando como fizemos anteriormente — quantidade de lucros/preuízos em%
GetTotalResult
Tabela com as métricas
GetPL_detales
Breve resumo da curva PL
Get_Symb
Array de símbolos presentes no histórico de negociação

Gráfico PL — de acordo com o histórico real de negociação:

O gráfico é igual a um gráfico PL usual. Nós podemos ver isso no terminal depois de todos os passes do testador.

Gráfico PL — ao negociar um único lote:

Este gráfico é semelhante ao descrito anteriormente, diferindo no volume negociado. Ele é calculado como se nós estivéssemos negociando um único lote o tempo todo. Os preços de entrada e saída são calculados como preços médios pelo número total de entradas e saídas à mercado do EA. O lucro da negociação também é calculado com base no lucro negociado pelo EA, mas ele é convertido no lucro obtido como se fosse negociado um único lote através da proporção.

Gráfico PL — indicativo:

Gráfico PL normalizado. Se PL > 0, o PL é dividido pelo negócio máximo de perdas atingido até o momento, caso contrário, o PL é dividido pelo maior lucro do negócio alcançada até o momento.

Os gráficos de histograma são construídos de maneira semelhante.

Distribuições e VaR

O VaR paramétrico é construído usando os dados absolutos e o crescimento.

O mesmo vale para o gráfico de distribuição.

Gráficos de métricas:

Construído em cada iteração do loop de acordo com as equações apropriadas através de todo o histórico disponível para essa iteração específica.

Gráficos de lucro diário:

Construído por 4 possíveis combinações de lucro mencionadas na tabela. Parece um histograma.

O método que cria todos os dados mencionados é o seguinte:

//+------------------------------------------------------------------+
//| Cálculo das métricas/recálculo                                   |
//+------------------------------------------------------------------+
void CReportCreator::Create(DealDetales &history[],DealDetales &BH_history[],const double _balance,const string &Symb[],double _r)
  {
   Clear(); // Apaga os dados
            // Salva o saldo
   this.balance=_balance;
   if(this.balance<=0)
     {
      CDealHistoryGetter dealGetter;
      this.balance=dealGetter.getBalance(history[ArraySize(history)-1].DT_open);
     }
   if(this.balance<0)
      this.balance=0;
// Salva a taxa sem risco
   if(_r<0) _r=0;
   this.r=r;

// Estruturas auxiliares
   CalculationData data_H,data_BH;
   ZeroMemory(data_H);
   ZeroMemory(data_BH);
// Ordena o histórico de negociação
   sorter.Method(Sort_Ascending);
   sorter.Sort<DealDetales>(history,&historyComparer);
// loop pelo histórico de negociação
   for(int i=0;i<ArraySize(history);i++)
     {
      if(isSymb(Symb,history[i].symbol))
         CalcData(history[i],data_H,false);
     }
// Ordena o histórico de Buy And Hold e o loop apropriado
   sorter.Sort<DealDetales>(BH_history,&historyComparer);
   for(int i=0;i<ArraySize(BH_history);i++)
     {
      if(isSymb(Symb,BH_history[i].symbol))
         CalcData(BH_history[i],data_BH,true);
     }

// PL médio diária (tipo médio)
   avarageDay(DailyPL_data.avarage_close.Mn);
   avarageDay(DailyPL_data.avarage_close.Tu);
   avarageDay(DailyPL_data.avarage_close.We);
   avarageDay(DailyPL_data.avarage_close.Th);
   avarageDay(DailyPL_data.avarage_close.Fr);

   avarageDay(DailyPL_data.avarage_open.Mn);
   avarageDay(DailyPL_data.avarage_open.Tu);
   avarageDay(DailyPL_data.avarage_open.We);
   avarageDay(DailyPL_data.avarage_open.Th);
   avarageDay(DailyPL_data.avarage_open.Fr);

// Preenche as tabelas da métrica lucro/prejuízo
   RatioTable_data.data_H.oneLot.Accomulated_Profit;
   RatioTable_data.data_H.oneLot.Accomulated_DD;
   RatioTable_data.data_H.oneLot.Max_Profit_forDeal;
   RatioTable_data.data_H.oneLot.Max_DD_forDeal;
   RatioTable_data.data_H.oneLot.Total_Profit_numDeals/data_H.num_deals;
   RatioTable_data.data_H.oneLot.Total_DD_numDeals/data_H.num_deals;

   RatioTable_data.Total_absolute.Profit=data_H.total.Accomulated_Profit;
   RatioTable_data.Total_absolute.Drawdown=data_H.total.Accomulated_DD;
   RatioTable_data.Total_max.Profit=data_H.total.Max_Profit_forDeal;
   RatioTable_data.Total_max.Drawdown=data_H.total.Max_DD_forDeal;
   RatioTable_data.Total_percent.Profit=data_H.total.Total_Profit_numDeals/data_H.num_deals;
   RatioTable_data.Total_percent.Drawdown=data_H.total.Total_DD_numDeals/data_H.num_deals;

// Calcula a distribuição normal
   NormalPDF_chart_calc(OneLot_PDF_chart,PL.PL_oneLot);
   NormalPDF_chart_calc(Total_PDF_chart,PL.PL_total);

// TotalResult
   CalcTotalResult(data_H,true,TotalResult_data.oneLot);
   CalcTotalResult(data_H,false,TotalResult_data.total);

// PL_detales
   CalcPL_detales(data_H.oneLot,data_H.num_deals,PL_detales_data.oneLot);
   CalcPL_detales(data_H.total,data_H.num_deals,PL_detales_data.total);
  }

Como pode ser visto a partir de sua implementação, parte dos dados é calculada quando o loop percorre o histórico, enquanto alguns dados são calculados após a passagem de todos os loops com base nos dados das estruturas: CalculationData data_H, data_BH.

O método CalcData é implementado de maneira semelhante ao método Create. Esse é o único método que chama os métodos que devem executar os cálculos em cada iteração. Todos os métodos que calculam os dados finais são calculados com base nas informações contidas nas estruturas acima mencionadas. O preenchimento/reabastecimento das estruturas descritas é realizado pelo seguinte método:

//+------------------------------------------------------------------+
//| Calcula os dados auxiliares                                      |
//+------------------------------------------------------------------+
void CReportCreator::CalcData_item(const DealDetales &deal,CalculationData_item &out,
                                   bool isOneLot)
  {
   double pl=(isOneLot ? deal.pl_oneLot : deal.pl_forDeal); // PL
   int n=0;
// Quantia de lucros e prejuízos
   if(pl>=0)
     {
      out.Total_Profit_numDeals++;
      n=1;
      out.dealsCounter.Counter.DD=0;
      out.dealsCounter.Counter.Profit++;
     }
   else
     {
      out.Total_DD_numDeals++;
      out.dealsCounter.Counter.DD++;
      out.dealsCounter.Counter.Profit=0;
     }
   out.dealsCounter.DD=MathMax(out.dealsCounter.DD,out.dealsCounter.Counter.DD);
   out.dealsCounter.Profit=MathMax(out.dealsCounter.Profit,out.dealsCounter.Counter.Profit);

// Série de lucros e prejuízos
   int s=ArraySize(out.R_arr);
   if(!(s>0 && out.R_arr[s-1]==n))
     {
      ArrayResize(out.R_arr,s+1,s+1);
      out.R_arr[s]=n;
     }

   out.PL+=pl; // Total PL
               // Lucro Máximo / DD
   if(out.Max_DD_forDeal>pl)
     {
      out.Max_DD_forDeal=pl;
      out.DT_Max_DD_forDeal=deal.DT_close;
     }
   if(out.Max_Profit_forDeal<pl)
     {
      out.Max_Profit_forDeal=pl;
      out.DT_Max_Profit_forDeal=deal.DT_close;
     }
// Lucro Acumulado / DD
   out.Accomulated_DD+=(pl>0 ? 0 : pl);
   out.Accomulated_Profit+=(pl>0 ? pl : 0);
// Pontos extremos pelo lucro
   double maxPL=MathMax(out.Max_Profit_byPL,out.PL);
   if(compareDouble(maxPL,out.Max_Profit_byPL)==1/* || !isNot_firstDeal*/)// outra verificação é necessária para salvar a data
     {
      out.DT_Max_Profit_byPL=deal.DT_close;
      out.Max_Profit_byPL=maxPL;
     }
   double maxDD=out.Max_DD_byPL;
   double DD=0;
   if(out.PL>0)DD=out.PL-maxPL;
   else DD=-(MathAbs(out.PL)+maxPL);
   maxDD=MathMin(maxDD,DD);
   if(compareDouble(maxDD,out.Max_DD_byPL)==-1/* || !isNot_firstDeal*/)// outra verificação é necessária para salvar a data
     {
      out.Max_DD_byPL=maxDD;
      out.DT_Max_DD_byPL=deal.DT_close;
     }
   out.DD_percent=(balance>0 ?(MathAbs(DD)/(maxPL>0 ? maxPL : balance)) :(maxPL>0 ?(MathAbs(DD)/maxPL) : 0));
  }

Este é o método básico que calcula todos os dados de entrada para cada um dos métodos de cálculo. Essa abordagem (mover o cálculo dos dados de entrada para esse método) permite evitar passes excessivos nos loops do histórico que ocorreram na versão anterior da classe que cria um relatório de negociação. Esse método é chamado dentro do método CalcData.

A classe do filtro de resultados do passe de otimização possui o seguinte cabeçalho:

//+--------------------------------------------------------------------------+
//| Classe de ordenação dos passes de otimização após a descarregá-los do banco de dados |
//+--------------------------------------------------------------------------+
class CParamsFiltre
  {
public:
                     CParamsFiltre(){sorter.Method(Sort_Ascending);} // Construtor padrão
   int               Total(){return ArraySize(arr_main);}; // Número total de parâmetros descarregados (de acordo com a tabela de dados de otimização)
   void              Clear(){ArrayFree(arr_main);ArrayFree(arr_result);}; // Limpar todas os arrays
   void              Add(LotDependency_item &customCoef,CDataKeeper &params[],long ID,double total_PL,bool addToResult); // Adiciona um novo valor ao array
   double            GetCustomCoef(long ID,bool isOneLot);// Obtém uma métrica personalizada por ID
   void              GetParamNames(CArrayString &out);// Obtém o nome dos parâmetros do EA
   void              Get_UniqueCoef(UniqCoefData_item &data[],string paramName,CArrayString &coefValue); // Obtém métricas exclusivas
   void              Filtre(string Name,string from,string till,long &ID_Arr[]);// Ordena o array arr_result
   void              ResetFiltre(long &ID_arr[]);// Redefine o filtro

   bool              Get_Distribution(Chart_item &out[],bool isMainTable);// Cria uma distribuição por ambos arrays
   bool              Get_Distribution(Chart_item &out[],string Name,string value);// Cria uma distribuição pelos dados selecionados

private:
   CGenericSorter    sorter; // Ordenador
   CCoefComparer     cmp_coef;// Compara as métricas
   CChartComparer    cmp_chart;// Compara os gráficos

   bool              selectCoefByName(CDataKeeper &_input[],CDataKeeper &out,string Name);// Seleciona as métricas pelo nome
   double            Mx(CoefStruct &_arr[]);// Média aritmética
   double            Std(CoefStruct &_arr[],double _Mx);// Desvio padrão

   CoefStruct        arr_main[]; // Tabela de dados de otimização equivalente
   CoefStruct        arr_result[];// Tabela de resultado equivalente
  };

Analise a estrutura da classe e conta sobre alguns dos métodos em mais detalhes. Como nós podemos ver, a classe tem dois arrays globais: arr_main e arr_result. Os arrays são armazenamentos de dados de otimização. Depois de descarregar a tabela com os passes de otimização do banco de dados, ela é dividida em duas tabelas:

  • main — todos os dados descarregados são obtidos, exceto os dados descartados durante uma ordenação condicional
  • result — os n melhores dados selecionados inicialmente são obtidos. Depois disso, a classe descrita classifica essa tabela específica e, consequentemente, reduz ou redefine o número de suas entradas.

Os arrays descritos armazenam o ID e os parâmetros do EA, bem como alguns outros dados das tabelas acima, de acordo com os nomes dos arrays. Em essência, essa classe executa duas funções — um armazenamento de dados conveniente para operações com tabelas e ordenação da tabela de resultados dos passes de otimização selecionados. A classe de ordenação e duas classes de comparadores estão envolvidas no processo de ordenação dos arrays mencionados, bem como na ordenação das distribuições construídas de acordo com as tabelas descritas.

Como essa classe opera com as métricas do EA, ou seja, sua representação na forma da classe CdataKeeper, é criado um método privado selectCoefByName. Ele seleciona uma métrica necessária e retorna o resultado por referência do array de métricas passadas do EA de um passe de otimização específico.

O método Add adiciona a linha carregada para o banco de dados (ambas os arrays), considerando que addToResult==true ou somente para o array arr_main se addToResult ==false. O ID é um parâmetro único de cada passe de otimização, portanto, todo o trabalho na definição de um determinado passe selecionado é baseado nele. Nós obtemos a métrica calculada pelo usuário para esse parâmetro fora dos arrays fornecidos. O programa em si não conhece a equação para calcular uma avaliação personalizada, uma vez que a avaliação é calculada durante a otimização do EA sem a participação do programa. É por isso que nós precisamos salvar uma avaliação personalizada para esses arrays. Quando ele é solicitado, nós obtemos ele usando o método GetCustomCoef pelo ID passado.

Os métodos de classe mais importantes são os seguintes:

  • Filtre — ordena a tabela de resultados, de modo que ela contenha os valores de uma métrica selecionada em uma faixa de passe (de/até).
  • ResetFiltre — redefine toda a informação ordenada.
  • Get_Distribution(Chart_item &out[],bool isMainTable) — compila a distribuição pelo PL negociado de acordo com a tabela selecionada, especificada usando o parâmetro isMainTable.
  • Get_Distribution(Chart_item &out[],string Name,string value) — cria um novo array onde um parâmetro selecionado (Name) é igual ao valor passado (value). Em outras palavras, a passagem ao longo do array arr_result é executada em um loop. Durante cada iteração do loop, o parâmetro que nos interessa é selecionado pelo seu nome (usando a função selectCoefByName) fora de todos os parâmetros do EA. Além disso, é verificado se o seu valor é igual ao valor solicitado (value). Se sim, o valor do array arr_result é adicionado ao array temporário. Em seguida, é criado e retornado uma distribuição pelo array temporário. Em outras palavras, é assim que nós selecionamos todos os passes de otimização, nos quais o valor do parâmetro selecionado pelo nome foi detectado e é igual ao valor passado. Isso é necessário para estimar o quanto esse parâmetro específico afeta o EA como um todo. A implementação da classe descrita é comentada adequadamente no código e, portanto, eu não fornecerei a implementação desses métodos aqui.


O "Presenter"

O presenter serve como um conector. Esse é um tipo de ligação entre a camada gráfica do aplicativo e sua lógica descrita acima. Nesta aplicação, o presenter é implementado usando abstrações — a interface IPresenter. Essa interface contém o nome dos métodos de call-back necessários; eles, por sua vez, são implementados na classe presenter, que deve herdar a interface necessária. Essa divisão foi criada para finalizar o aplicativo. Se você precisar reescrever o bloco do presenter, isso pode ser feito facilmente sem afetar o bloco de gráficos ou a lógica do aplicativo. A interface descrita é apresentada da seguinte forma:

//+------------------------------------------------------------------+
//| Interface Presenter                                              |
//+------------------------------------------------------------------+
interface IPresenter
  {
   void Btn_Update_Click(); // Baixa os dados e constrói o formulário inteiro
   void Btn_Load_Click(); // Cria um relatório
   void OptimisationData(bool isMainTable);// Seleciona a linha de otimização nas tabelas 
   void Update_PLByDays(); // Carrega o lucro e perda por dias
   void DaySelect();// Seleciona um dia da tabela PL por dias da semana
   void PL_pressed(PLSelected_type type);// Constrói o gráfico PL pelo histórico selecionado 
   void PL_pressed_2(bool isRealPL);// Constrói os gráficos "Other charts"
   void SaveToFile_Click();// Salve o arquivo de dados (nas sandboxes)
   void SaveParam_passed(SaveParam_type type);// Seleciona os dados para gravar no arquivo
   void OptimisationParam_selected(); // Seleciona o parâmetro de otimização e preenche a guia "Optimisation selection"
   void CompareTables(bool isChecked);// Constrói a distribuição pela tabela de resultados (para a correlação com a tabela comum (principal))
   void show_FriquencyChart(bool isChecked);// Exibe o gráfico de frequência de lucro/prejuízo
   void FriquencyChart_click();// Seleciona uma linha na tabela de métricas e cria uma distribuição
   void Filtre_click();// Ordena por condições selecionadas
   void Reset_click();// Redefine os filtros
   void PL_pressed_3(bool isRealPL);// Constrói os gráficos de lucro/prejuízo por todos os dados na tabela de resultados
   void PL_pressed_4(bool isRealPL);// Constrói as tabelas de estatísticas
   void setChartFlag(bool isPlot);// Condição para construir (ou não construir) os gráficos do método PL_pressed_3(bool isRealPL);
  };

A classe presenter implementa a interface necessária e se parece com isso:

class CPresenter : public IPresenter
  {
public:
                     CPresenter(CWindowManager *_windowManager); // Construtor

   void              Btn_Update_Click();// Baixa os dados e constrói o formulário inteiro
   void              Btn_Load_Click();// Cria um relatório
   void              OptimisationData(bool isMainTable);// Seleciona a linha de otimização nas tabelas 
   void              Update_PLByDays(); // Carrega o lucro e perda por dias
   void              PL_pressed(PLSelected_type type);// Constrói o gráfico PL pelo histórico selecionado 
   void              PL_pressed_2(bool isRealPL);// Constrói os gráficos "Other charts"
   void              SaveToFile_Click();// Salve o arquivo de dados (nas sandboxes)
   void              SaveParam_passed(SaveParam_type type);// Seleciona os dados para gravar no arquivo
   void              OptimisationParam_selected(); // Seleciona o parâmetro de otimização e preenche a guia "Optimisation selection"
   void              CompareTables(bool isChecked);// Constrói a distribuição pela tabela de resultados (para a correlação com a tabela comum (principal))
   void              show_FriquencyChart(bool isChecked);// Exibe o gráfico de frequência de lucro/prejuízo
   void              FriquencyChart_click();// Seleciona uma linha na tabela de métricas e cria uma distribuição
   void              Filtre_click();// Ordena por condições selecionadas
   void              PL_pressed_3(bool isRealPL);// Constrói os gráficos de lucro/prejuízo por todos os dados na tabela de resultados
   void              PL_pressed_4(bool isRealPL);// Constrói as tabelas de estatísticas
   void              DaySelect();// Seleciona um dia da tabela PL por dias da semana
   void              Reset_click();// Redefine os filtros
   void              setChartFlag(bool isPlot);// Condição para construir (ou não construir) os gráficos do método PL_pressed_3(bool isRealPL);

private:
   CWindowManager   *windowManager;// Referência à classe da janela
   CDBReader         dbReader;// Classe para trabalhar com o banco de dados
   CReportCreator    reportCreator; // Classe para processar os dados

   CGenericSorter    sorter; // Classe de ordenação
   CoefData_comparer coefComparer; // Classe de comparação dos dados

   void              loadData();// Carrega os dados do banco de dados e preenche as tabelas

   void              insertDataTo_main_Table(bool isResult,const CoefData_item &data[]); // Insere os dados na tabela de resultados e na tabela "Main" (tabelas com as métricas dos passes de otimização)
   void              insertRowTo_main_Table(CTable *tb,int n,const CoefData_item &data); // Inserção direta de dados nas tabelas de passe de otimização
   void              selectChartByID(long ID,bool recalc=true);// Seleciona os gráficos por ID
   void              createReport();// Cria um relatório
   string            getCorrectPath(string path,string name);// Obtém o caminho correto para o arquivo
   bool              getPLChart(PLChart_item &data[],bool isOneLot,long ID);

   bool              curveAdd(CGraphic *chart_ptr,const PLChart_item &data[],bool isHist);// Adiciona o gráfico para Other Charts
   bool              curveAdd(CGraphic *chart_ptr,const CoefChart_item &data[],double borderPoint);// Adiciona o gráfico para Other Charts
   bool              curveAdd(CGraphic *chart_ptr,const Distribution_item &data);// Adiciona o gráfico para Other Charts
   void              setCombobox(CComboBox *cb_ptr,CArrayString &arr,bool isFirstIndex=true);// Define os parâmetros da caixa de combinação
   void              addPDF_line(CGraphic *chart_ptr,double &x[],color clr,int width,string _name=NULL);// Adiciona a linha de suavização do gráfico de distribuição
   void              plotMainPDF();// Constrói a distribuição pela tabela "Main" (Optimisation Data)
   void              updateDT(CDropCalendar *dt_ptr,datetime DT);// Atualiza os calendários suspensos

   CParamsFiltre     coefKeeper;// Ordena os passes de otimização (por distribuições)
   CArrayString      headder; // Cabeçalho das tabelas de métricas

   bool              _isUpbateClick; // Flag do pressionamento do botão Update e o carregamento dos dados do banco de dados
   long              _selectedID; // ID de uma série selecionada de todo o gráfico PL (vermelho se estiver com perdas e verde se lucrativo)
   long              _ID,_ID_Arr[];// Array de IDs selecionados para a tabela Result após o upload dos dados
   bool              _IsForvard_inTables,_IsForvard_inReport; // Flag do tipo de dados de otimização nas tabelas de passe de otimização
   datetime          _DT_from,_DT_till;
   double            _Gap; // Tipo salvo da lacuna adicionada (spread extension / ou slippage simulation...) do gráfico de otimização selecionado anteriormente<
  };

Cada um dos callbacks é bem comentado, então não há necessidade de insistir neles aqui. Só é necessário dizer que esta é exatamente a parte do aplicativo onde todo o comportamento do formulário é implementado. Ele contém os gráficos de construção, preenchimento de caixas de combinação, métodos de chamada para upload e manipulação dos dados do banco de dados, bem como outras operações que conectam várias classes.


Conclusão

Nós desenvolvemos o aplicativo que manipula a tabela com todos os parâmetros de otimização possíveis passados através do testador, bem como a adição ao EA para salvar todos os passes de otimização no banco de dados. Além do relatório de negociação detalhado que nós obtemos ao selecionar um parâmetro que nos interessa, o programa também nos permite visualizar completamente um intervalo de todo o histórico de otimização selecionado por tempo, bem como todas as métricas para um determinado intervalo de tempo. Também é possível simular o desvio pelo aumento do parâmetro Gap e ver como isso afeta o comportamento dos gráficos e métricas. Outra adição é a capacidade de ordenar os resultados de otimização em um determinado intervalo de valores da métrica.

A maneira mais fácil de obter os 100 melhores passes da otimização é conectar a classe CDBWriter ao seu robô, assim como com no EA de exemplo (nos arquivos anexados), definir o filtro condicional (por exemplo, Profit Factor >= 1 exclui imediatamente todos as combinações de prejuízo) e clicar em Update deixando o parâmetro "Show n params" igual a 100. Neste caso, os 100 melhores passes de otimização (de acordo com o seu filtro) são exibidos na tabela de resultados. Cada uma das opções do aplicativo resultante, bem como métodos mais refinados de seleção das proporções, serão discutidas em mais detalhes no próximo artigo.


Os seguintes arquivos estão anexados ao artigo:

Experts/2MA_Martin — teste do EA project

  • 2MA_Martin.mq5 — código do modelo do EA. O arquivo DBWriter.mqh que salva os dados de otimização no banco de dados está incluído nele.
  • Robot.mq5 — Lógica do EA
  • Robot.mqh — arquivo de cabeçalho implementado no arquivo Robot.mq5
  • Trade.mq5 — lógica de negociação do EA
  • Trade.mqh — arquivo de cabeçalho implementado no arquivo Trade.mq5

Experts/OptimisationSelector — projeto de aplicativo descrito

  • OptimisationSelector.mq5 — modelo de um EA chamando todo o código do projeto
  • ParamsFiltre.mq5 — filtro e distribuições por tabelas de resultados
  • ParamsFiltre.mqh — arquivo de cabeçalho implementado no arquivo ParamsFiltre.mq5
  • Presenter.mq5 — presenter
  • Presenter.mqh — arquivo de cabeçalho implementado no arquivo Presenter.mq5
  • Presenter_interface.mqh — interface presenter
  • Window_1.mq5 — gráficos
  • Window_1.mqh — arquivo de cabeçalho implementado no arquivo Window_1.mq5

Include/CustomGeneric

  • GenericSorter.mqh — ordenação dos dados
  • ICustomComparer.mqh — interface ICustomSorter

Include/History manager

  • DealHistoryGetter.mqh — descarrega o histórico de negociações do terminal e converte-o em uma visualização solicitada
  • ReportCreator.mqh — classe que cria o histórico de negociação

Include/OptimisationSelector

  • DataKeeper.mqh — classe para armazenar as métricas do EA associadas ao nome da métrica
  • DBReader.mqh — classe de leitura das tabelas requeridas do banco de dados
  • DBWriter.mqh — classe que escreve no banco de dados

Include/Sqlite3

  • sqlite_amalgmation.mqh — importa as funções para trabalhar com o banco de dados
  • SqliteManager.mqh — conector para o banco de dados e classe de instrução
  • SqliteReader.mqh — classe de leitura das respostas a partir do banco de dados

Include/WinApi

  • memcpy.mqh — importa a função memcpy
  • Mutex.mqh — importa as funções de criação do Mutex
  • strcpy.mqh — importa a função strcpy
  • strlen.mqh — importa a função strlen

Libraries

  • Sqlite3_32.dll — Dll Sqlite para terminais de 32 bits
  • Sqlite3_64.dll — Dll Sqlite para terminais de 64 bits

Test database

  • 2MA_Martin optimisation data - Banco de dados Sqlite

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/5214

Arquivos anexados |
MQL5.zip (6783.27 KB)
Métodos de controle remoto de EAs Métodos de controle remoto de EAs
A principal vantagem dos robôs de negociação é o fato de poderem trabalhar 24 horas por dia em servidores VPS remotos. Ás vezes, é necessário intervir em seu trabalho manualmente, porém, pode não haver acesso direto ao servidor. Será que é possível gerenciar o trabalho de EAs remotamente? Esse artigo propõe uma das maneiras para controlar robôs por meio de comandos externos.
Receitas MQL5 – Obtendo as propriedades de uma posição de cobertura aberta Receitas MQL5 – Obtendo as propriedades de uma posição de cobertura aberta
A plataforma MetaTrader 5 não é apenas multimercado, pois ela também permite que utilizar diferentes sistemas de registro de posição. Esses recursos expandem significativamente as ferramentas para a implementação e formalização de ideias de negociação. O artigo trata de como processar e levar em conta as propriedades das posições quando elas são registradas independentemente (cobertura - 'hedge'). Além disso, é proposta uma classe derivada, é exemplificado como processar e obter as propriedades de uma posição de cobertura.
Otimização automática de EAs no MetaTrader 5 Otimização automática de EAs no MetaTrader 5
Este artigo descreve um mecanismo de auto-otimização de um EA para o MetaTrader 5.
Modelo de continuação de movimento - estatísticas de desempenho e pesquisa em gráficos Modelo de continuação de movimento - estatísticas de desempenho e pesquisa em gráficos
Nesse artigo, quero descrever como funciona um dos modelos de continuação de movimento. O trabalho é baseado na definição de duas ondas — uma principal e outra corretiva. Como extremos serão usados fractais e, como eu os chamo, potenciais fractais - extremos que ainda não se formaram como fractais.