English Русский
preview
Desenvolvendo um EA multimoeda (Parte 9): Coleta dos resultados de otimização de instâncias individuais da estratégia de trading

Desenvolvendo um EA multimoeda (Parte 9): Coleta dos resultados de otimização de instâncias individuais da estratégia de trading

MetaTrader 5Testador | 11 setembro 2024, 14:51
53 0
Yuriy Bykov
Yuriy Bykov

Introdução

Já fizemos em artigos anteriores várias coisas valiosas por si só. Temos uma ou mais estratégias de trading que podemos implementar no EA. Desenvolvemos um esquema de conexão de várias instâncias de estratégias de trading em um único EA. Adicionamos ferramentas para gerenciar o máximo rebaixamento permitido. Exploramos maneiras possíveis de automatizar a seleção de conjuntos de parâmetros das estratégias para seu melhor desempenho em grupo. Aprendemos a montar o EA a partir de grupos de instâncias de estratégias e até mesmo de grupos de diferentes grupos de instâncias de estratégias. No entanto, o valor dos resultados já obtidos aumentará significativamente se conseguirmos unificá-los.

Neste artigo, tentaremos esboçar um esquema geral do processo: as estratégias de trading individuais são inseridas, e o resultado é um EA finalizado, onde são usadas instâncias selecionadas e agrupadas das estratégias de trading originais, que oferecem os melhores resultados.

Após mapear um esboço, olharemos mais de perto para uma de suas partes, analisaremos o que será necessário para a implementação dessa etapa escolhida, e então nos dedicaremos diretamente à sua execução.


Principais Etapas

Vamos listar as principais etapas que enfrentaremos no processo de desenvolvimento do EA:

  1. Implementação da estratégia de trading. Desenvolvemos uma classe, herdeira de CVirtualStrategy, que implementa a lógica de trading para a abertura, acompanhamento e fechamento de posições e ordens virtuais. Foi isso que fizemos nas primeiras quatro partes do ciclo.

  2. Otimização da estratégia de trading. Selecionamos bons conjuntos de parâmetros de entrada para a estratégia de trading, que mostram resultados dignos de atenção. Se não forem encontrados, voltamos ao ponto 1.
    Geralmente, é mais conveniente realizar a otimização em um único símbolo e período. Para a otimização genética, provavelmente precisaremos rodá-la várias vezes com diferentes critérios de otimização, incluindo algum critério personalizado. Usar a otimização por busca exaustiva só será viável em estratégias com um número muito pequeno de parâmetros. Mesmo em nossa estratégia modelo, a busca exaustiva é muito custosa. Portanto, daqui em diante, quando falarmos de otimização,
    nos referiremos especificamente à otimização genética no testador de estratégias do MetaTrader 5. O processo de otimização não foi descrito em detalhes nos artigos, pois é padrão.

  3. Clusterização dos conjuntos. Esta etapa não é obrigatória, mas permite economizar algum tempo na próxima etapa. Aqui, reduzimos significativamente o número de conjuntos de parâmetros das instâncias da estratégia de trading, entre os quais selecionaremos bons grupos. Mencionado na sexta parte.

  4. Seleção de grupos de conjuntos de parâmetros. Com base nos resultados da etapa anterior, realizamos uma otimização que seleciona
    os conjuntos de parâmetros mais compatíveis das instâncias da estratégia de trading, obtendo os melhores resultados. Isto também é descrito principalmente na sexta parte e desenvolvido na sétima.

  5. Seleção de grupos a partir de grupos de conjuntos de parâmetros. Agora agrupamos os resultados da etapa anterior seguindo o mesmo princípio de como agrupamos os conjuntos de parâmetros de instâncias individuais no modo de coleta de quadros da estratégia.

  6. Iteração de símbolos e períodos. Repetimos as etapas 2 a 5 para todos os símbolos e períodos desejados. Talvez, além do símbolo e do período, para algumas estratégias de trading, seja possível realizar uma otimização separada para determinadas classes de outros parâmetros de entrada. 

  7. Outras estratégias. Se houver outras estratégias de trading em mente, repetimos as etapas 1 a 6 para cada uma delas.

  8. Montagem do EA. Todas as melhores combinações de grupos para diferentes estratégias de trading, símbolos, períodos e outros parâmetros são reunidas em um único EA final.

Cada etapa, após ser concluída, gera dados que precisam ser armazenados e utilizados nas próximas etapas. Até agora, temos usado soluções temporárias, convenientes o suficiente para uso uma ou duas vezes, mas não ideais para uso repetido.

Por exemplo, salvamos os resultados da otimização da segunda etapa em um arquivo Excel, adicionamos manualmente as colunas ausentes e, após salvar como arquivo CSV, usamos na terceira etapa.

Os resultados da terceira etapa foram usados diretamente na interface do testador de estratégias ou novamente salvos em arquivos Excel, onde realizamos algum processamento e usamos os resultados mais uma vez na interface do testador.

A quinta etapa não foi realizada de fato, apenas destacamos a possibilidade de sua execução. Portanto, não chegamos aos seus resultados.

