Os 100 melhores passes de otimização (parte 1). Desenvolvimento de um analisador de otimizações
- Introdução
- Estrutura do analisador de otimização
- Gráficos
- Trabalhando com o banco de dados
- Cálculos
- O "Presenter"
- Conclusão
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 algoritmo de negociação
- Dll Sqlite3
- A biblioteca de interface gráfica mencionada acima com as edições necessárias (descrita no bloco Graphics abaixo)
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; 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: |
---|
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:
- Adicionar um ponteiro ao elemento principal — o contêiner de guias deve conhecer o elemento ao qual ele está atribuído
- String de criação do elemento de controle
- 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:
- Time — eixo X (rótulo de intervalo de tempo)
- PL_total — lucro/perda se aumentarmos o lote em proporção ao robô
- PL_oneLot — lucro/perda se negociar um único lote constantemente
- DD_total — rebaixamento se negociar um lote da mesma forma que o EA negociou
- DD_oneLot — rebaixamento se estiver negociando um único lote
- isForvard — propriedade do gráfico de forward
Tabela OptimisationParams:
- ID — índice de entrada de preenchimento automático exclusivo no banco de dados
- HistoryBorder — histórico da data de conclusão da otimização
- TF — tempo gráfico
- Param_1...Param_n — parâmetro
- InitalBalance — valor do saldo inicial
Tabela ParamsCoefitients:
- ID — chave externa, referência ao OptimisationParams(ID)
- isForvard — propriedade da otimização de forward
- isOneLot — propriedade do gráfico em que a métrica foi baseada
- DD — rebaixamento
- averagePL — lucro/perda média pelo gráfico PL
- averageDD — rebaixamento médio
- averageProfit — lucro médio
- profitFactor — fator de lucro
- recoveryFactor — fator de recuperação
- sharpRatio — Sharpe ratio
- altman_Z_Score — Altman Z score
- VaR_absolute_90 — VaR 90
- VaR_absolute_95 — VaR 95
- VaR_absolute_99 — VaR 99
- VaR_growth_90 — VaR 90
- VaR_growth_95 — VaR 95
- VaR_growth_99 — VaR 99
- winCoef — taxa de acerto
- customCoef — métrica personalizada
Tabela ParamType:
- ParamName — nome do parâmetro do robô
- ParamType — tipo do parâmetro do robô (int/double/string)
Tabela TradingHistory
- ID — chave externa, referência ao OptimisationParams(ID)
- isForvard — flag do forward test
- Symbol — símbolo
- DT_open — data de abertura
- Day_open — dia de abertura
- DT_close — data de fechamento
- Day_close — dia de fechamento
- Volume — número de lotes
- isLong — propriedade comprado/vendido
- Price_in — preço de entrada
- Price_out — preço de saída
- PL_oneLot — lucro ao negociar um único lote
- PL_forDeal — lucro ao negociar como nós fizemos anteriormente
- OpenComment — comentário de entrada
- 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:
- O nome da variável do EA
- O tipo da variável
- 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.
- 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:
- ou um usuário, por alguns motivos, interrompeu a otimização histórica e depois iniciou ela novamente,
- 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 ¶ms[],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
- 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
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso