
Desenvolvendo um EA multimoeda (Parte 16): Influência de diferentes históricos de cotações nos resultados de testes
Introdução
Como de costume, lembramos que, na parte anterior, começamos a preparar o EA multimoeda em desenvolvimento para operar em uma conta real. Nesse processo, adicionamos suporte a diferentes nomes de instrumentos financeiros, encerramento automático das operações ao ajustar as configurações de estratégias de negociação e retomada adequada das operações do EA após reinicializações por diversos motivos.
Contudo, as etapas de preparação não se encerram aqui. Identificamos outras ações necessárias, mas abordaremos o assunto mais tarde. Por ora, vamos explorar um aspecto importante: garantir resultados consistentes do EA em desenvolvimento entre diferentes corretoras. Sabe-se que, embora semelhantes, as cotações dos instrumentos financeiros não são idênticas entre corretoras. Portanto, ao testar e otimizar com base em certas cotações, ajustamos os parâmetros ideais especificamente para essas condições. Espera-se que, ao operar com cotações diferentes, as variações sejam mínimas, resultando em pequenas diferenças nos resultados operacionais.
Entretanto, essa questão é importante demais para ser ignorada sem uma verificação detalhada. Por isso, analisaremos o comportamento do nosso EA ao ser testado com cotações de diferentes corretoras.
Comparação dos resultados
Primeiramente, executaremos nosso EA com cotações do servidor MetaQuotes-Demo. O primeiro teste foi realizado com o gerenciamento de risco ativado. No entanto, adiantando os fatos, observamos que, com outras cotações, o gerenciamento de risco encerrou as operações muito antes do fim do intervalo de teste, assim, desativaremos essa função para obter uma visão mais completa dos resultados. Isso permitirá uma comparação mais justa dos resultados. Eis o que obtivemos:
Figura 1. Resultados dos testes com cotações do servidor MetaQuotes-Demo sem o gerenciamento de risco
Agora, conectaremos o terminal a um servidor real de outra corretora e repetiremos o teste do EA com os mesmos parâmetros:
Figura 2. Resultados dos testes com cotações de um servidor real de outra corretora sem o gerenciamento de risco
Essa foi uma reviravolta inesperada. A conta foi completamente zerada em menos de um ano dos dois previstos. Vamos investigar as razões desse comportamento para determinar se é possível corrigir a situação.
Investigando a causa
Salvaremos os relatórios do testador no formato de arquivos XML. Abriremos os arquivos e identificaremos a seção onde começa a lista de operações realizadas. Organizaremos as janelas dos arquivos abertos de modo que seja possível visualizar simultaneamente as partes superiores das listas de operações dos dois relatórios.
Figura 3. Partes superiores das listas de operações realizadas pelo EA durante os testes com cotações de diferentes servidores
Mesmo pelas primeiras linhas dos relatórios, fica claro que as posições foram abertas em momentos distintos. Assim, se houve diferenças nas cotações para os mesmos instantes em servidores distintos, provavelmente elas não tiveram um impacto tão destrutivo quanto os diferentes momentos de abertura.
Vamos analisar onde os momentos de abertura de posições são definidos nas estratégias, para isso devemos verificar o arquivo que implementa a classe da estratégia de negociação simples, SimpleVolumesStrategy.mqh. Ao observarmos o código, rapidamente encontramos o método SignalForOpen(), que retorna o sinal para abertura:
//+------------------------------------------------------------------+ //| Signal for opening pending orders | //+------------------------------------------------------------------+ int CSimpleVolumesStrategy::SignalForOpen() { // By default, there is no signal int signal = 0; // Copy volume values from the indicator buffer to the receiving array int res = CopyBuffer(m_iVolumesHandle, 0, 0, m_signalPeriod, m_volumes); // If the required amount of numbers have been copied if(res == m_signalPeriod) { // Calculate their average value double avrVolume = ArrayAverage(m_volumes); // If the current volume exceeds the specified level, then if(m_volumes[0] > avrVolume * (1 + m_signalDeviation + m_ordersTotal * m_signaAddlDeviation)) { // if the opening price of the candle is less than the current (closing) price, then if(iOpen(m_symbol, m_timeframe, 0) < iClose(m_symbol, m_timeframe, 0)) { signal = 1; // buy signal } else { signal = -1; // otherwise, sell signal } } } return signal; }
Vemos que o sinal de abertura é baseado nos valores dos volumes de ticks para o instrumento financeiro atual. Os preços (atuais ou anteriores) não são considerados diretamente na geração desse sinal. Na verdade, eles são considerados apenas após a decisão de abrir a posição, influenciando unicamente a direção da abertura. Portanto, parece que a questão reside nas grandes diferenças nos valores dos volumes de ticks provenientes de servidores distintos.
Isso é perfeitamente plausível, já que, para que os gráficos de velas coincidam visualmente entre corretoras, basta fornecer apenas quatro ticks corretos por minuto para construir os preços de Open, Close, High e Low da menor vela no período M1. A quantidade de ticks intermediários dentro dos limites entre Low e High não é relevante. Assim, o número de ticks armazenados no histórico e sua distribuição temporal dentro de uma única vela dependem exclusivamente da corretora, que possui liberdade para definir os parâmetros mais convenientes. Além disso, é importante lembrar que até mesmo servidores de contas demo e reais de uma mesma corretora podem apresentar diferenças.
Se o problema for realmente esse, será possível contorná-lo. Porém, antes de implementarmos essa solução, é essencial garantir que a causa das divergências observadas tenha sido identificada corretamente, para evitar esforços desperdiçados.
Definindo o caminho
Para verificar nossa hipótese, utilizaremos os seguintes instrumentos:
- Armazenamento do histórico. Adicionaremos ao nosso EA a capacidade de salvar o histórico de operações (abertura e fechamento de posições) ao final de um teste no testador. Esse armazenamento poderá ser feito em arquivo ou em banco de dados. Como inicialmente esse recurso será usado apenas como ferramenta auxiliar, parece mais simples optar pelo armazenamento em arquivo. Caso decidamos utilizá-lo de forma mais permanente no futuro, poderemos expandi-lo para incluir o suporte ao salvamento em banco de dados.
- Reprodução de negociações. Desenvolveremos um novo EA que não terá nenhuma regra para abertura de posições, mas apenas reproduzirá as operações de abertura e fechamento, lendo-as do histórico salvo pelo outro EA. Como decidimos, por enquanto, salvar o histórico em arquivo, esse novo EA receberá como parâmetro de entrada o nome do arquivo contendo o histórico de negociações, lendo e executando as operações nele registradas.
Após criarmos essas ferramentas, faremos dois testes distintos. No primeiro, executaremos nosso EA no testador utilizando cotações do servidor MetaQuotes-Demo e salvaremos o histórico de operações desse teste em um arquivo. Este será o primeiro teste. Em seguida, utilizaremos o novo EA de reprodução de negociações para repetir o teste com cotações de outro servidor, utilizando o arquivo de histórico salvo anteriormente. Este será o segundo teste. Se as diferenças observadas anteriormente nos resultados das operações forem realmente causadas por grandes variações nos dados de volume de ticks, enquanto os preços permanecerem semelhantes, o segundo teste deverá apresentar resultados semelhantes aos do primeiro.
Salvando o histórico
Existem diversas maneiras de implementar o salvamento do histórico. Por exemplo, podemos adicionar um método à classe CVirtualAdvisor, que seria chamado pelo manipulador de eventos OnTester(). Contudo, essa abordagem amplia a classe existente, adicionando funcionalidades que, em princípio, não são essenciais para o funcionamento principal. Por isso, optaremos por criar uma classe separada, chamada CExpertHistory, dedicada exclusivamente a essa tarefa. Não será necessário instanciar múltiplos objetos dessa classe, podemos torná-la estática, contendo apenas propriedades e métodos estáticos.
O principal método público da classe será Export(), enquanto os demais métodos desempenharão funções auxiliares. O método Export() receberá dois parâmetros: o nome do arquivo onde o histórico será salvo e uma flag para indicar o uso da pasta de dados compartilhada do terminal. O nome do arquivo pode, por padrão, ser uma string vazia. Nesse caso, será gerado automaticamente por um método auxiliar chamado GetHistoryFileName(). Com a flag para salvar na pasta compartilhada, podemos decidir se o arquivo será armazenado na pasta compartilhada ou na pasta local do terminal. Por padrão, o valor estará configurado para salvar na pasta compartilhada, já que abrir a pasta local do agente de teste é mais complicado durante a execução no testador.
Como propriedades da classe, utilizaremos um símbolo delimitador para a abertura do arquivo CSV em modo de gravação, um manipulador para o arquivo aberto, que será utilizado nos métodos auxiliares, e um array com os nomes das colunas dos dados a serem salvos.
//+------------------------------------------------------------------+ //| Export trade history to file | //+------------------------------------------------------------------+ class CExpertHistory { private: static string s_sep; // Separator character static int s_file; // File handle for writing static string s_columnNames[]; // Array of column names // Write deal history to file static void WriteDealsHistory(); // Write one row of deal history to file static void WriteDealsHistoryRow(const string &fields[]); // Get the first deal date static datetime GetStartDate(); // Form a file name static string GetHistoryFileName(); public: // Export deal history static void Export( string exportFileName = "", // File name for export. If empty, the name is generated int commonFlag = FILE_COMMON // Save the file in shared data folder ); }; // Static class variables string CExpertHistory::s_sep = ","; int CExpertHistory::s_file; string CExpertHistory::s_columnNames[] = {"DATE", "TICKET", "TYPE", "SYMBOL", "VOLUME", "ENTRY", "PRICE", "STOPLOSS", "TAKEPROFIT", "PROFIT", "COMMISSION", "FEE", "SWAP", "MAGIC", "COMMENT" };
No método principal Export(), o arquivo será criado e aberto para gravação com o nome especificado ou gerado. Se o arquivo for aberto com sucesso, o método de gravação do histórico de operações será chamado, e o arquivo será fechado.
//+------------------------------------------------------------------+ //| Export deal history | //+------------------------------------------------------------------+ void CExpertHistory::Export(string exportFileName = "", int commonFlag = FILE_COMMON) { // If the file name is not specified, then generate it if(exportFileName == "") { exportFileName = GetHistoryFileName(); } // Open the file for writing in the desired data folder s_file = FileOpen(exportFileName, commonFlag | FILE_WRITE | FILE_CSV | FILE_ANSI, s_sep); // If the file is open, if(s_file > 0) { // Set the deal history WriteDealsHistory(); // Close the file FileClose(s_file); } else { PrintFormat(__FUNCTION__" | ERROR: Can't open file [%s]. Last error: %d", exportFileName, GetLastError()); } }
No método GetHistoryFileName(), o nome do arquivo será composto de vários fragmentos. Primeiro, será adicionado o nome do EA e sua versão, caso especificada na constante __VERSION__. Em segundo lugar, incluirá as datas de início e término do histórico de operações. A data de início será determinada pela data da primeira operação no histórico, obtida pelo método GetStartDate(). A data de término será baseada no horário atual, pois o histórico será exportado após a conclusão do teste. Ou seja, o horário atual no momento da chamada do método de salvamento representará o fim do teste. Por fim, ao nome do arquivo serão adicionados valores de algumas métricas do teste: saldo inicial, saldo final, rebaixamento e o índice de Sharpe.
Se o nome for muito longo, ele será encurtado para o tamanho permitido e será adicionado o sufixo ".history.csv".
//+------------------------------------------------------------------+ //| Form the file name | //+------------------------------------------------------------------+ string CExpertHistory::GetHistoryFileName() { // Take the EA name string fileName = MQLInfoString(MQL_PROGRAM_NAME); // If a version is specified, add it #ifdef __VERSION__ fileName += "." + __VERSION__; #endif fileName += " "; // Add the history start and end date fileName += "[" + TimeToString(GetStartDate(), TIME_DATE); fileName += " - " + TimeToString(TimeCurrent(), TIME_DATE) + "]"; fileName += " "; // Add some statistical characteristics fileName += "[" + DoubleToString(TesterStatistics(STAT_INITIAL_DEPOSIT), 0); fileName += ", " + DoubleToString(TesterStatistics(STAT_INITIAL_DEPOSIT) + TesterStatistics(STAT_PROFIT), 0); fileName += ", " + DoubleToString(TesterStatistics(STAT_EQUITY_DD_RELATIVE), 0); fileName += ", " + DoubleToString(TesterStatistics(STAT_SHARPE_RATIO), 2); fileName += "]"; // If the name is too long, shorten it if(StringLen(fileName) > 255 - 13) { fileName = StringSubstr(fileName, 0, 255 - 13); } // Add extension fileName += ".history.csv"; return fileName; }
No método de gravação do histórico em arquivo, primeiro gravamos o cabeçalho, ou seja, uma linha com os nomes das colunas de dados. Em seguida, selecionamos todo o histórico disponível e iteramos sobre todas as operações. Para cada operação, obtemos suas propriedades. Se a operação for de abertura de posição ou uma operação de balanço, formamos um array com os valores de todas as propriedades da operação e o passamos para o método de gravação de uma operação WriteDealsHistoryRow().
//+------------------------------------------------------------------+ //| Write deal history to file | //+------------------------------------------------------------------+ void CExpertHistory::WriteDealsHistory() { // Write a header with column names WriteDealsHistoryRow(s_columnNames); // Variables for each deal properties uint total; ulong ticket = 0; long entry; double price; double sl, tp; double profit, commission, fee, swap; double volume; datetime time; string symbol; long type, magic; string comment; // Take the entire history HistorySelect(0, TimeCurrent()); total = HistoryDealsTotal(); // For all deals for(uint i = 0; i < total; i++) { // If the deal is successfully selected, if((ticket = HistoryDealGetTicket(i)) > 0) { // Get the values of its properties time = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME); type = HistoryDealGetInteger(ticket, DEAL_TYPE); symbol = HistoryDealGetString(ticket, DEAL_SYMBOL); volume = HistoryDealGetDouble(ticket, DEAL_VOLUME); entry = HistoryDealGetInteger(ticket, DEAL_ENTRY); price = HistoryDealGetDouble(ticket, DEAL_PRICE); sl = HistoryDealGetDouble(ticket, DEAL_SL); tp = HistoryDealGetDouble(ticket, DEAL_TP); profit = HistoryDealGetDouble(ticket, DEAL_PROFIT); commission = HistoryDealGetDouble(ticket, DEAL_COMMISSION); fee = HistoryDealGetDouble(ticket, DEAL_FEE); swap = HistoryDealGetDouble(ticket, DEAL_SWAP); magic = HistoryDealGetInteger(ticket, DEAL_MAGIC); comment = HistoryDealGetString(ticket, DEAL_COMMENT); if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL || type == DEAL_TYPE_BALANCE) { // Replace the separator characters in the comment with a space StringReplace(comment, s_sep, " "); // Form an array of values for writing one deal to the file string string fields[] = {TimeToString(time, TIME_DATE | TIME_MINUTES | TIME_SECONDS), IntegerToString(ticket), IntegerToString(type), symbol, DoubleToString(volume), IntegerToString(entry), DoubleToString(price, 5), DoubleToString(sl, 5), DoubleToString(tp, 5), DoubleToString(profit), DoubleToString(commission), DoubleToString(fee), DoubleToString(swap), IntegerToString(magic), comment }; // Set the values of a single deal to the file WriteDealsHistoryRow(fields); } } } }
No método WriteDealsHistoryRow(), simplesmente concatenamos todos os valores do array recebido em uma única linha, separados por um delimitador especificado, e gravamos no arquivo CSV aberto. Para concatenar os valores, utilizamos o novo macro JOIN, que foi adicionado à nossa coleção de macros no arquivo Macros.mqh.
//+------------------------------------------------------------------+ //| Write one row of deal history to the file | //+------------------------------------------------------------------+ void CExpertHistory::WriteDealsHistoryRow(const string &fields[]) { // Row to be set string row = ""; // Concatenate all array values into one row using a separator JOIN(fields, row, ","); // Write a row to the file FileWrite(s_file, row); }
Salvaremos as alterações realizadas no arquivo ExpertHistory.mqh na pasta atual.
Agora resta pouco: conectar esse arquivo ao arquivo do EA e adicionar a chamada do método CExpertHistory::Export() no manipulador de eventos OnTester().
... #include "ExpertHistory.mqh" ... //+------------------------------------------------------------------+ //| Test results | //+------------------------------------------------------------------+ double OnTester(void) { CExpertHistory::Export(); return expert.Tester(); }
Salvaremos as alterações realizadas no arquivo SimpleVolumesExpert.mq5 na pasta atual.
Executaremos o teste do EA. Após a conclusão, na pasta comum de dados, surgiu um arquivo com o nome:
SimpleVolumesExpert.1.19 [2021.01.01 - 2022.12.30] [10000, 34518, 1294, 3.75].history.csv
Pelo nome, fica claro que o histórico de operações cobre dois anos (2021 e 2022), o saldo inicial da conta era de $10.000, e o saldo final foi de $34.518. Durante o intervalo de teste, a máxima redução relativa nos fundos foi de $1.294, e o índice de Sharpe foi de 3,75. Se abrirmos o arquivo gerado no Excel, veremos o seguinte:
Figura 4. Resultados da exportação do histórico de operações para o arquivo CSV
Os dados parecem estar corretos. Agora, vamos para a segunda etapa: escrever um EA que possa reproduzir operações em outra conta com base nesse arquivo CSV.
Reprodução de operações
Começaremos a implementação do novo EA criando a estratégia de negociação. Afinal, seguir as instruções de outro sobre quando e quais posições abrir também pode ser chamado de estratégia de negociação. Se a fonte dos sinais for confiável, por que não utilizá-los? Assim, criaremos uma nova classe CHistoryStrategy, herdando-a de CVirtualStrategy. Os métodos que precisaremos implementar obrigatoriamente nessa classe incluem o construtor, o método de processamento de tick e o método de conversão para string. Embora o último método não seja útil neste caso, sua presença é obrigatória devido à herança, já que na classe pai ele é abstrato.
Como ficará claro adiante, na nova classe será suficiente adicionar as seguintes propriedades:
- m_symbols — array com os nomes dos símbolos (instrumentos financeiros);
- m_history — matriz bidimensional para leitura do histórico de operações do arquivo (N linhas x 15 colunas);
- m_totalDeals — número total de operações no histórico;
- m_currentDeal — número atual da operação;
- m_symbolInfo — objeto para obter informações sobre as propriedades do símbolo.
//+------------------------------------------------------------------+ //| Trading strategy for reproducing the history of deals | //+------------------------------------------------------------------+ class CHistoryStrategy : public CVirtualStrategy { protected: string m_symbols[]; // Symbols (trading instruments) string m_history[][15]; // Array of deal history (N rows * 15 columns) int m_totalDeals; // Number of deals in history int m_currentDeal; // Current deal index CSymbolInfo m_symbolInfo; // Object for getting information about the symbol properties public: CHistoryStrategy(string p_params); // Constructor virtual void Tick() override; // OnTick event handler virtual string operator~() override; // Convert object to string };
O construtor da estratégia deve aceitar um argumento: uma string de inicialização. Esse requisito também decorre da herança. A string de inicialização deve conter todos os valores necessários, e o construtor será responsável por lê-los e utilizá-los de forma apropriada. Para esta estratégia simples, basta que a string de inicialização contenha apenas um valor: o nome do arquivo com o histórico. Todos os demais dados necessários para o funcionamento da estratégia serão extraídos desse arquivo. O construtor pode ser implementado da seguinte maneira:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CHistoryStrategy::CHistoryStrategy(string p_params) { m_params = p_params; // Read the file name from the parameters string fileName = ReadString(p_params); // If the name is read, then if(IsValid()) { // Attempting to open a file in the data folder int f = FileOpen(fileName, FILE_READ | FILE_CSV | FILE_ANSI | FILE_SHARE_READ, ','); // If failed to open a file, then try to open the file from the shared folder if(f == INVALID_HANDLE) { f = FileOpen(fileName, FILE_COMMON | FILE_READ | FILE_CSV | FILE_ANSI | FILE_SHARE_READ, ','); } // If this does not work, report an error and exit if(f == INVALID_HANDLE) { SetInvalid(__FUNCTION__, StringFormat("ERROR: Can't open file %s from common folder %s, error code: %d", fileName, TerminalInfoString(TERMINAL_COMMONDATA_PATH), GetLastError())); return; } // Read the file up to the header string (usually it comes first) while(!FileIsEnding(f)) { string s = FileReadString(f); // If we find a header string, read the names of all columns without saving them if(s == "DATE") { FORI(14, FileReadString(f)); break; } } // Read the remaining rows until the end of the file while(!FileIsEnding(f)) { // If the array for storing the read history is filled, increase its size if(m_totalDeals == ArraySize(m_history)) { ArrayResize(m_history, ArraySize(m_history) + 10000, 100000); } // Read 15 values from the next file string into the array string FORI(15, m_history[m_totalDeals][i] = FileReadString(f)); // If the deal symbol is not empty, if(m_history[m_totalDeals][SYMBOL] != "") { // Add it to the symbol array if there is no such symbol there yet ADD(m_symbols, m_history[m_totalDeals][SYMBOL]); } // Increase the counter of read deals m_totalDeals++; } // Close the file FileClose(f); PrintFormat(__FUNCTION__" | OK: Found %d rows in %s", m_totalDeals, fileName); // If there are read deals except for the very first one (account top-up), then if(m_totalDeals > 1) { // Set the exact size for the history array ArrayResize(m_history, m_totalDeals); // Current time datetime ct = TimeCurrent(); PrintFormat(__FUNCTION__" |\n" "Start time in tester: %s\n" "Start time in history: %s", TimeToString(ct, TIME_DATE), m_history[0][DATE]); // If the test start date is greater than the history start date, then report an error if(StringToTime(m_history[0][DATE]) < ct) { SetInvalid(__FUNCTION__, StringFormat("ERROR: For this history file [%s] set start date less than %s", fileName, m_history[0][DATE])); } } // Create virtual positions for each symbol CVirtualReceiver::Get(GetPointer(this), m_orders, ArraySize(m_symbols)); // Register the event handler for a new bar on the minimum timeframe FOREACH(m_symbols, IsNewBar(m_symbols[i], PERIOD_M1)); } }
Nele, lemos o nome do arquivo a partir da string de inicialização e tentamos abri-lo. Se o arquivo for aberto com sucesso, seja da pasta local ou da pasta comum de dados, lemos seu conteúdo e preenchemos o array m_history. Durante a leitura, também adicionamos os nomes dos símbolos ao array m_symbols sempre que um novo nome é encontrado. Essa tarefa é realizada pelo macro ADD().
Simultaneamente, contamos o número de registros de operações lidos, armazenando-o na propriedade m_totalDeals. Este número é usado como índice da primeira dimensão do array m_history, onde as informações sobre cada operação são registradas. Após ler todo o conteúdo do arquivo, fechamos o arquivo.
Em seguida, verificamos se a data de início do teste é posterior à data de início do histórico. Essa situação não é permitida, pois tornaria impossível modelar parte das operações presentes no início do histórico, o que poderia levar a distorções nos resultados durante o teste. Portanto, o construtor só criará um objeto válido se o histórico de operações começar antes ou ao mesmo tempo que a data de início do teste.
Um aspecto essencial no construtor é a alocação de posições virtuais exclusivamente baseada na quantidade de nomes de símbolos únicos encontrados no histórico. Como a tarefa da estratégia é garantir o volume adequado de posições abertas para cada símbolo, isso pode ser alcançado utilizando apenas uma posição virtual por símbolo.
O método de processamento de ticks operará exclusivamente com o array de operações lidas. Como aberturas ou fechamentos de posições podem ocorrer para vários símbolos ao mesmo tempo, organizaremos um loop que processará todas as linhas do histórico de operações cujo tempo seja menor ou igual ao tempo atual. As operações restantes serão processadas em ticks futuros, à medida que o tempo atual avançar e novas operações se tornarem relevantes.
Se for encontrada pelo menos uma operação a ser processada, identificaremos seu símbolo e o índice correspondente no array m_symbols. Com base nesse índice, determinaremos qual posição virtual no array m_orders é responsável por esse símbolo. Caso o índice não seja encontrado (o que não deveria ocorrer durante uma operação correta), simplesmente ignoraremos essa operação. Também ignoraremos operações que representem transações de balanço na conta.
Agora começa a parte mais interessante. Precisamos processar a operação lida. Aqui, há duas situações possíveis: não existe uma posição virtual aberta para o símbolo em questão ou já há uma posição virtual aberta.
No primeiro caso, é simples: abrimos uma posição na direção da operação, com o volume correspondente. No segundo caso, pode ser necessário aumentar o volume da posição atual para o símbolo ou reduzi-lo. E essa redução pode ser tão grande que a direção da posição aberta se inverta.
Para simplificar os cálculos, procederemos da seguinte forma:
- Converteremos o volume da nova operação para um formato "com sinal". Ou seja, se a operação for na direção SELL, atribuímos a ela um volume negativo.
- Obteremos o volume da posição aberta para o mesmo símbolo da nova operação. O método CVirtualOrder::Volume() já retorna o volume nesse formato "com sinal".
- Somaremos o volume da nova operação ao volume da posição já aberta. O resultado será o novo volume que deve permanecer aberto após considerar a nova operação. Este valor também estará no formato "com sinal".
- Fecharemos a posição virtual atualmente aberta.
- Se o novo volume não for zero, abrimos uma nova posição virtual para o símbolo. A direção será determinada pelo sinal do novo volume (positivo para BUY, negativo para SELL), e o módulo do novo volume será passado como argumento ao método de abertura de posição virtual.
Após esse procedimento, incrementamos o contador de operações processadas no histórico e avançamos para a próxima iteração do laço. Se não houver mais operações para processar atualmente ou se o histórico tiver terminado, a execução do tick é encerrada.
//+------------------------------------------------------------------+ //| OnTick event handler | //+------------------------------------------------------------------+ void CHistoryStrategy::Tick() override { //--- while(m_currentDeal < m_totalDeals && StringToTime(m_history[m_currentDeal][DATE]) <= TimeCurrent()) { // Deal symbol string symbol = m_history[m_currentDeal][SYMBOL]; // Find the index of the current deal symbol in the array of symbols int index; FIND(m_symbols, symbol, index); // If not found, then skip the current deal if(index == -1) { m_currentDeal++; continue; } // Deal type ENUM_DEAL_TYPE type = (ENUM_DEAL_TYPE) StringToInteger(m_history[m_currentDeal][TYPE]); // Current deal volume double volume = NormalizeDouble(StringToDouble(m_history[m_currentDeal][VOLUME]), 2); // If this is a top-up/withdrawal, skip the deal if(volume == 0) { m_currentDeal++; continue; } // Report information about the read deal PrintFormat(__FUNCTION__" | Process deal #%d: %s %.2f %s", m_currentDeal, (type == DEAL_TYPE_BUY ? "BUY" : (type == DEAL_TYPE_SELL ? "SELL" : EnumToString(type))), volume, symbol); // If this is a sell deal, then make the volume negative if(type == DEAL_TYPE_SELL) { volume *= -1; } // If the virtual position for the current deal symbol is open, if(m_orders[index].IsOpen()) { // Add its volume to the volume of the current trade volume += m_orders[index].Volume(); // Close the virtual position m_orders[index].Close(); } // If the volume for the current symbol is not 0, if(MathAbs(volume) > 0.00001) { // Open a virtual position of the required volume and direction m_orders[index].Open(symbol, (volume > 0 ? ORDER_TYPE_BUY : ORDER_TYPE_SELL), MathAbs(volume)); } // Increase the counter of handled deals m_currentDeal++; } }
Salvaremos o código obtido no arquivo HistoryStrategy.mqh na pasta atual.
Agora, criaremos um novo arquivo de EA baseado no já existente SimpleVolumesExpert.mq5. Para alcançar o resultado desejado, precisamos adicionar um parâmetro de entrada ao EA, no qual será possível especificar o nome do arquivo com o histórico.
input group "::: Testing the deal history" input string historyFileName_ = ""; // File with history
A parte do código responsável por carregar as strings de inicialização das estratégias a partir do banco de dados agora é desnecessária, então a removeremos.
Na string de inicialização do EA, incluiremos a criação de uma instância da estratégia da classe CHistoryStrategy, passando o nome do arquivo com o histórico como argumento:
// Prepare the initialization string for an EA with a group of several strategies string expertParams = StringFormat( "class CVirtualAdvisor(\n" " class CVirtualStrategyGroup(\n" " [\n" " class CHistoryStrategy(\"%s\")\n" " ],%f\n" " ),\n" " class CVirtualRiskManager(\n" " %d,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%.2f,%d,%.2f,%.2f" " )\n" " ,%d,%s,%d\n" ")", historyFileName_, scale_, rmIsActive_, rmStartBaseBalance_, rmCalcDailyLossLimit_, rmMaxDailyLossLimit_, rmCloseDailyPart_, rmCalcOverallLossLimit_, rmMaxOverallLossLimit_, rmCloseOverallPart_, rmCalcOverallProfitLimit_, rmMaxOverallProfitLimit_, rmMaxOverallProfitDate_, rmMaxRestoreTime_, rmLastVirtualProfitFactor_, magic_, "HistoryReceiver", useOnlyNewBars_ );
Com isso, as alterações no arquivo do EA estão concluídas. Salvaremos o arquivo sob o nome HistoryReceiverExpert.mq5 na pasta atual.
Agora temos um EA funcional, capaz de reproduzir o histórico de operações. Na verdade, suas capacidades vão além. Podemos verificar facilmente como os resultados da negociação se comportam ao aumentarmos o volume das posições abertas proporcionalmente ao crescimento do saldo da conta, mesmo que o histórico tenha sido registrado com base em operações fixas. Podemos aplicar diferentes parâmetros de gerenciamento de risco para avaliar seu impacto nas negociações, mesmo que o histórico tenha sido gravado com outros parâmetros ou até mesmo com o gerenciamento de risco desativado. Após cada execução no testador, o histórico de operações é salvo automaticamente em um novo arquivo.
No entanto, se não precisamos, por enquanto, de todas essas funcionalidades adicionais, se não queremos ativar o gerenciamento de risco e se preferimos evitar uma longa lista de parâmetros de entrada não utilizados relacionados a ele, podemos criar uma nova classe de EA simplificada, sem nada "supérfluo". Nesta classe, podemos também eliminar o salvamento do estado, o desenho das posições virtuais nos gráficos e outras funcionalidades pouco utilizadas.
A implementação dessa classe pode ser algo assim:
//+------------------------------------------------------------------+ //| Trade history replay EA class | //+------------------------------------------------------------------+ class CVirtualHistoryAdvisor : public CAdvisor { protected: CVirtualReceiver *m_receiver; // Receiver object that brings positions to the market bool m_useOnlyNewBar; // Handle only new bar ticks datetime m_fromDate; // Test start time public: CVirtualHistoryAdvisor(string p_param); // Constructor ~CVirtualHistoryAdvisor(); // Destructor virtual void Tick() override; // OnTick event handler virtual double Tester() override; // OnTester event handler virtual string operator~() override; // Convert object to string }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualHistoryAdvisor::CVirtualHistoryAdvisor(string p_params) { // Save the initialization string m_params = p_params; // Read the file name from the initialization string string fileName = ReadString(p_params); // Read the work flag only at the bar opening m_useOnlyNewBar = (bool) ReadLong(p_params); // If there are no read errors, if(IsValid()) { if(!MQLInfoInteger(MQL_TESTER)) { // Otherwise, set the object state to invalid SetInvalid(__FUNCTION__, "ERROR: This expert can run only in tester"); return; } if(fileName == "") { // Otherwise, set the object state to invalid SetInvalid(__FUNCTION__, "ERROR: Set file name with deals history in "); return; } string strategyParams = StringFormat("class CHistoryStrategy(\"%s\")", fileName); CREATE(CHistoryStrategy, strategy, strategyParams); Add(strategy); // Initialize the receiver with the static receiver m_receiver = CVirtualReceiver::Instance(65677); // Save the work (test) start time m_fromDate = TimeCurrent(); } } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CVirtualHistoryAdvisor::~CVirtualHistoryAdvisor() { if(!!m_receiver) delete m_receiver; // Remove the recipient DestroyNewBar(); // Remove the new bar tracking objects } //+------------------------------------------------------------------+ //| OnTick event handler | //+------------------------------------------------------------------+ void CVirtualHistoryAdvisor::Tick(void) { // Define a new bar for all required symbols and timeframes bool isNewBar = UpdateNewBar(); // If there is no new bar anywhere, and we only work on new bars, then exit if(!isNewBar && m_useOnlyNewBar) { return; } // Start handling in strategies CAdvisor::Tick(); // Receiver handles virtual positions m_receiver.Tick(); // Adjusting market volumes m_receiver.Correct(); } //+------------------------------------------------------------------+ //| OnTester event handler | //+------------------------------------------------------------------+ double CVirtualHistoryAdvisor::Tester() { // Maximum absolute drawdown double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD); // Profit double profit = TesterStatistics(STAT_PROFIT); // Fixed balance for trading from settings double fixedBalance = CMoney::FixedBalance(); // The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_ double coeff = fixedBalance * 0.1 / MathMax(1, balanceDrawdown); // Calculate the profit in annual terms long totalSeconds = TimeCurrent() - m_fromDate; double totalYears = totalSeconds / (365.0 * 24 * 3600); double fittedProfit = profit * coeff / totalYears; // If it is not specified, then take the initial balance (although this will give a distorted result) if(fixedBalance < 1) { fixedBalance = TesterStatistics(STAT_INITIAL_DEPOSIT); balanceDrawdown = TesterStatistics(STAT_EQUITY_DDREL_PERCENT); coeff = 0.1 / balanceDrawdown; fittedProfit = fixedBalance * MathPow(1 + profit * coeff / fixedBalance, 1 / totalYears); } return fittedProfit; } //+------------------------------------------------------------------+ //| Convert an object to a string | //+------------------------------------------------------------------+ string CVirtualHistoryAdvisor::operator~() { return StringFormat("%s(%s)", typename(this), m_params); } //+------------------------------------------------------------------+
O EA desta classe aceitará apenas dois parâmetros na string de inicialização: o nome do arquivo com o histórico e uma flag para operar apenas na abertura de um candle de minuto. Salvaremos este código no arquivo VirtualHistoryAdvisor.mqh na pasta atual.
O arquivo do EA que utiliza essa classe também pode ser simplificado em relação ao exemplo anterior:
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "::: Testing the deal history" input string historyFileName_ = ""; // File with history input group "::: Money management" sinput double fixedBalance_ = 10000; // - Used deposit (0 - use all) in the account currency input double scale_ = 1.00; // - Group scaling multiplier input group "::: Other parameters" input bool useOnlyNewBars_ = true; // - Work only at bar opening datetime fromDate = TimeCurrent(); // Operation start time CVirtualHistoryAdvisor *expert; // EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Set parameters in the money management class CMoney::DepoPart(scale_); CMoney::FixedBalance(fixedBalance_); // Prepare the initialization string for the deal history replay EA string expertParams = StringFormat( "class CVirtualHistoryAdvisor(\"%s\",%f,%d)", historyFileName_, useOnlyNewBars_ ); // Create an EA handling virtual positions expert = NEW(expertParams); // If the EA is not created, then return an error if(!expert) return INIT_FAILED; // Successful initialization return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { expert.Tick(); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if(!!expert) delete expert; } //+------------------------------------------------------------------+ //| Test results | //+------------------------------------------------------------------+ double OnTester(void) { return expert.Tester(); } //+------------------------------------------------------------------+
Salvaremos este código no arquivo SimpleHistoryReceiverExpert.mq5 na pasta atual.
Resultados do Teste
Executaremos um dos EAs criados, especificando o nome correto do arquivo com o histórico de operações salvo. Primeiro, rodaremos no mesmo servidor de cotações utilizado para gerar o histórico (MetaQuotes-Demo). Os resultados obtidos coincidiram completamente com os resultados originais! É preciso admitir que isso foi um resultado surpreendentemente bom, demonstrando a correção da implementação planejada.
Agora, vejamos o que acontece ao executar o EA em outro servidor:
Figura 5. Resultados da reprodução do histórico de operações em cotações de um servidor real de outra corretora.
O gráfico da curva de saldo é, visualmente, quase indistinguível do gráfico dos resultados originais no MetaQuotes-Demo. No entanto, os valores numéricos apresentam pequenas diferenças. Vamos revisar os valores originais para comparação:
Figura 6. Resultados do teste original nas cotações do servidor MetaQuotes-Demo.
Notamos uma leve redução no lucro médio anual geral e normalizado, no índice de Sharpe e um pequeno aumento no rebaixamento. Contudo, esses resultados não se comparam à perda total do depósito que obtivemos inicialmente ao rodar o EA com cotações reais de outro servidor. Isso é bastante encorajador e abre novas possibilidades para questões que talvez precisemos resolver ao preparar o EA para uso em negociações reais.
Considerações finais
É hora de resumir os resultados parciais. Demonstramos que a estratégia de negociação utilizada pode ter consequências muito negativas ao trocar o servidor de cotações. No entanto, ao entender as causas desse comportamento, mostramos que os resultados de negociação voltam a ser comparáveis ao manter a lógica dos sinais de abertura de posições no servidor com as cotações originais e transferir apenas as operações de abertura e fechamento para um novo servidor.
Para isso, desenvolvemos duas ferramentas funcionais: uma para salvar o histórico de operações após a execução do testador e outra para reproduzir as operações com base nesse histórico. No entanto, essas ferramentas são úteis apenas no ambiente de teste, não se aplicando a negociações reais. Na negociação real, elas são irrelevantes. Agora, com os testes confirmando a viabilidade dessa abordagem, podemos começar a implementá-la em operações reais com maior confiança.
Precisaremos dividir o EA em dois EAs distintos. O primeiro será responsável por tomar decisões sobre a abertura de posições e executá-las, operando no servidor de cotações mais conveniente. Simultaneamente, enquanto realiza negociações, ele deverá transmitir a lista de posições abertas em um formato que o segundo EA consiga interpretar. O segundo EA, por sua vez, funcionará em outro terminal, conectado, se necessário, a um servidor de cotações diferente. Sua tarefa será manter o volume de posições abertas constantemente conforme os valores transmitidos pelo primeiro EA. Esse esquema de funcionamento permitirá superar a limitação identificada no início deste artigo.
É possível ir ainda mais além. O esquema mencionado pressupõe, implicitamente, que os dois terminais estejam operando no mesmo computador. No entanto, isso não é obrigatório. Os terminais podem funcionar em computadores diferentes, desde que o primeiro EA consiga transmitir as informações sobre posições ao segundo EA por algum meio de comunicação. É claro que, dessa forma, não será possível implementar com sucesso estratégias de negociação que dependam rigorosamente do tempo e do preço exatos para a abertura de posições. No entanto, nosso trabalho é direcionado para o uso de outras estratégias, para as quais uma precisão extrema nas entradas não é necessária. Assim, eventuais atrasos no canal de comunicação não deverão representar um obstáculo significativo para a implementação dessa abordagem.
Por ora, não nos adiantaremos demais. Continuaremos avançando de forma metódica na direção escolhida nas próximas partes deste artigo.
Obrigado pela atenção e até breve!
Conteúdo do arquivo
# | Nome | Versão | Descrição | Últimas alterações | |
---|---|---|---|---|---|
MQL5/Experts/Article.15330 | |||||
1 | Advisor.mqh | 1.04 | Classe base do EA | Parte 10 | |
2 | Database.mqh | 1.03 | Classe para trabalho com banco de dados | Parte 13 | |
3 | ExpertHistory.mqh | 1.00 | Classe para exportar o histórico de operações para um arquivo | Parte 16 | |
4 | Factorable.mqh | 1.01 | Classe base para objetos criados a partir de uma string | Parte 10 | |
5 | HistoryReceiverExpert.mq5 | 1.00 | EA para reprodução de histórico de operações com gerenciamento de risco | Parte 16 | |
6 | HistoryStrategy.mqh | 1.00 | Classe de estratégia de negociação para reprodução de histórico de operações | Parte 16 | |
7 | Interface.mqh | 1.00 | Classe base para visualização de diversos objetos | Parte 4 | |
8 | Macros.mqh | 1.02 | Macros úteis para operações com arrays | Parte 16 | |
9 | Money.mqh | 1.01 | Classe base para gerenciamento de capital | Parte 12 | |
10 | NewBarEvent.mqh | 1.00 | Classe para identificar novos candles para um símbolo específico | Parte 8 | |
11 | Receiver.mqh | 1.04 | Classe base para conversão de volumes abertos em posições de mercado | Parte 12 | |
12 | SimpleHistoryReceiverExpert.mq5 | 1.00 | EA simplificado para reprodução de histórico de operações | Parte 16 | |
13 | SimpleVolumesExpert.mq5 | 1.19 | EA para operação paralela de vários grupos de estratégias modelo. Os parâmetros devem ser carregados de um banco de dados de otimização. | Parte 16 | |
14 | SimpleVolumesStrategy.mqh | 1.09 | Classe de estratégia de negociação usando volumes tick | Parte 15 | |
15 | Strategy.mqh | 1.04 | Classe base para estratégia de negociação | Parte 10 | |
16 | TesterHandler.mqh | 1.02 | Classe para processamento de eventos de otimização | Parte 13 | |
17 | VirtualAdvisor.mqh | 1.06 | Classe de EA que trabalha com posições (ordens) virtuais | Parte 15 | |
18 | VirtualChartOrder.mqh | 1.00 | Classe de posição virtual gráfica | Parte 4 | |
19 | VirtualFactory.mqh | 1.04 | Classe de fábrica de objetos | Parte 16 | |
20 | VirtualHistoryAdvisor.mqh | 1.00 | Classe de EA para reprodução de histórico de operações | Parte 16 | |
21 | VirtualInterface.mqh | 1.00 | Classe de interface gráfica do EA | Parte 4 | |
22 | VirtualOrder.mqh | 1.04 | Classe de ordens e posições virtuais | Parte 8 | |
23 | VirtualReceiver.mqh | 1.03 | Classe para conversão de volumes abertos em posições de mercado (receptor) | Parte 12 | |
24 | VirtualRiskManager.mqh | 1.02 | Classe de gerenciamento de risco (gerenciador de risco) | Parte 15 | |
25 | VirtualStrategy.mqh | 1.05 | Classe de estratégia de negociação com posições virtuais | Parte 15 | |
26 | VirtualStrategyGroup.mqh | 1.00 | Classe de grupo de estratégias de negociação ou grupos de estratégias de negociação | Parte 11 | |
27 | VirtualSymbolReceiver.mqh | 1.00 | Classe de receptor simbólico | Parte 3 | |
MQL5/Files | |||||
1 | SimpleVolumesExpert.1.19 [2021.01.01 - 2022.12.30] [10000, 34518, 1294, 3.75].history.csv | Histórico de operações do EA SimpleVolumesExpert.mq5, gerado após exportação. Pode ser utilizado para reprodução no testador de operações com os EAs SimpleHistoryReceiverExpert.mq5 ou HistoryReceiverExpert.mq5. |
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/15330
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.





- 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
Recebi este erro quando tentei executar um backtest no EA:
2025.02.04 01:11:13.690 Core 01 2021.01.01 00:00:00 erro de banco de dados, não existe tal tabela: passes
2025.02.04 01:11:13.690 Core 01 tester stopped because OnInit returns non-zero code 1
Alguma ajuda, por favor?
Recebi este erro quando tentei executar um backtest no EA
Provavelmente, o motivo é que você não criou um banco de dados e não executou as duas primeiras etapas de otimização, que preencherão o banco de dados com informações sobre as passagens executadas(parte 9, parte 11, parte 13). Infelizmente, no momento em que este artigo foi escrito, ainda não havia uma ferramenta simples para criar um banco de dados, criar um projeto de otimização e exportar seus resultados para o EA final. Revisitamos essa questão na parte 21, mas não terminamos de abordá-la. Ela continuará nas partes 22 e 23 (ainda não prontas para publicação).