Gostaríamos de implementar um esquema unificado de armazenamento e utilização para todos esses dados gerados. 


Opções de implementação

Basicamente, o principal tipo de dado que precisamos armazenar e usar são os resultados de otimização de vários EAs. Como é sabido, todos os resultados de otimização são gravados pelo testador de estratégias em um arquivo de cache separado com a extensão *.opt, que pode ser reaberto no testador posteriormente ou até mesmo em outro terminal MetaTrader 5. O nome do arquivo é definido a partir de um hash, calculado com base no nome do EA otimizado e nos parâmetros de otimização. Isso permite não perder informações sobre as execuções já realizadas ao continuar a otimização após uma interrupção antecipada ou mudança no critério de otimização.

Portanto, uma das opções consideradas é o uso de arquivos de cache de otimização para armazenar resultados intermediários. Para trabalhar com eles, existe uma boa biblioteca de fxsaber, que nos permite acessar todas as informações salvas a partir de programas em MQL5.

No entanto, à medida que o número de otimizações realizadas aumenta, também aumentará o número de arquivos com seus resultados. Para não nos perdermos com esses arquivos, será necessário desenvolver um esquema adicional de organização para o armazenamento e manipulação desses arquivos de cache. Se a otimização for realizada em mais de um servidor, será preciso implementar a sincronização ou o armazenamento de todos os arquivos de cache em um único local. Além disso, para a próxima etapa, ainda precisaremos processar os resultados para exportá-los para o EA.

Assim, vamos aborar o armazenamento de todos os resultados em um banco de dados. À primeira vista, isso pode exigir um esforço significativo de tempo para implementação. No entanto, esse trabalho pode ser dividido em etapas menores, e seus resultados podem ser utilizados imediatamente, sem a necessidade de esperar pela implementação completa. Essa abordagem oferece uma grande liberdade na escolha dos meios mais convenientes para o processamento intermediário dos resultados armazenados. Por exemplo, parte do processamento pode ser realizada por consultas SQL simples, outra parte pode ser calculada em MQL5, e alguns cálculos podem ser feitos em programas escritos em Python ou R. Dessa forma, podemos testar diferentes métodos de processamento e escolher o mais adequado.

O MQL5 oferece funções embutidas para trabalhar com o banco de dados SQLite. Além disso, há bibliotecas de terceiros que permitem trabalhar com MySQL, por exemplo. Ainda não está claro se as funcionalidades do SQLite serão suficientes para nós, mas provavelmente essa base de dados será adequada para nossas necessidades. Se não for, então pensaremos na migração para outro SGBD.


Iniciando o projeto do banco de dados

Primeiro, precisamos identificar as entidades sobre as quais queremos armazenar informações. Certamente, uma dessas entidades é a execução do testador. Os campos dessa entidade incluirão os dados de entrada do teste e os resultados obtidos. Esses campos podem ser divididos em entidades separadas. A entidade de dados de entrada pode ser subdividida em entidades menores: EA, configurações de otimização e parâmetros de uma execução individual do EA. Mas vamos continuar seguindo o princípio da ação mínima. Inicialmente, uma tabela com os campos para os resultados da execução que usamos nos artigos anteriores, juntamente com um ou dois campos de texto para os parâmetros de entrada, será suficiente.

Essa tabela pode ser criada com a seguinte consulta SQL:

CREATE TABLE passes (
    id                    INTEGER  PRIMARY KEY AUTOINCREMENT,
    pass                  INT,	-- pass index

    inputs                TEXT, -- pass input values
    params                TEXT, -- additional pass data

    initial_deposit       REAL, -- pass results...
    withdrawal            REAL,
    profit                REAL,
    gross_profit          REAL,
    gross_loss            REAL,
    max_profittrade       REAL,
    max_losstrade         REAL,
    conprofitmax          REAL,
    conprofitmax_trades   REAL,
    max_conwins           REAL,
    max_conprofit_trades  REAL,
    conlossmax            REAL,
    conlossmax_trades     REAL,
    max_conlosses         REAL,
    max_conloss_trades    REAL,
    balancemin            REAL,
    balance_dd            REAL,
    balancedd_percent     REAL,
    balance_ddrel_percent REAL,
    balance_dd_relative   REAL,
    equitymin             REAL,
    equity_dd             REAL,
    equitydd_percent      REAL,
    equity_ddrel_percent  REAL,
    equity_dd_relative    REAL,
    expected_payoff       REAL,
    profit_factor         REAL,
    recovery_factor       REAL,
    sharpe_ratio          REAL,
    min_marginlevel       REAL,
    deals                 REAL,
    trades                REAL,
    profit_trades         REAL,
    loss_trades           REAL,
    short_trades          REAL,
    long_trades           REAL,
    profit_shorttrades    REAL,
    profit_longtrades     REAL,
    profittrades_avgcon   REAL,
    losstrades_avgcon     REAL,
    complex_criterion     REAL,
    custom_ontester       REAL,
    pass_date             DATETIME DEFAULT (datetime('now') ) 
                                   NOT NULL
);

Vamos criar uma classe auxiliar CDatabase, que conterá os métodos para trabalhar com o banco de dados. Ela pode ser estática, já que não precisaremos de muitas instâncias dela em um único programa, apenas uma será suficiente. Como estamos planejando armazenar todas as informações em um único banco de dados, o nome do arquivo do banco de dados pode ser definido diretamente no código-fonte. 

Essa classe incluirá um campo s_db para armazenar o handle do banco de dados aberto. Seu valor será atribuído pelo método de abertura do banco de dados Open(). Se o banco de dados ainda não tiver sido criado no momento da abertura, ele será criado pelo método Create(). Após a abertura, poderemos executar consultas SQL individuais ao banco de dados usando o método Execute(), ou consultas SQL em massa dentro de uma única transação com o método ExecuteTransaction(). No final, vamos fechar o banco de dados utilizando o método Close().

Também podemos declarar um pequeno macro que permite usar um nome mais curto DB em vez do nome completo da classe CDatabase.

#define DB CDatabase

//+------------------------------------------------------------------+
//| Class for handling the database                                  |
//+------------------------------------------------------------------+
class CDatabase {
   static int        s_db;          // DB connection handle
   static string     s_fileName;    // DB file name
public:
   static bool       IsOpen();      // Is the DB open?

   static void       Create();      // Create an empty DB
   static void       Open();        // Opening DB
   static void       Close();       // Closing DB

   // Execute one query to the DB
   static bool       Execute(string &query);

   // Execute multiple DB queries in one transaction
   static bool       ExecuteTransaction(string &queries[]);
};

int    CDatabase::s_db       =  INVALID_HANDLE;
string CDatabase::s_fileName = "database.sqlite";


No método de criação do banco de dados, por enquanto, apenas criaremos um array com as consultas SQL para a criação das tabelas e as executaremos em uma única transação:

//+------------------------------------------------------------------+
//| Create an empty DB                                               |
//+------------------------------------------------------------------+
void CDatabase::Create() {
   // Array of DB creation requests 
   string queries[] = {
      "DROP TABLE IF EXISTS passes;",

      "CREATE TABLE passes ("
      "id                    INTEGER  PRIMARY KEY AUTOINCREMENT,"
      "pass                  INT,"
      "inputs                TEXT,"
      "params                TEXT,"
      "initial_deposit       REAL,"
      "withdrawal            REAL,"
      "profit                REAL,"
      "gross_profit          REAL,"
      "gross_loss            REAL,"
      ...
      "pass_date             DATETIME DEFAULT (datetime('now') ) NOT NULL"
      ");"
      ,
   };

   // Execute all requests
   ExecuteTransaction(queries);
}


No método de abertura do banco de dados, primeiro tentamos abrir o arquivo de banco de dados existente. Se ele não existir, então o criaremos e o abriremos, depois criaremos a estrutura do banco de dados chamando o método Create()

//+------------------------------------------------------------------+
//| Is the DB open?                                                  |
//+------------------------------------------------------------------+
bool CDatabase::IsOpen() {
   return (s_db != INVALID_HANDLE);
}
...

//+------------------------------------------------------------------+
//| Open DB                                                          |
//+------------------------------------------------------------------+
void CDatabase::Open() {
// Try to open an existing DB file
   s_db = DatabaseOpen(s_fileName, DATABASE_OPEN_READWRITE | DATABASE_OPEN_COMMON);

// If the DB file is not found, try to create it when opening
   if(!IsOpen()) {
      s_db = DatabaseOpen(s_fileName,
                          DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE |
                          DATABASE_OPEN_COMMON);

      // Report an error in case of failure
      if(!IsOpen()) {
         PrintFormat(__FUNCTION__" | ERROR: %s open failed with code %d",
                     s_fileName, GetLastError());
         return;
      }

      // Create the database structure
      Create();
   }
   PrintFormat(__FUNCTION__" | Database %s opened successfully", s_fileName);
}


No método de execução de várias consultas ExecuteTransaction(), criamos uma transação e começamos a executar todas as consultas SQL em loop, uma após a outra. Se ocorrer um erro ao executar qualquer consulta, interrompemos o loop, relatamos o erro e cancelamos todas as consultas anteriores dentro dessa transação. Se nenhum erro ocorrer, confirmamos a transação:

//+------------------------------------------------------------------+
//| Execute multiple DB queries in one transaction                   |
//+------------------------------------------------------------------+
bool CDatabase::ExecuteTransaction(string &queries[]) {
// Open a transaction
   DatabaseTransactionBegin(s_db);

   bool res = true;
// Send all execution requests
   FOREACH(queries, {
      res &= Execute(queries[i]);
      if(!res) break;
   });

// If an error occurred in any request, then
   if(!res) {
      // Report it
      PrintFormat(__FUNCTION__" | ERROR: Transaction failed, error code=%d", GetLastError());
      // Cancel transaction
      DatabaseTransactionRollback(s_db);
   } else {
      // Otherwise, confirm transaction
      DatabaseTransactionCommit(s_db);
      PrintFormat(__FUNCTION__" | Transaction done successfully");
   }
   return res;
}


Salvamos as alterações feitas no arquivo Database.mqh na pasta atual.


Modificação do EA para coletar dados de otimização

Ao usar apenas agentes no computador local durante o processo de otimização, podemos realizar a gravação dos resultados no banco de dados tanto no manipulador de evento OnTester() quanto no manipulador de evento OnDeinit(). Ao usar agentes na rede local ou na MQL5 Cloud Network, conseguir salvar os resultados será muito difícil, se não impossível. Felizmente, o MQL5 oferece uma excelente maneira padrão de obter qualquer informação dos agentes de teste, independentemente de onde eles estejam, por meio da criação, envio e recebimento de frames de dados.

Esse mecanismo é descrito detalhadamente na documentação e no tutorial sobre algotrading. Para usá-lo, precisamos adicionar ao EA otimizado três manipuladores de eventos adicionais: OnTesterInit(), OnTesterPass() e OnTesterDeinit()

A otimização sempre é iniciada a partir de algum terminal MetaTrader 5, que chamaremos de principal a partir de agora. Ao iniciar a otimização de um EA com esses manipuladores a partir do terminal principal, antes de distribuir as instâncias do EA para os agentes de teste para realizar os ciclos de otimização com diferentes conjuntos de parâmetros, um novo gráfico é aberto no terminal principal, e uma nova instância do EA é lançada nesse gráfico.

Essa instância é iniciada em um modo especial: nela, os manipuladores padrão OnInit(), OnTick() e OnDeinit() não são executados, sendo executados apenas esses três novos manipuladores. Esse modo até tem um nome próprio - modo de coleta de frames de resultados de otimização. Se necessário, nas funções do EA, podemos verificar se o EA foi iniciado nesse modo usando uma chamada à função MQLInfoInteger():

// Check if the EA is running in data frame collection mode 
bool isFrameMode = MQLInfoInteger(MQL_FRAME_MODE); 

Como os nomes sugerem, no modo de coleta de frames, o manipulador OnTesterInit() é executado uma vez antes do início do processo de otimização, OnTesterPass() é executado toda vez que um dos agentes de teste conclui um ciclo, e OnTesterDeinit() é executado uma vez após todos os ciclos de otimização planejados serem concluídos ou interrompidos.

É precisamente a instância do EA, iniciada no gráfico do terminal principal no modo de coleta de frames, que será responsável pela coleta dos frames de dados de todos os agentes de teste. "Frame de dados" é apenas um nome conveniente para descrever os processos de troca de informações entre os agentes de teste e o EA no terminal principal. Ele representa um conjunto de dados com um nome e um identificador numérico, que o agente de teste criou e enviou ao terminal principal após a conclusão de mais um ciclo de otimização. 

Vale ressaltar que faz sentido criar frames de dados apenas nas instâncias do EA que operam no modo normal nos agentes de teste, enquanto a coleta e o processamento dos frames de dados são realizados apenas na instância do EA no terminal principal, funcionando no modo de coleta de frames. Portanto, vamos começar com o processo de criação dos frames.

A criação dos frames pode ser incluída no EA dentro do manipulador OnTester() ou em qualquer função ou método chamado a partir de OnTester(). Esse manipulador é acionado após a conclusão de um ciclo, e podemos obter nele os valores de todas as características estatísticas do ciclo concluído e, se necessário, calcular o valor de um critério personalizado para avaliar os resultados do ciclo.

Atualmente, temos nele um código que calcula o critério personalizado, mostrando o lucro previsto que poderia ser obtido sob a condição de um rebaixamento máximo de 10%:

//+------------------------------------------------------------------+
//| Test results                                                     |
//+------------------------------------------------------------------+
double OnTester(void) {
// Maximum absolute drawdown
   double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD);

// Profit
   double profit = TesterStatistics(STAT_PROFIT);

// The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_
   double coeff = fixedBalance_ * 0.1 / balanceDrawdown;

// Recalculate the profit
   double fittedProfit = profit * coeff;

   return fittedProfit;
}


Vamos mover esse código do arquivo do EA SimpleVolumesExpertSingle.mq5 para um novo método da classe CVirtualAdvisor, e no EA deixaremos apenas o retorno do resultado da chamada desse método:

//+------------------------------------------------------------------+
//| Test results                                                     |
//+------------------------------------------------------------------+
double OnTester(void) {
   return expert.Tester();
}


Ao mover, devemos considerar que dentro do método não podemos mais usar a variável fixedBalance_, pois em outro EA ela pode não existir. Mas podemos obter seu valor da classe estática CMoney através da chamada do método CMoney::FixedBalance(). Paralelamente, faremos outra alteração no cálculo do nosso critério personalizado. Após determinar o lucro previsto, o recalcularemos para uma unidade de tempo, como lucro anual, por exemplo. Isso nos permitirá comparar aproximadamente os resultados dos ciclos de diferentes durações.

Para isso, precisaremos registrar no EA a data de início do teste. Adicionaremos uma nova propriedade m_fromDate, onde gravaremos o tempo atual no construtor do objeto EA.

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...

   datetime          m_fromDate;

public:
   ...
   virtual double    Tester() override;         // OnTester event handler
   ...
};


//+------------------------------------------------------------------+
//| OnTester event handler                                           |
//+------------------------------------------------------------------+
double CVirtualAdvisor::Tester() {
// Maximum absolute drawdown
   double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD);

// Profit
   double profit = TesterStatistics(STAT_PROFIT);

// The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_
   double coeff = CMoney::FixedBalance() * 0.1 / balanceDrawdown;

// Calculate the profit in annual terms
   long totalSeconds = TimeCurrent() - m_fromDate;
   double fittedProfit = profit * coeff * 365 * 24 * 3600 / totalSeconds ;

// Perform data frame generation on the test agent
   CTesterHandler::Tester(fittedProfit,
                          ~((CVirtualStrategy *) m_strategies[0]));

   return fittedProfit;
}


É possível que, no futuro, façamos vários critérios personalizados de otimização, e então esse código será movido novamente para outro local. Mas, por enquanto, não nos desviaremos do extenso tema da pesquisa de diferentes funções de fitness para a otimização de EAs e deixaremos esse código como está.

Agora, vamos adicionar novos manipuladores ao arquivo do EA SimpleVolumesExpertSingle.mq5: OnTesterInit(), OnTesterPass() e OnTesterDeinit(). Como planejamos que a lógica dessas funções seja a mesma para todos os EAs, implementaremos inicialmente essas funções no nível do expert (objeto da classe CVirtualAdvisor).

Ao mesmo tempo, é importante levar em consideração que, quando o EA for iniciado no terminal principal no modo de coleta de frames, a função OnInit(), que cria a instância do expert, não será executada. Portanto, para evitar a necessidade de adicionar a criação/remoção da instância do expert nos novos manipuladores, faremos com que os métodos de tratamento desses eventos na classe CVirtualAdvisor sejam estáticos. Assim, no EA, precisaremos adicionar o seguinte código:  

//+------------------------------------------------------------------+
//| Initialization before starting optimization                      |
//+------------------------------------------------------------------+
int OnTesterInit(void) {
   return CVirtualAdvisor::TesterInit();
}

//+------------------------------------------------------------------+
//| Actions after completing the next optimization pass              |
//+------------------------------------------------------------------+
void OnTesterPass() {
   CVirtualAdvisor::TesterPass();
}

//+------------------------------------------------------------------+
//| Actions after optimization is complete                           |
//+------------------------------------------------------------------+
void OnTesterDeinit(void) {
   CVirtualAdvisor::TesterDeinit();
}


Outra mudança que podemos fazer com vistas ao futuro é eliminar a chamada separada do método de adição de estratégias de trading ao expert CVirtualAdvisor::Add() após a criação do expert. Em vez disso, passaremos diretamente as informações sobre as estratégias para o construtor do expert, e ele chamará o método Add() internamente. Com isso, o método poderá ser removido da parte pública.

Com essa abordagem, a função de inicialização do EA OnInit() ficará assim:

int OnInit() {
   CMoney::FixedBalance(fixedBalance_);

// Create an EA handling virtual positions
   expert = new CVirtualAdvisor(
      new CSimpleVolumesStrategy(
         symbol_, timeframe_,
         signalPeriod_, signalDeviation_, signaAddlDeviation_,
         openDistance_, stopLevel_, takeLevel_, ordersExpiration_,
         maxCountOfOrders_, 0), // One strategy instance
      magic_, "SimpleVolumesSingle", true);

   return(INIT_SUCCEEDED);
}

Salvemos as alterações feitas no arquivo SimpleVolumesExpertSingle.mq5 na pasta atual.

Modificação da classe expert

Para não sobrecarregar o código da classe expert CVirtualAdvisor, vamos mover o código dos manipuladores de eventos TesterInit, TesterPass e OnTesterDeinit para uma classe separada CTesterHandler, onde criaremos métodos estáticos para lidar com cada um desses eventos. Assim, na classe CVirtualAdvisor, precisaremos adicionar um código semelhante ao do arquivo principal do EA:

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
   ...

public:
   ...
   static int        TesterInit();     // OnTesterInit event handler
   static void       TesterPass();     // OnTesterDeinit event handler
   static void       TesterDeinit();   // OnTesterDeinit event handler
};


//+------------------------------------------------------------------+
//| Initialization before starting optimization                      |
//+------------------------------------------------------------------+
int CVirtualAdvisor::TesterInit() {
   return CTesterHandler::TesterInit();
}

//+------------------------------------------------------------------+
//| Actions after completing the next optimization pass              |
//+------------------------------------------------------------------+
void CVirtualAdvisor::TesterPass() {
   CTesterHandler::TesterPass();
}

//+------------------------------------------------------------------+
//| Actions after optimization is complete                           |
//+------------------------------------------------------------------+
void CVirtualAdvisor::TesterDeinit() {
   CTesterHandler::TesterDeinit();
}


Faremos também algumas modificações no código do construtor do objeto expert. Com vistas ao futuro, moveremos todas as ações do construtor para um novo método de inicialização Init(). Isso nos permitirá adicionar vários construtores com diferentes conjuntos de parâmetros, que usarão o mesmo método de inicialização após uma pequena pré-processamento dos parâmetros.

Adicionaremos construtores cujo primeiro argumento será um objeto de estratégia ou um objeto de grupo de estratégias. Dessa forma, a adição das estratégias ao expert poderá ser feita diretamente no construtor. Nesse caso, não será mais necessário chamar o método Add() na função OnInit() do EA.

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...

   datetime          m_fromDate;

public:
                     CVirtualAdvisor(CVirtualStrategy *p_strategy, ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false); // Constructor
                     CVirtualAdvisor(CVirtualStrategyGroup *p_group, ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false); // Constructor
   void              CVirtualAdvisor::Init(CVirtualStrategyGroup *p_group,
                                           ulong p_magic = 1,
                                           string p_name = "",
                                           bool p_useOnlyNewBar = false
                                          );
   ...
};

...

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(CVirtualStrategy *p_strategy,
                                 ulong p_magic = 1,
                                 string p_name = "",
                                 bool p_useOnlyNewBar = false
                                ) {
   CVirtualStrategy *strategies[] = {p_strategy};
   Init(new CVirtualStrategyGroup(strategies), p_magic, p_name, p_useOnlyNewBar);
};

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(CVirtualStrategyGroup *p_group,
                                 ulong p_magic = 1,
                                 string p_name = "",
                                 bool p_useOnlyNewBar = false
                                ) {
   Init(p_group, p_magic, p_name, p_useOnlyNewBar);
};

//+------------------------------------------------------------------+
//| EA initialization method                                         |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Init(CVirtualStrategyGroup *p_group,
                           ulong p_magic = 1,
                           string p_name = "",
                           bool p_useOnlyNewBar = false
                          ) {
// Initialize the receiver with a static receiver
   m_receiver = CVirtualReceiver::Instance(p_magic);
// Initialize the interface with the static interface
   m_interface = CVirtualInterface::Instance(p_magic);
   m_lastSaveTime = 0;
   m_useOnlyNewBar = p_useOnlyNewBar;
   m_name = StringFormat("%s-%d%s.csv",
                         (p_name != "" ? p_name : "Expert"),
                         p_magic,
                         (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                        );

   m_fromDate = TimeCurrent();

   Add(p_group);
   delete p_group;
};

Salvemos as alterações feitas no arquivo VirtualExpert.mqh na pasta atual.


Classe de tratamento de eventos de otimização

Agora, vamos nos concentrar diretamente na implementação das ações realizadas antes do início, após a conclusão de um ciclo, e após o término da otimização. Criaremos a classe CTesterHandler e adicionaremos métodos de tratamento para os eventos necessários, além de alguns métodos auxiliares colocados na parte privada da classe: 

//+------------------------------------------------------------------+
//| Optimization event handling class                                |
//+------------------------------------------------------------------+
class CTesterHandler {
   static string     s_fileName;                   // File name for writing frame data
   static void       ProcessFrames();              // Handle incoming frames
   static string     GetFrameInputs(ulong pass);   // Get pass inputs
public:
   static int        TesterInit();     // Handle the optimization start in the main terminal
   static void       TesterDeinit();   // Handle the optimization completion in the main terminal
   static void       TesterPass();     // Handle the completion of a pass on an agent in the main terminal

   static void       Tester(const double OnTesterValue,
                            const string params);  // Handle completion of tester pass for agent
};

string CTesterHandler::s_fileName = "data.bin";    // File name for writing frame data


Os manipuladores de eventos para o terminal principal serão bem simples, pois o código principal será movido para funções auxiliares:

//+------------------------------------------------------------------+
//| Handling the optimization start in the main terminal             |
//+------------------------------------------------------------------+
int CTesterHandler::TesterInit(void) {
// Open / create a database
   DB::Open();

// If failed to open it, we do not start optimization
   if(!DB::IsOpen()) {
      return INIT_FAILED;
   }

// Close a successfully opened database
   DB::Close();

   return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| Handling the optimization completion in the main terminal        |
//+------------------------------------------------------------------+
void CTesterHandler::TesterDeinit(void) {
// Handle the latest data frames received from agents
   ProcessFrames();

// Close the chart with the EA running in frame collection mode
   ChartClose();
}

//+--------------------------------------------------------------------+
//| Handling the completion of a pass on an agent in the main terminal |
//+--------------------------------------------------------------------+
void CTesterHandler::TesterPass(void) {
// Handle data frames received from the agent
   ProcessFrames();
}


As ações realizadas após a conclusão de um ciclo existirão em dois formatos:

  • Para o agente de teste. Aqui, após o ciclo, serão coletadas as informações necessárias e criado o frame de dados para envio ao terminal principal. Essas ações serão reunidas no manipulador de evento Tester().

  • Para o terminal principal. Aqui, poderão ser realizadas ações para receber os frames de dados dos agentes de teste, analisar as informações contidas nos frames e inseri-las no banco de dados. Essas ações serão reunidas no manipulador TesterPass().

As ações de criação do frame de dados para o agente de teste precisam ser executadas no EA dentro do manipulador OnTester. Como o código foi movido para o nível do objeto expert (classe CVirtualAdvisor), é lá que o método CTesterHandler::Tester() deve ser chamado. Os parâmetros que passaremos para esse método serão o valor recém-calculado do critério de otimização personalizado e uma string com a descrição dos parâmetros da estratégia utilizada no EA otimizado. Para criar essa string, utilizaremos o operador ~ (til) que já foi implementado para os objetos da classe CVirtualStrategy.

//+------------------------------------------------------------------+
//| OnTester event handler                                           |
//+------------------------------------------------------------------+
double CVirtualAdvisor::Tester() {
// Maximum absolute drawdown
   double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD);

// Profit
   double profit = TesterStatistics(STAT_PROFIT);

// The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_
   double coeff = CMoney::FixedBalance() * 0.1 / balanceDrawdown;

// Calculate the profit in annual terms
   long totalSeconds = TimeCurrent() - m_fromDate;
   double fittedProfit = profit * coeff * 365 * 24 * 3600 / totalSeconds ;

// Perform data frame generation on the test agent
   CTesterHandler::Tester(fittedProfit,
                          ~((CVirtualStrategy *) m_strategies[0]));

   return fittedProfit;
}


Dentro do método CTesterHandler::Tester(), iteraremos sobre todos os possíveis nomes das características estatísticas disponíveis, obteremos seus valores, os converteremos em strings e adicionaremos essas strings ao array stats. Por que precisamos converter características numéricas em strings? Isso é necessário para podermos enviar todas essas informações em um único frame, juntamente com a string que descreve os parâmetros da estratégia. Em um frame, só é possível enviar um array de valores de tipos simples (o que não inclui strings), ou então um arquivo criado previamente com quaisquer dados. Portanto, para evitar o envio de dois frames separados (um contendo números e outro contendo strings de um arquivo), converteremos todos os dados em strings, escreveremos em um arquivo, e enviaremos o conteúdo desse arquivo em um único frame: 

//+------------------------------------------------------------------+
//| Handling completion of tester pass for agent                     |
//+------------------------------------------------------------------+
void CTesterHandler::Tester(double custom,   // Custom criteria
                            string params    // Description of EA parameters in the current pass
                           ) {
// Array of names of saved statistical characteristics of the pass
   ENUM_STATISTICS statNames[] = {
      STAT_INITIAL_DEPOSIT,
      STAT_WITHDRAWAL,
      STAT_PROFIT,
      ... 
   };

// Array for values of statistical characteristics of the pass as strings
   string stats[];
   ArrayResize(stats, ArraySize(statNames));

// Fill the array of values of statistical characteristics of the pass
   FOREACH(statNames, stats[i] = DoubleToString(TesterStatistics(statNames[i]), 2));

// Add the custom criterion value to it
   APPEND(stats, DoubleToString(custom, 2));

// Screen the quotes in the description of parameters just in case
   StringReplace(params, "'", "\\'");

// Open the file to write data for the frame
   int f = FileOpen(s_fileName, FILE_WRITE | FILE_TXT | FILE_ANSI);

// Write statistical characteristics
   FOREACH(stats, FileWriteString(f, stats[i] + ","));

// Write a description of the EA parameters
   FileWriteString(f, StringFormat("'%s'", params));

// Close the file
   FileClose(f);

// Create a frame with data from the recorded file and send it to the main terminal
   if(!FrameAdd("", 0, 0, s_fileName)) {
      PrintFormat(__FUNCTION__" | ERROR: Frame add error: %d", GetLastError());
   }
}


Finalmente, o método auxiliar que irá receber os frames de dados e salvar as informações deles no banco de dados. Nesse método, em um loop, processamos todos os frames recebidos que ainda não foram tratados até o momento. De cada frame, extraímos os dados como um array de caracteres e os convertemos em uma string. Em seguida, formamos uma string contendo os nomes e os valores dos parâmetros do ciclo com um índice específico. Esses valores serão usados para formar uma consulta SQL para inserir uma nova linha na tabela passes no nosso banco de dados. A consulta SQL criada é adicionada ao array de consultas SQL.

Após processar todos os frames de dados recebidos até o momento, executamos todas as consultas SQL do array dentro de uma única transação.

//+------------------------------------------------------------------+
//| Handling incoming frames                                         |
//+------------------------------------------------------------------+
void CTesterHandler::ProcessFrames(void) {
// Open the database
   DB::Open();

// Variables for reading data from frames
   string   name;      // Frame name (not used)
   ulong    pass;      // Frame pass index
   long     id;        // Frame type ID (not used) 
   double   value;     // Single frame value (not used)
   uchar    data[];    //  Frame data array as a character array

   string   values;    // Frame data as a string
   string   inputs;    // String with names and values of pass parameters
   string   query;     // A single SQL query string
   string   queries[]; // SQL queries for adding records to the database


// Go through frames and read data from them
   while(FrameNext(pass, name, id, value, data)) {
      // Convert the array of characters read from the frame into a string
      values = CharArrayToString(data);

      // Form a string with names and values of the pass parameters
      inputs = GetFrameInputs(pass);

      // Form an SQL query from the received data
      query = StringFormat("INSERT INTO passes "
                           "VALUES (NULL, %d, %s,\n'%s',\n'%s');",
                           pass, values, inputs,
                           TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS));

      // Add it to the SQL query array
      APPEND(queries, query);
   }

// Execute all requests
   DB::ExecuteTransaction(queries);

// Close the database
   DB::Close();
}


O método auxiliar que forma a string com os nomes e valores das variáveis de entrada do ciclo, GetFrameInputs(), foi retirado do manual de algotrading e adaptado para nossas necessidades.

O código gerado será salvo no arquivo TesterHandler.mqh na pasta atual.


Verificação do funcionamento

Para verificar o funcionamento, executaremos a otimização com um pequeno número de parâmetros a serem testados em um período de tempo não muito extenso. Após a conclusão do processo de otimização, poderemos verificar os resultados no testador de estratégias e no banco de dados criado.

Figura 1. Resultados da otimização no testador de estratégias

Figura 2. Resultados da otimização no banco de dados 

Como pode ser observado, os resultados no banco de dados coincidem com os resultados no testador: com a mesma ordenação pelo critério personalizado, vemos a mesma sequência de valores de lucro (profit) em ambos os lugares. O melhor ciclo indica que, em um ano, o lucro esperado pode ultrapassar $5000 com um depósito inicial de $10000 e um rebaixamento máximo permitido de 10% do saldo inicial ($1000). No entanto, neste momento, não são tão importantes as características quantitativas dos resultados da otimização quanto esses resultados agora podem ser armazenados no banco de dados.


Considerações finais

Assim, demos mais um passo em direção ao objetivo estabelecido. Conseguimos armazenar os resultados das otimizações dos parâmetros do EA em nosso banco de dados. Com isso, estabelecemos a base para a futura implementação automatizada da segunda etapa de desenvolvimento do Expert Advisor.

Muitas questões ainda ficaram em aberto. Muitas coisas durante o trabalho neste artigo tiveram que ser deixadas para o futuro, pois sua implementação demandaria esforços consideráveis. Mas, com os resultados obtidos, podemos formular com mais clareza o direcionamento do desenvolvimento futuro do projeto.

O armazenamento realizado funciona até agora para um único processo de otimização, no sentido de que salvamos as informações dos ciclos, mas ainda é difícil agrupar as linhas relacionadas a um único processo de otimização. Para isso, será necessário fazer modificações na estrutura do banco de dados, que atualmente é bastante simples. No futuro, tentaremos automatizar a execução de vários processos de otimização consecutivos com a definição prévia de diferentes variantes dos parâmetros a serem otimizados.

Obrigado pela atenção e até a próxima!



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

Arquivos anexados |
Advisor.mqh (4.4 KB)
Database.mqh (13.37 KB)
Interface.mqh (3.21 KB)
Macros.mqh (2.28 KB)
Money.mqh (4.61 KB)
NewBarEvent.mqh (11.52 KB)
Receiver.mqh (1.79 KB)
Strategy.mqh (1.73 KB)
TesterHandler.mqh (17.5 KB)
VirtualAdvisor.mqh (22.65 KB)
VirtualOrder.mqh (39.52 KB)
VirtualReceiver.mqh (17.43 KB)
Do básico ao intermediário: Comando SWITCH Do básico ao intermediário: Comando SWITCH
Neste artigo iremos aprender como utilizar o comando SWITCH em sua forma mais simples e básica. O conteúdo exposto aqui, visa e tem como objetivo, pura e simplesmente a didática. De modo algum deve ser encarado como sendo, uma aplicação cuja finalidade não venha a ser o aprendizado e estudo dos conceitos mostrados.
Redes neurais de maneira fácil (Parte 86): Transformador em forma de U Redes neurais de maneira fácil (Parte 86): Transformador em forma de U
Continuamos a analisar algoritmos de previsão de séries temporais. E neste artigo, proponho que você conheça o método U-shaped Transformer.
Redes neurais de maneira fácil (Parte 87): Segmentação de séries temporais Redes neurais de maneira fácil (Parte 87): Segmentação de séries temporais
A previsão desempenha um papel importante na análise de séries temporais. No novo artigo, falaremos sobre as vantagens da segmentação de séries temporais.
Desenvolvendo um sistema de Replay (Parte 64): Dando play no serviço (V) Desenvolvendo um sistema de Replay (Parte 64): Dando play no serviço (V)
Neste artigo irei mostrar como corrigir duas falhas que se encontram presentes no código. No entanto tais correções foram explicadas para que você, aspirante a programador, consiga entender que nem sempre as coisas irão acontecer como você havia previsto. Mas isto não é motivo para desespero e sim uma oportunidade de aprendizado. O conteúdo exposto aqui, visa e tem como objetivo, pura e simplesmente a didática. De modo algum deve ser encarado como sendo, uma aplicação cuja finalidade não venha a ser o aprendizado e estudo dos conceitos mostrados.