English Русский 中文 Español Deutsch 日本語
preview
Avaliação visual e ajuste da negociação no MetaTrader 5

Avaliação visual e ajuste da negociação no MetaTrader 5

MetaTrader 5Exemplos |
216 16
Artyom Trishkin
Artyom Trishkin

Conteúdo



Introdução

Imagine a seguinte situação: em uma conta específica, há uma negociação relativamente ativa sendo realizada há bastante tempo, com diferentes instrumentos, por vários EAs e até mesmo manualmente. E agora, passado algum tempo, queremos ver os resultados desse trabalho. Claro que é possível conferir os relatórios padrão de negociação no terminal pressionando Alt+E. Também dá para carregar os ícones das negociações no gráfico e visualizar as aberturas e fechamentos das posições. Mas e se quisermos ver de forma dinâmica como foi realizada a negociação, onde e como as posições foram abertas e fechadas? Ver separadamente por cada símbolo, ou todos de uma vez, os momentos de abertura e fechamento das posições, em quais níveis os stop orders foram colocados e se seu tamanho foi justificado. E se depois nos perguntarmos "o que teria acontecido se..." (e aqui cabem várias opções: outros stops, com algoritmos e critérios diferentes, uso de trailing nas posições, ou mover os stops para o ponto de equilíbrio etc.); e depois ainda testar todos esses "e se" com resultados visíveis e claros. Como a negociação poderia ter mudado se...

Para resolver esse tipo de questão, na verdade, já temos tudo o que precisamos. Basta carregar o histórico da conta em um arquivo (todas as negociações realizadas) e, em seguida, executar um EA no testador de estratégias que leia essas negociações do arquivo e abra/feche posições no testador do terminal cliente. Com um EA assim, podemos adicionar código para alterar as condições de saída das posições e comparar como a negociação teria mudado, e o que teria acontecido se...

E o que isso nos oferece? Mais uma ferramenta para buscar melhores resultados, para fazer ajustes na negociação que já estava sendo feita na conta; o teste visual permite ver em tempo real se as posições foram abertas corretamente em determinado instrumento, se foram fechadas no momento certo, e assim por diante. E o mais importante: podemos simplesmente adicionar um novo algoritmo no código do EA, testar, obter um resultado e aplicar as correções nos EAs que estão operando nessa conta.

Vamos programar esse comportamento do EA:

  • se o EA for iniciado no gráfico de qualquer instrumento, ele reunirá todo o histórico de negociações da conta atual, salvará todas as negociações em um único arquivo e não fará mais nada a partir daí;
  • se o EA for iniciado no testador, ele lerá o histórico de negociações salvo no arquivo e, durante a execução do teste, repetirá todas as negociações do arquivo, abrindo e fechando posições.

Dessa forma, o EA primeiro prepara o arquivo com o histórico de negociações (ao ser executado no gráfico), e depois executa as negociações do arquivo, repetindo integralmente a negociação da conta (quando rodado no testador de estratégias).

Em seguida, faremos melhorias no EA para que seja possível definir outros valores de StopLoss e TakeProfit para as posições abertas no testador.



Salvando o histórico de negociações em um arquivo

No diretório do terminal \MQL5\Experts\ criaremos uma nova pasta chamadaTradingByHistoryDeals, e dentro dela, um novo arquivo de EA com o nome TradingByHistoryDeals.mq5.

No EA, deve haver a possibilidade de selecionar qual símbolo e qual magic number serão utilizados para o teste. Caso haja vários EAs operando em diferentes símbolos ou magic numbers na conta, será possível escolher nas configurações qual símbolo ou magic number nos interessa — ou então considerar todos de uma vez.

//+------------------------------------------------------------------+
//|                                        TradingByHistoryDeals.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Expert                                                           |
//+------------------------------------------------------------------+
//--- input parameters
input    string   InpTestedSymbol   =  "";      /* The symbol being tested in the tester        */ 
input    long     InpTestedMagic    =  -1;      /* The magic number being tested in the tester  */ 
sinput   bool     InpShowDataInLog  =  false;   /* Show collected data in the log               */ 

Os valores padrão para símbolo e magic number são: string vazia e -1. Com esses valores, o EA não irá filtrar o histórico de negociações nem por símbolo, nem por magic number — todo o histórico de negociação será testado. Uma terceira opção permitirá ao EA exibir (ou não) no log as descrições de todas as negociações salvas no arquivo, possibilitando verificar visualmente se os dados foram salvos corretamente.

Cada negociação é um conjunto completo de diferentes parâmetros, descritos por diversas propriedades da negociação. A forma mais simples de representar isso é armazenar todas as propriedades da negociação em uma estrutura. Para gravar uma grande quantidade de negociações em um arquivo, é necessário usar um array de estruturas. E depois, esse array será salvo em arquivo. A linguagem MQL5 fornece todos os recursos necessários para isso. A lógica para salvar o histórico de negociações em um arquivo será a seguinte:

  • percorrer as negociações históricas em um laço;
  • obter cada negociação e gravar seus dados em uma estrutura;
  • salvar a estrutura da negociação criada no array de negociações;
  • ao final do laço, salvar o array de estruturas preparado no arquivo.

Todos os códigos adicionais (estruturas, classes, enumerações) serão escritos em um arquivo separado. Vamos nomeá-lo com o nome da futura classe do objeto de negociação por símbolo.

Na mesma pasta, criaremos um novo arquivo incluível com o nome SymbolTrade.mqh.

Em seguida, escreveremos as definições de macros para o nome da pasta onde será salvo o arquivo de histórico, o nome do arquivo e o caminho completo para ele, e incluir no arquivo criado todos os arquivos necessários da Biblioteca Padrão:

//+------------------------------------------------------------------+
//|                                                  SymbolTrade.mqh |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#define   DIRECTORY  "TradingByHistoryDeals"
#define   FILE_NAME  "HistoryDealsData.bin"
#define   PATH       DIRECTORY+"\\"+FILE_NAME

#include <Arrays\ArrayObj.mqh>
#include <Trade\Trade.mqh>

Agora vamos escrever a estrutura da negociação:

//+------------------------------------------------------------------+
//|  Deal structure. Used to create a deal history file              |
//+------------------------------------------------------------------+
struct SDeal
  {
   ulong             ticket;                 // Deal ticket
   long              order;                  // Order the deal is based on
   long              pos_id;                 // Position ID
   long              time_msc;               // Time in milliseconds
   datetime          time;                   // Time
   double            volume;                 // Volume
   double            price;                  // Price
   double            profit;                 // Profit
   double            commission;             // Deal commission
   double            swap;                   // Accumulated swap at closing
   double            fee;                    // Payment for the deal is accrued immediately after the deal is completed
   double            sl;                     // Stop Loss level
   double            tp;                     // Take Profit level
   ENUM_DEAL_TYPE    type;                   // Type
   ENUM_DEAL_ENTRY   entry;                  // Position change method
   ENUM_DEAL_REASON  reason;                 // Deal reason or source
   long              magic;                  // EA ID
   int               digits;                 // Symbol digits
   ushort            symbol[16];             // Symbol
   ushort            comment[64];            // Deal comment
   ushort            external_id[256];       // Deal ID in an external trading system (on the exchange)
   
//--- Set string properties
   bool              SetSymbol(const string deal_symbol)          { return(::StringToShortArray(deal_symbol, symbol)==deal_symbol.Length());                }
   bool              SetComment(const string deal_comment)        { return(::StringToShortArray(deal_comment, comment)==deal_comment.Length());             }
   bool              SetExternalID(const string deal_external_id) { return(::StringToShortArray(deal_external_id, external_id)==deal_external_id.Length()); }
                       
//--- Return string properties
   string            Symbol(void)                                 { return(::ShortArrayToString(symbol));                                                   }
   string            Comment(void)                                { return(::ShortArrayToString(comment));                                                  }
   string            ExternalID(void)                             { return(::ShortArrayToString(external_id));                                              }
  };

Como iremos salvar estruturas de negociações em arquivo, e apenas estruturas com tipos simples podem ser gravadas em arquivos (ver FileWriteArray()), todas as variáveis do tipo string precisam ser substituídas por arrays de ushort, além de criarmos métodos para gravar e recuperar as propriedades string da estrutura.

A estrutura criada servirá apenas para salvar o histórico de negociações em arquivo e ler esse histórico do arquivo. No próprio EA, mais adiante, será criada uma lista de objetos onde serão armazenados os objetos da classe de negociação. Para buscar uma negociação específica na lista e ordenar o array, será necessário indicar a propriedade da negociação pela qual a busca será feita. Para isso, a lista de objetos deve estar ordenada conforme a propriedade desejada.

Vamos escrever uma enumeração com todas as propriedades do objeto de negociação que poderão ser utilizadas para buscas:

//--- Deal sorting types
enum ENUM_DEAL_SORT_MODE
  {
   SORT_MODE_DEAL_TICKET = 0,          // Mode of comparing/sorting by a deal ticket
   SORT_MODE_DEAL_ORDER,               // Mode of comparing/sorting by the order a deal is based on
   SORT_MODE_DEAL_TIME,                // Mode of comparing/sorting by a deal time
   SORT_MODE_DEAL_TIME_MSC,            // Mode of comparing/sorting by a deal time in milliseconds
   SORT_MODE_DEAL_TYPE,                // Mode of comparing/sorting by a deal type
   SORT_MODE_DEAL_ENTRY,               // Mode of comparing/sorting by a deal direction
   SORT_MODE_DEAL_MAGIC,               // Mode of comparing/sorting by a deal magic number
   SORT_MODE_DEAL_REASON,              // Mode of comparing/sorting by a deal reason or source
   SORT_MODE_DEAL_POSITION_ID,         // Mode of comparing/sorting by a position ID
   SORT_MODE_DEAL_VOLUME,              // Mode of comparing/sorting by a deal volume
   SORT_MODE_DEAL_PRICE,               // Mode of comparing/sorting by a deal price
   SORT_MODE_DEAL_COMMISSION,          // Mode of comparing/sorting by commission
   SORT_MODE_DEAL_SWAP,                // Mode of comparing/sorting by accumulated swap on close
   SORT_MODE_DEAL_PROFIT,              // Mode of comparing/sorting by a deal financial result
   SORT_MODE_DEAL_FEE,                 // Mode of comparing/sorting by a deal fee
   SORT_MODE_DEAL_SL,                  // Mode of comparing/sorting by Stop Loss level
   SORT_MODE_DEAL_TP,                  // Mode of comparing/sorting by Take Profit level
   SORT_MODE_DEAL_SYMBOL,              // Mode of comparing/sorting by a name of a traded symbol
   SORT_MODE_DEAL_COMMENT,             // Mode of comparing/sorting by a deal comment
   SORT_MODE_DEAL_EXTERNAL_ID,         // Mode of comparing/sorting by a deal ID in an external trading system
   SORT_MODE_DEAL_TICKET_TESTER,       // Mode of comparing/sorting by a deal ticket in the tester
   SORT_MODE_DEAL_POS_ID_TESTER,       // Mode of comparing/sorting by a position ID in the tester
  };

Aqui, além das propriedades padrão da negociação, temos mais duas: o ticket da negociação no testador e o identificador da posição no testador. Isso porque estaremos negociando no testador com base em dados de negociações reais, e as posições abertas no testador — e, consequentemente, as negociações com base nas quais essas posições foram abertas — terão ticket e identificador totalmente diferentes no testador. Para conseguirmos relacionar uma negociação real com a negociação correspondente no testador (e também o identificador), será necessário armazenar o ticket e o identificador da posição no testador nas propriedades do objeto de negociação, e, a partir desses dados salvos, fazer a correspondência entre a negociação no testador e a negociação real do histórico.

Vamos pausar esse arquivo por enquanto e voltar ao arquivo do EA que criamos anteriormente. Adicionaremos um array de estruturas, onde serão inseridas as estruturas de todas as negociações do histórico:

//--- input parameters
input    string   InpTestedSymbol   =  "";      /* The symbol being tested in the tester        */ 
input    long     InpTestedMagic    =  -1;      /* The magic number being tested in the tester  */ 
sinput   bool     InpShowDataInLog  =  false;   /* Show collected data in the log               */ 

//--- global variables
SDeal          ExtArrayDeals[]={};

E escreveremos funções para trabalhar com as negociações históricas.

Função que salva o histórico de negociações em um array:

//+------------------------------------------------------------------+
//| Save deals from history into the array                           |
//+------------------------------------------------------------------+
int SaveDealsToArray(SDeal &array[], bool logs=false)
  {
//--- deal structure
   SDeal deal={};
   
//--- request the deal history in the interval from the very beginning to the current moment 
   if(!HistorySelect(0, TimeCurrent()))
     {
      Print("HistorySelect() failed. Error ", GetLastError());
      return 0;
     }
   
//--- total number of deals in the list 
   int total=HistoryDealsTotal(); 

//--- handle each deal 
   for(int i=0; i<total; i++) 
     { 
      //--- get the ticket of the next deal (the deal is automatically selected to get its properties)
      ulong ticket=HistoryDealGetTicket(i);
      if(ticket==0)
         continue;
      
      //--- save only balance and trading deals
      ENUM_DEAL_TYPE deal_type=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket, DEAL_TYPE);
      if(deal_type!=DEAL_TYPE_BUY && deal_type!=DEAL_TYPE_SELL && deal_type!=DEAL_TYPE_BALANCE)
         continue;
      
      //--- save the deal properties in the structure
      deal.ticket=ticket;
      deal.type=deal_type;
      deal.order=HistoryDealGetInteger(ticket, DEAL_ORDER);
      deal.entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket, DEAL_ENTRY);
      deal.reason=(ENUM_DEAL_REASON)HistoryDealGetInteger(ticket, DEAL_REASON);
      deal.time=(datetime)HistoryDealGetInteger(ticket, DEAL_TIME);
      deal.time_msc=HistoryDealGetInteger(ticket, DEAL_TIME_MSC);
      deal.pos_id=HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
      deal.volume=HistoryDealGetDouble(ticket, DEAL_VOLUME);
      deal.price=HistoryDealGetDouble(ticket, DEAL_PRICE);
      deal.profit=HistoryDealGetDouble(ticket, DEAL_PROFIT);
      deal.commission=HistoryDealGetDouble(ticket, DEAL_COMMISSION);
      deal.swap=HistoryDealGetDouble(ticket, DEAL_SWAP);
      deal.fee=HistoryDealGetDouble(ticket, DEAL_FEE);
      deal.sl=HistoryDealGetDouble(ticket, DEAL_SL);
      deal.tp=HistoryDealGetDouble(ticket, DEAL_TP);
      deal.magic=HistoryDealGetInteger(ticket, DEAL_MAGIC);
      deal.SetSymbol(HistoryDealGetString(ticket, DEAL_SYMBOL));
      deal.SetComment(HistoryDealGetString(ticket, DEAL_COMMENT));
      deal.SetExternalID(HistoryDealGetString(ticket, DEAL_EXTERNAL_ID));
      deal.digits=(int)SymbolInfoInteger(deal.Symbol(), SYMBOL_DIGITS);
      
      //--- increase the array and
      int size=(int)array.Size();
      ResetLastError();
      if(ArrayResize(array, size+1, total)!=size+1)
        {
         Print("ArrayResize() failed. Error ", GetLastError());
         continue;
        }
      //--- save the deal in the array
      array[size]=deal;
      //--- if allowed, display the description of the saved deal to the journal
      if(logs)
         DealPrint(deal, i);
     }
//--- return the number of deals stored in the array
   return (int)array.Size();
  }

O código da função está comentado em detalhes. Selecionamos todo o histórico de negociações desde o início até o momento atual, pegamos cada negociação histórica uma a uma, salvamos suas propriedades nos campos da estrutura e armazenamos a variável da estrutura no array. Ao final do laço sobre o histórico de negociações, a função retorna o tamanho do array de negociações obtido. Para acompanhar o progresso da gravação das negociações no array, podemos imprimir cada negociação processada no log. Para isso, ao chamar a função, basta definir o parâmetro formal logs como true.

Função que imprime no log todas as negociações do array de negociações:

//+------------------------------------------------------------------+
//| Display deals from the array to the journal                      |
//+------------------------------------------------------------------+
void DealsArrayPrint(SDeal &array[])
  {
   int total=(int)array.Size();
//--- if an empty array is passed, report this and return 'false'
   if(total==0)
     {
      PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__);
      return;
     }
//--- In a loop through the deal array, print out a description of each deal
   for(int i=0; i<total; i++)
     {
      DealPrint(array[i], i);
     }
  }

Para exibir a descrição da negociação no log, criaremos algumas funções.

Função que retorna a descrição do tipo de negociação:

//+------------------------------------------------------------------+
//| Return the deal type description                                 |
//+------------------------------------------------------------------+
string DealTypeDescription(const ENUM_DEAL_TYPE type)
  {
   switch(type)
     {
      case DEAL_TYPE_BUY                     :  return "Buy";
      case DEAL_TYPE_SELL                    :  return "Sell";
      case DEAL_TYPE_BALANCE                 :  return "Balance";
      case DEAL_TYPE_CREDIT                  :  return "Credit";
      case DEAL_TYPE_CHARGE                  :  return "Additional charge";
      case DEAL_TYPE_CORRECTION              :  return "Correction";
      case DEAL_TYPE_BONUS                   :  return "Bonus";
      case DEAL_TYPE_COMMISSION              :  return "Additional commission";
      case DEAL_TYPE_COMMISSION_DAILY        :  return "Daily commission";
      case DEAL_TYPE_COMMISSION_MONTHLY      :  return "Monthly commission";
      case DEAL_TYPE_COMMISSION_AGENT_DAILY  :  return "Daily agent commission";
      case DEAL_TYPE_COMMISSION_AGENT_MONTHLY:  return "Monthly agent commission";
      case DEAL_TYPE_INTEREST                :  return "Interest rate";
      case DEAL_TYPE_BUY_CANCELED            :  return "Canceled buy deal";
      case DEAL_TYPE_SELL_CANCELED           :  return "Canceled sell deal";
      case DEAL_DIVIDEND                     :  return "Dividend operations";
      case DEAL_DIVIDEND_FRANKED             :  return "Franked (non-taxable) dividend operations";
      case DEAL_TAX                          :  return "Tax charges";
      default                                :  return "Unknown deal type: "+(string)type;
     }
  }

Dependendo do tipo de negociação passado para a função, será exibida a string correspondente.

Função que retorna a descrição da forma de modificação da posição:

//+------------------------------------------------------------------+
//| Return position change method                                    |
//+------------------------------------------------------------------+
string DealEntryDescription(const ENUM_DEAL_ENTRY entry)
  {
   switch(entry)
     {
      case DEAL_ENTRY_IN      :  return "Entry In";
      case DEAL_ENTRY_OUT     :  return "Entry Out";
      case DEAL_ENTRY_INOUT   :  return "Entry InOut";
      case DEAL_ENTRY_OUT_BY  :  return "Entry OutBy";
      default                 :  return "Unknown entry: "+(string)entry;
     }
  }

Dependendo da forma de modificação da posição passada para a função, será exibida a string correspondente.

Função que retorna a descrição da negociação:

//+------------------------------------------------------------------+
//| Return deal description                                          |
//+------------------------------------------------------------------+
string DealDescription(SDeal &deal, const int index)
  {
   string indexs=StringFormat("% 5d", index);
   if(deal.type!=DEAL_TYPE_BALANCE)
      return(StringFormat("%s: deal #%I64u %s, type %s, Position #%I64d %s (magic %I64d), Price %.*f at %s, sl %.*f, tp %.*f",
                          indexs, deal.ticket, DealEntryDescription(deal.entry), DealTypeDescription(deal.type),
                          deal.pos_id, deal.Symbol(), deal.magic, deal.digits, deal.price,
                          TimeToString(deal.time, TIME_DATE|TIME_MINUTES|TIME_SECONDS), deal.digits, deal.sl, deal.digits, deal.tp));
   else
      return(StringFormat("%s: deal #%I64u %s, type %s %.2f %s at %s",
                          indexs, deal.ticket, DealEntryDescription(deal.entry), DealTypeDescription(deal.type),
                          deal.profit, AccountInfoString(ACCOUNT_CURRENCY), TimeToString(deal.time)));
  }

Se for uma negociação de balanço, a descrição será exibida no formato

    0: deal #190715988 Entry In, type Balance 3000.00 USD at 2024.09.13 21:48

Caso contrário, a descrição da negociação será exibida em outro formato:

    1: deal #190724678 Entry In, type Buy, Position #225824633 USDCHF (magic 600), Price 0.84940 at 2024.09.13 23:49:03, sl 0.84811, tp 0.84983

Função que imprime no log a descrição da negociação:

//+------------------------------------------------------------------+
//| Print deal data in the journal                                   |
//+------------------------------------------------------------------+
void DealPrint(SDeal &deal, const int index)
  {
   Print(DealDescription(deal, index));
  }

Aqui tudo é visual, apenas imprimimos a string obtida da função DealDescription().

Vamos escrever funções para gravar e ler o array de negociações em arquivo / a partir de arquivo.

Função que abre o arquivo para gravação:

//+------------------------------------------------------------------+
//| Open a file for writing, return a handle                         |
//+------------------------------------------------------------------+
bool FileOpenToWrite(int &handle)
  {
   ResetLastError();
   handle=FileOpen(PATH, FILE_WRITE|FILE_BIN|FILE_COMMON);
   if(handle==INVALID_HANDLE)
     {
      PrintFormat("%s: FileOpen() failed. Error %d",__FUNCTION__, GetLastError());
      return false;
     }
//--- successful
   return true;
  }

Função que abre o arquivo para leitura:

//+------------------------------------------------------------------+
//| Open a file for reading, return a handle                         |
//+------------------------------------------------------------------+
bool FileOpenToRead(int &handle)
  {
   ResetLastError();
   handle=FileOpen(PATH, FILE_READ|FILE_BIN|FILE_COMMON);
   if(handle==INVALID_HANDLE)
     {
      PrintFormat("%s: FileOpen() failed. Error %d",__FUNCTION__, GetLastError());
      return false;
     }
//--- successful
   return true;
  }

As funções abrem o arquivo para leitura/gravação. Como parâmetros formais, por referência, é passada uma variável onde será armazenado o handle do arquivo. Retornam true em caso de abertura bem-sucedida do arquivo e false em caso de erro.

Função que salva no arquivo os dados das negociações do array:

//+------------------------------------------------------------------+
//| Save deal data from the array to the file                        |
//+------------------------------------------------------------------+
bool FileWriteDealsFromArray(SDeal &array[], ulong &file_size)
  {
//--- if an empty array is passed, report this and return 'false'
   if(array.Size()==0)
     {
      PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__);
      return false;
     }
     
//--- open the file for writing, get its handle
   int handle=INVALID_HANDLE;
   if(!FileOpenToWrite(handle))
      return false;
   
//--- move the file pointer to the end of the file
   bool res=true;
   ResetLastError();
   res&=FileSeek(handle, 0, SEEK_END);
   if(!res)
      PrintFormat("%s: FileSeek(SEEK_END) failed. Error %d",__FUNCTION__, GetLastError());
   
//--- write the array data to the end of the file 
   file_size=0;
   res&=(FileWriteArray(handle, array)==array.Size());
   if(!res)
      PrintFormat("%s: FileWriteArray() failed. Error ",__FUNCTION__, GetLastError());
   else
      file_size=FileSize(handle);

//--- close the file 
   FileClose(handle);
   return res;
  }

A função recebe um array de estruturas que precisa ser salvo em arquivo. A variável que receberá o tamanho do arquivo criado é passada por referência nos parâmetros formais da função. O arquivo é aberto, o ponteiro do arquivo é movido para o final e os dados do array de estruturas são gravados no arquivo a partir desse ponto. Após o fim da gravação, o arquivo é fechado.

Depois que o array de estruturas de negociações é salvo no arquivo, todas essas negociações podem ser lidas novamente do arquivo para um array, e então ele poderá ser usado para criar listas de negociações e manipulá-las no testador.

Função que carrega os dados das negociações do arquivo para o array:

//+------------------------------------------------------------------+
//| Load the deal data from the file into the array                  |
//+------------------------------------------------------------------+
bool FileReadDealsToArray(SDeal &array[], ulong &file_size)
  {
//--- open the file for reading, get its handle
   int handle=INVALID_HANDLE;
   if(!FileOpenToRead(handle))
      return false;
   
//--- move the file pointer to the end of the file 
   bool res=true;
   ResetLastError();
   
//--- read data from the file into the array
   file_size=0;
   res=(FileReadArray(handle, array)>0);
   if(!res)
      PrintFormat("%s: FileWriteArray() failed. Error ",__FUNCTION__, GetLastError());
   else
      file_size=FileSize(handle);

//--- close the file 
   FileClose(handle);
   return res;
  }

Com base nas funções criadas acima, escreveremos uma função para ler o histórico de negociações e gravá-lo em um arquivo.

Função que prepara o arquivo com as negociações do histórico:

//+------------------------------------------------------------------+
//| Prepare a file with history deals                                |
//+------------------------------------------------------------------+
bool PreparesDealsHistoryFile(SDeal &deals_array[])
  {
//--- save all the account deals in the deal array
   int total=SaveDealsToArray(deals_array);
   if(total==0)
      return false;
      
//--- write the deal array data to the file
   ulong file_size=0;
   if(!FileWriteDealsFromArray(deals_array, file_size))
      return false;
      
//--- print in the journal how many deals were read and saved to the file, the path to the file and its size
   PrintFormat("%u deals were saved in an array and written to a \"%s\" file of %I64u bytes in size",
               deals_array.Size(), "TERMINAL_COMMONDATA_PATH\\Files\\"+ PATH, file_size);
   
//--- now, to perform a check, we will read the data from the file into the array
   ArrayResize(deals_array, 0, total);
   if(!FileReadDealsToArray(deals_array, file_size))
      return false;
      
//--- print in the journal how many bytes were read from the file and the number of deals received in the array
   PrintFormat("%I64u bytes were read from the file \"%s\" and written to the deals array. A total of %u deals were received", file_size, FILE_NAME, deals_array.Size());
   return true;
  }

Os comentários no código tornam toda a lógica aqui compreensível. A função é executada no manipulador OnInit() e prepara o arquivo com as negociações para uso posterior:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- If the EA is not running in the tester
   if(!MQLInfoInteger(MQL_TESTER))
     {
      //--- prepare a file with all historical deals
      if(!PreparesDealsHistoryFile(ExtArrayDeals))
         return(INIT_FAILED);
         
      //--- print all deals in the journal after loading them from the file 
      if(InpShowDataInLog)
         DealsArrayPrint(ExtArrayDeals);
         
      //--- get the first balance deal, create the message text and display it using Alert
      SDeal    deal=ExtArrayDeals[0];
      long     leverage=AccountInfoInteger(ACCOUNT_LEVERAGE);
      double   start_money=deal.profit;
      datetime first_time=deal.time;
      string   start_time=TimeToString(deal.time, TIME_DATE);
      string   message=StringFormat("Now you can run testing\nInterval: %s - current date\nInitial deposit: %.2f, leverage 1:%I64u", start_time, start_money, leverage);
      
      //--- notify via alert of the recommended parameters of the strategy tester for starting the test
      Alert(message);
     }
     
//--- All is successful
   return(INIT_SUCCEEDED);
  }

Além de salvar todas as negociações históricas no arquivo, também é exibido um alerta com uma mensagem sobre as configurações recomendadas do testador: saldo inicial, alavancagem e data de início do teste correspondente à data da primeira negociação de balanço. Algo assim:

Alert: Now you can run testing
Interval: 2024.09.13 - current date
Initial deposit: 3000.00, leverage 1:500

Essas configurações do testador resultarão em um resultado final no testador mais próximo possível daquele obtido na conta real.

A estrutura de negociação, escrita no arquivo \MQL5\Experts\TradingByHistoryDeals\SymbolTrade.mqh, serve apenas para salvar o histórico de negociações em arquivo e para ler esse histórico do arquivo. Para o próximo passo, precisamos criar uma classe de negociação, cujos objetos serão armazenados em listas. E essas listas, por sua vez, estarão nos objetos da classe de negociação usada no testador. Esses objetos de negociação também serão instâncias de uma classe própria, que também serão armazenadas em uma lista. Cada objeto de negociação será identificado por pertencer a um determinado símbolo — ou seja, para cada símbolo envolvido na negociação, haverá um objeto de negociação correspondente. Cada objeto conterá apenas a lista de negociações referentes ao seu símbolo, além de suas próprias instâncias da classe CTrade da Biblioteca Padrão. Isso permitirá configurar cada instância de CTrade de forma individual, de acordo com as condições específicas do símbolo negociado.

Vamos escrever a classe de negociação no arquivo \MQL5\Experts\TradingByHistoryDeals\SymbolTrade.mqh.

//+------------------------------------------------------------------+
//| Deal class. Used for trading in the strategy tester              |
//+------------------------------------------------------------------+
class CDeal : public CObject
  {
protected:
//--- Integer properties
   ulong             m_ticket;            // Deal ticket. Unique number assigned to each deal
   long              m_order;             // Deal order number
   datetime          m_time;              // Deal execution time
   long              m_time_msc;          // Deal execution time in milliseconds since 01.01.1970
   ENUM_DEAL_TYPE    m_type;              // Deal type
   ENUM_DEAL_ENTRY   m_entry;             // Deal entry - entry in, entry out, reverse
   long              m_magic;             // Magic number for a deal (see ORDER_MAGIC)
   ENUM_DEAL_REASON  m_reason;            // Deal execution reason or source
   long              m_pos_id;            // The ID of the position opened, modified or closed by the deal
   
//--- Real properties
   double            m_volume;            // Deal volume
   double            m_price;             // Deal price
   double            m_commission;        // Deal commission
   double            m_swap;              // Accumulated swap when closing
   double            m_profit;            // Deal financial result
   double            m_fee;               // Fee for making a deal charged immediately after performing a deal
   double            m_sl;                // Stop Loss level
   double            m_tp;                // Take Profit level

//--- String properties
   string            m_symbol;            // Name of the symbol for which the deal is executed
   string            m_comment;           // Deal comment
   string            m_external_id;       // Deal ID in an external trading system (on the exchange)
   
//--- Additional properties
   int               m_digits;            // Symbol digits
   double            m_point;             // Symbol point
   ulong             m_ticket_tester;     // Position ticket in the tester
   long              m_pos_id_tester;     // Position ID in the tester
   
public:
//--- Set deal propertie
   void              SetTicket(const ulong ticket)             { this.m_ticket=ticket;          }
   void              SetOrder(const long order)                { this.m_order=order;            }
   void              SetTime(const datetime time)              { this.m_time=time;              }
   void              SetTimeMsc(const long value)              { this.m_time_msc=value;         }
   void              SetType(const ENUM_DEAL_TYPE type)        { this.m_type=type;              }
   void              SetEntry(const ENUM_DEAL_ENTRY entry)     { this.m_entry=entry;            }
   void              SetMagic(const long magic)                { this.m_magic=magic;            }
   void              SetReason(const ENUM_DEAL_REASON reason)  { this.m_reason=reason;          }
   void              SetPositionID(const long id)              { this.m_pos_id=id;              }
   void              SetVolume(const double volume)            { this.m_volume=volume;          }
   void              SetPrice(const double price)              { this.m_price=price;            }
   void              SetCommission(const double commission)    { this.m_commission=commission;  }
   void              SetSwap(const double swap)                { this.m_swap=swap;              }
   void              SetProfit(const double profit)            { this.m_profit=profit;          }
   void              SetFee(const double fee)                  { this.m_fee=fee;                }
   void              SetSL(const double sl)                    { this.m_sl=sl;                  }
   void              SetTP(const double tp)                    { this.m_tp=tp;                  }
   void              SetSymbol(const string symbol)            { this.m_symbol=symbol;          }
   void              SetComment(const string comment)          { this.m_comment=comment;        }
   void              SetExternalID(const string ext_id)        { this.m_external_id=ext_id;     }
   void              SetTicketTester(const ulong ticket)       { this.m_ticket_tester=ticket;   }
   void              SetPosIDTester(const long pos_id)         { this.m_pos_id_tester=pos_id;   }
   
//--- Return deal properties
   ulong             Ticket(void)                        const { return this.m_ticket;          }
   long              Order(void)                         const { return this.m_order;           }
   datetime          Time(void)                          const { return this.m_time;            }
   long              TimeMsc(void)                       const { return this.m_time_msc;        }
   ENUM_DEAL_TYPE    TypeDeal(void)                      const { return this.m_type;            }
   ENUM_DEAL_ENTRY   Entry(void)                         const { return this.m_entry;           }
   long              Magic(void)                         const { return this.m_magic;           }
   ENUM_DEAL_REASON  Reason(void)                        const { return this.m_reason;          }
   long              PositionID(void)                    const { return this.m_pos_id;          }
   double            Volume(void)                        const { return this.m_volume;          }
   double            Price(void)                         const { return this.m_price;           }
   double            Commission(void)                    const { return this.m_commission;      }
   double            Swap(void)                          const { return this.m_swap;            }
   double            Profit(void)                        const { return this.m_profit;          }
   double            Fee(void)                           const { return this.m_fee;             }
   double            SL(void)                            const { return this.m_sl;              }
   double            TP(void)                            const { return this.m_tp;              }
   string            Symbol(void)                        const { return this.m_symbol;          }
   string            Comment(void)                       const { return this.m_comment;         }
   string            ExternalID(void)                    const { return this.m_external_id;     }

   int               Digits(void)                        const { return this.m_digits;          }
   double            Point(void)                         const { return this.m_point;           }
   ulong             TicketTester(void)                  const { return this.m_ticket_tester;   }
   long              PosIDTester(void)                   const { return this.m_pos_id_tester;   }
   
//--- Compare two objects by the property specified in 'mode'
   virtual int       Compare(const CObject *node, const int mode=0) const
                       {
                        const CDeal *obj=node;
                        switch(mode)
                          {
                           case SORT_MODE_DEAL_TICKET          :  return(this.Ticket() > obj.Ticket()          ?  1  :  this.Ticket() < obj.Ticket()           ? -1  :  0);
                           case SORT_MODE_DEAL_ORDER           :  return(this.Order() > obj.Order()            ?  1  :  this.Order() < obj.Order()             ? -1  :  0);
                           case SORT_MODE_DEAL_TIME            :  return(this.Time() > obj.Time()              ?  1  :  this.Time() < obj.Time()               ? -1  :  0);
                           case SORT_MODE_DEAL_TIME_MSC        :  return(this.TimeMsc() > obj.TimeMsc()        ?  1  :  this.TimeMsc() < obj.TimeMsc()         ? -1  :  0);
                           case SORT_MODE_DEAL_TYPE            :  return(this.TypeDeal() > obj.TypeDeal()      ?  1  :  this.TypeDeal() < obj.TypeDeal()       ? -1  :  0);
                           case SORT_MODE_DEAL_ENTRY           :  return(this.Entry() > obj.Entry()            ?  1  :  this.Entry() < obj.Entry()             ? -1  :  0);
                           case SORT_MODE_DEAL_MAGIC           :  return(this.Magic() > obj.Magic()            ?  1  :  this.Magic() < obj.Magic()             ? -1  :  0);
                           case SORT_MODE_DEAL_REASON          :  return(this.Reason() > obj.Reason()          ?  1  :  this.Reason() < obj.Reason()           ? -1  :  0);
                           case SORT_MODE_DEAL_POSITION_ID     :  return(this.PositionID() > obj.PositionID()  ?  1  :  this.PositionID() < obj.PositionID()   ? -1  :  0);
                           case SORT_MODE_DEAL_VOLUME          :  return(this.Volume() > obj.Volume()          ?  1  :  this.Volume() < obj.Volume()           ? -1  :  0);
                           case SORT_MODE_DEAL_PRICE           :  return(this.Price() > obj.Price()            ?  1  :  this.Price() < obj.Price()             ? -1  :  0);
                           case SORT_MODE_DEAL_COMMISSION      :  return(this.Commission() > obj.Commission()  ?  1  :  this.Commission() < obj.Commission()   ? -1  :  0);
                           case SORT_MODE_DEAL_SWAP            :  return(this.Swap() > obj.Swap()              ?  1  :  this.Swap() < obj.Swap()               ? -1  :  0);
                           case SORT_MODE_DEAL_PROFIT          :  return(this.Profit() > obj.Profit()          ?  1  :  this.Profit() < obj.Profit()           ? -1  :  0);
                           case SORT_MODE_DEAL_FEE             :  return(this.Fee() > obj.Fee()                ?  1  :  this.Fee() < obj.Fee()                 ? -1  :  0);
                           case SORT_MODE_DEAL_SL              :  return(this.SL() > obj.SL()                  ?  1  :  this.SL() < obj.SL()                   ? -1  :  0);
                           case SORT_MODE_DEAL_TP              :  return(this.TP() > obj.TP()                  ?  1  :  this.TP() < obj.TP()                   ? -1  :  0);
                           case SORT_MODE_DEAL_SYMBOL          :  return(this.Symbol() > obj.Symbol()          ?  1  :  this.Symbol() < obj.Symbol()           ? -1  :  0);
                           case SORT_MODE_DEAL_COMMENT         :  return(this.Comment() > obj.Comment()        ?  1  :  this.Comment() < obj.Comment()         ? -1  :  0);
                           case SORT_MODE_DEAL_EXTERNAL_ID     :  return(this.ExternalID()  >obj.ExternalID()  ?  1  :  this.ExternalID()  <obj.ExternalID()   ? -1  :  0);
                           case SORT_MODE_DEAL_TICKET_TESTER   :  return(this.TicketTester()>obj.TicketTester()?  1  :  this.TicketTester()<obj.TicketTester() ? -1  :  0);
                           case SORT_MODE_DEAL_POS_ID_TESTER   :  return(this.PosIDTester() >obj.PosIDTester() ?  1  :  this.PosIDTester() <obj.PosIDTester()  ? -1  :  0);
                           default                             :  return(WRONG_VALUE);
                          }
                       }
   
//--- Constructors/destructor
                     CDeal(const ulong ticket, const string symbol) : m_ticket(ticket), m_symbol(symbol), m_ticket_tester(0), m_pos_id_tester(0)
                       { this.m_digits=(int)::SymbolInfoInteger(symbol, SYMBOL_DIGITS); this.m_point=::SymbolInfoDouble(symbol, SYMBOL_POINT); }
                     CDeal(void) {}
                    ~CDeal(void) {}
  };

A classe é praticamente idêntica à estrutura de negociação criada anteriormente. Além das propriedades da negociação, foram adicionadas as propriedades Digits e Point do símbolo no qual a negociação foi realizada. Isso facilita a geração da descrição da negociação, pois esses dados são definidos no construtor da negociação logo ao criar o objeto, dispensando a necessidade de buscar essas propriedades para cada negociação individualmente (caso sejam necessárias) sempre que ela for acessada.
Também foi criado aqui um método virtual Compare() para comparar dois objetos de negociação, que será utilizado para ordenar as listas de negociações na busca pela negociação desejada com base em uma propriedade definida.

Agora vamos criar a classe de negociação por símbolo. A classe armazenará uma lista de negociações realizadas para o símbolo especificado nas propriedades do objeto, e será a partir dela que essas negociações serão solicitadas pelo testador para cópia. Em resumo, essa classe servirá como base para copiar no testador as negociações que foram realizadas na conta para o símbolo em questão:

//+------------------------------------------------------------------+
//|  Class for trading by symbol                                     |
//+------------------------------------------------------------------+
CDeal DealTmp; // Temporary deal object for searching by properties

class CSymbolTrade : public CObject
  {
private:
   int               m_index_next_deal;                  // Index of the next deal that has not yet been handled
   int               m_deals_processed;                  // Number of handled deals
protected:
   MqlTick           m_tick;                             // Tick structure
   CArrayObj         m_list_deals;                       // List of deals carried out by symbol
   CTrade            m_trade;                            // Trading class
   string            m_symbol;                           // Symbol name
public:
//--- Return the list of deals
   CArrayObj        *GetListDeals(void)                  { return(&this.m_list_deals);       }
   
//--- Set a symbol
   void              SetSymbol(const string symbol)      { this.m_symbol=symbol;             }
   
//--- (1) Set and (2) returns the number of handled deals
   void              SetNumProcessedDeals(const int num) { this.m_deals_processed=num;       }
   int               NumProcessedDeals(void)       const { return this.m_deals_processed;    }
   
//--- Add a deal to the deal array
   bool              AddDeal(CDeal *deal);
   
//--- Return the deal (1) by time in seconds, (2) by index in the list,
//--- (3) opening deal by position ID, (4) current deal in the list
   CDeal            *GetDealByTime(const datetime time);
   CDeal            *GetDealByIndex(const int index);
   CDeal            *GetDealInByPosID(const long pos_id);
   CDeal            *GetDealCurrent(void);
   
//--- Return (1) the number of deals in the list, (2) the index of the current deal in the list
   int               DealsTotal(void)              const { return this.m_list_deals.Total(); }
   int               DealCurrentIndex(void)        const { return this.m_index_next_deal;    }
   
//--- Return (1) symbol and (2) object description
   string            Symbol(void)                  const { return this.m_symbol;             }
   string            Description(void) const
                       {
                        return ::StringFormat("%s trade object. Total deals: %d", this.Symbol(), this.DealsTotal() );
                       }

//--- Return the current (1) Bid and (2) Ask price, time in (3) seconds, (4) milliseconds
   double            Bid(void);
   double            Ask(void);
   datetime          Time(void);
   long              TimeMsc(void);
   
//--- Open (1) long, (2) short position, (3) close a position by ticket
   ulong             Buy(const double volume, const ulong magic, const double sl, const double tp, const string comment);
   ulong             Sell(const double volume, const ulong magic, const double sl, const double tp, const string comment);
   bool              ClosePos(const ulong ticket);

//--- Return the result of comparing the current time with the specified one
   bool              CheckTime(const datetime time)      { return(this.Time()>=time);        }
//--- Sets the index of the next deal
   void              SetNextDealIndex(void)              { this.m_index_next_deal++;         }
   
//--- OnTester handler. Returns the number of deals processed by the tester.
   double            OnTester(void)
                       {
                        ::PrintFormat("Symbol %s: Total deals: %d, number of processed deals: %d", this.Symbol(), this.DealsTotal(), this.NumProcessedDeals());
                        return this.m_deals_processed;
                       }

//--- Compares two objects to each other (comparison by symbol only)
   virtual int       Compare(const CObject *node, const int mode=0) const
                       {
                        const CSymbolTrade *obj=node;
                        return(this.Symbol()>obj.Symbol() ? 1 : this.Symbol()<obj.Symbol() ? -1 : 0);
                       }
//--- Constructors/destructor
                     CSymbolTrade(void) : m_index_next_deal(0), m_deals_processed(0) {}
                     CSymbolTrade(const string symbol) : m_symbol(symbol), m_index_next_deal(0), m_deals_processed(0)
                       {
                        this.m_trade.SetMarginMode();
                        this.m_trade.SetTypeFillingBySymbol(this.m_symbol);
                       }
                    ~CSymbolTrade(void) {}
  };

Vamos analisar alguns métodos.

  • SetNumProcessedDeals() e NumProcessedDeals() definem e retornam a quantidade de negociações históricas já processadas pelo testador a partir da lista de negociações carregada do arquivo. São necessárias para controlar a correta manipulação das negociações históricas e obter a estatística final da quantidade de negociações processadas pelo testador;
  • GetDealCurrent() retorna o ponteiro para a negociação histórica atual, que deve ser processada pelo testador e marcada como processada;
  • DealCurrentIndex() retorna o índice da negociação histórica selecionada para ser processada pelo testador no momento atual;
  • SetNextDealIndex(), após o término da análise da negociação histórica atual, define o índice da próxima negociação a ser processada pelo testador. Como todas as negociações históricas na lista estão ordenadas por tempo em milissegundos, isso define o índice da negociação seguinte após o processamento da anterior pelo testador. Assim, percorremos sequencialmente todas as negociações do histórico, que serão processadas pelo testador no instante em que o tempo registrado nas propriedades da negociação atual for atingido;
  • CheckTime() verifica o momento em que o tempo do testador alcança o valor registrado nas propriedades da negociação histórica atual. A lógica é a seguinte: há uma negociação selecionada que precisa ser processada no testador. Enquanto o tempo no testador for menor que o da negociação, não fazemos nada, apenas avançamos para o próximo tick. Assim que o tempo no testador for igual ou maior que o tempo da negociação atual (o tempo pode não coincidir exatamente, por isso também se verifica "maior que"), a negociação é processada no testador de acordo com seu tipo e forma de modificação da posição. Em seguida, essa negociação é marcada como processada, o índice da próxima negociação é definido, e o processo de espera pelo tempo, controlado por esse método, continua, agora para a próxima negociação:
  • O manipulador OnTester() é chamado a partir do manipulador padrão OnTester() do EA, imprime no log o nome do símbolo, a quantidade de negociações históricas e as processadas pelo testador, e retorna a quantidade de negociações processadas para o símbolo do objeto de negociação;

A classe possui dois construtores: o padrão e um parametrizado.

No construtor parametrizado, é passado como parâmetro o nome do símbolo do objeto de negociação, que é definido ao criar o objeto, e no objeto da classe CTrade são configurados o modo de cálculo de margem,de acordo com as configurações da conta atual, e o tipo de ordem por execução,conforme as configurações do símbolo do objeto de negociação:

//--- Constructors/destructor
                     CSymbolTrade(void) : m_index_next_deal(0), m_deals_processed(0) {}
                     CSymbolTrade(const string symbol) : m_symbol(symbol), m_index_next_deal(0), m_deals_processed(0)
                       {
                        this.m_trade.SetMarginMode();
                        this.m_trade.SetTypeFillingBySymbol(this.m_symbol);
                       }

Método que adiciona uma negociação ao array de negociações:

//+------------------------------------------------------------------+
//| CSymbolTrade::Add a trade to the trades array                    |
//+------------------------------------------------------------------+
bool CSymbolTrade::AddDeal(CDeal *deal)
  {
//--- If the list already contains a deal with the deal ticket passed to the method, return 'true'
   this.m_list_deals.Sort(SORT_MODE_DEAL_TICKET);
   if(this.m_list_deals.Search(deal)>WRONG_VALUE)
      return true;
   
//--- Add a pointer to the deal to the list in sorting order by time in milliseconds
   this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC);
   if(!this.m_list_deals.InsertSort(deal))
     {
      ::PrintFormat("%s: Failed to add deal", __FUNCTION__);
      return false;
     }
//--- All is successful
   return true;
  }

O método recebe um ponteiro para o objeto da negociação. Se uma negociação com o mesmo ticket já estiver presente na lista, apenas retorna true. Caso contrário, a lista é ordenada pelo tempo das negociações em milissegundos e a nova negociação é adicionada à lista em ordem cronológica.

Método que retorna o ponteiro para o objeto da negociação com base no tempo em segundos:

//+------------------------------------------------------------------+
//| CSymbolTrade::Return the deal object by time in seconds          |
//+------------------------------------------------------------------+
CDeal* CSymbolTrade::GetDealByTime(const datetime time)
  {
   DealTmp.SetTime(time);
   this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC);
   int index=this.m_list_deals.Search(&DealTmp);
   return this.m_list_deals.At(index);
  }

O método recebe o tempo desejado. Define esse tempo em um objeto de negociação temporário, ordena a lista pelo tempo em milissegundos e busca o índice da negociação cujo tempo coincide com o fornecido (o tempo configurado no objeto temporário). Em seguida, retorna o ponteiro para a negociação na lista com base no índice encontrado. Se não houver negociação com esse tempo na lista, o índice será -1 e o método retornará NULL.

Curiosamente, a negociação é buscada pelo tempo em segundos, mas a lista é ordenada pelo tempo em milissegundos. Os testes mostraram que, se a lista também for ordenada por segundos, algumas negociações não são encontradas, apesar de estarem presentes. Isso provavelmente ocorre porque há várias negociações em um mesmo segundo, com tempos diferentes em milissegundos. E o ponteiro retornado pode apontar para uma negociação já processada anteriormente, já que várias têm o mesmo tempo em segundos.

Método que retorna o ponteiro para a negociação de abertura com base no identificador da posição:

//+------------------------------------------------------------------+
//|CSymbolTrade::Return the opening trade by position ID             |
//+------------------------------------------------------------------+
CDeal *CSymbolTrade::GetDealInByPosID(const long pos_id)
  {
   int total=this.m_list_deals.Total();
   for(int i=0; i<total; i++)
     {
      CDeal *deal=this.m_list_deals.At(i);
      if(deal==NULL || deal.PositionID()!=pos_id)
         continue;
      if(deal.Entry()==DEAL_ENTRY_IN)
         return deal;
     }
   return NULL;
  }

O método recebe o identificador da posição cuja negociação de abertura deve ser localizada. Em seguida, percorremos a lista de negociações e obtemos a negociação cujo identificador de posição seja igual ao passado para o método, e retornamos o ponteiro para a negociação cujo modo de modificação da posição seja "Entrada no mercado" (DEAL_ENTRY_IN).

Método que retorna o ponteiro para o objeto da negociação pelo índice na lista:

//+------------------------------------------------------------------+
//| CSymbolTrade::Return the deal object by index in the list        |
//+------------------------------------------------------------------+
CDeal *CSymbolTrade::GetDealByIndex(const int index)
  {
   return this.m_list_deals.At(index);
  }

Apenas retornamos o ponteiro para o objeto na lista conforme o índice passado ao método. Se o índice for inválido, será retornado NULL.

Método que retorna o ponteiro para a negociação indicada pelo índice da negociação atual:

//+------------------------------------------------------------------+
//| Return the deal pointed to by the current deal index             |
//+------------------------------------------------------------------+
CDeal *CSymbolTrade::GetDealCurrent(void)
  {
   this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC);
   return this.GetDealByIndex(this.m_index_next_deal);
  }

A lista de negociações é ordenada por tempo em milissegundos, e é retornado o ponteiro para a negociação cujo índice está armazenado na variável de classe m_index_next_deal.

Método que retorna o preço atual de Bid:

//+------------------------------------------------------------------+
//| CSymbolTrade::Return the current Bid price                       |
//+------------------------------------------------------------------+
double CSymbolTrade::Bid(void)
  {
   ::ResetLastError();
   if(!::SymbolInfoTick(this.m_symbol, this.m_tick))
     {
      ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError());
      return 0;
     }
   return this.m_tick.bid;
  }

Obtém os dados do último tick na estrutura de preços m_tick e retorna o preço Bid a partir dela.

Método que retorna o preço atual de Ask:

//+------------------------------------------------------------------+
//| CSymbolTrade::Return the current Ask price                       |
//+------------------------------------------------------------------+
double CSymbolTrade::Ask(void)
  {
   ::ResetLastError();
   if(!::SymbolInfoTick(this.m_symbol, this.m_tick))
     {
      ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError());
      return 0;
     }
   return this.m_tick.ask;
  }

Obtém os dados do último tick na estrutura de preços m_tick e retorna o preço Ask a partir dela.

Método que retorna o tempo atual em segundos:

//+------------------------------------------------------------------+
//| CSymbolTrade::Return the current time in seconds                 |
//+------------------------------------------------------------------+
datetime CSymbolTrade::Time(void)
  {
   ::ResetLastError();
   if(!::SymbolInfoTick(this.m_symbol, this.m_tick))
     {
      ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError());
      return 0;
     }
   return this.m_tick.time;
  }

Obtém os dados do último tick na estrutura de preços m_tick e retorna a hora em segundos.

Método que retorna o tempo atual em milissegundos:

//+------------------------------------------------------------------+
//| CSymbolTrade::Return the current time in milliseconds            |
//+------------------------------------------------------------------+
long CSymbolTrade::TimeMsc(void)
  {
   ::ResetLastError();
   if(!::SymbolInfoTick(this.m_symbol, this.m_tick))
     {
      ::PrintFormat("%s: SymbolInfoTick() failed. Error %d",__FUNCTION__, ::GetLastError());
      return 0;
     }
   return this.m_tick.time_msc;
  }

Obtém os dados do último tick na estrutura de preços m_tick e retorna a hora em milissegundos.

Método que abre uma posição de compra:

//+------------------------------------------------------------------+
//| CSymbolTrade::Open a long position                               |
//+------------------------------------------------------------------+
ulong CSymbolTrade::Buy(const double volume, const ulong magic, const double sl, const double tp, const string comment)
  {
   this.m_trade.SetExpertMagicNumber(magic);
   if(!this.m_trade.Buy(volume, this.m_symbol, 0, sl, tp, comment))
     {
      return 0;
     }
   return this.m_trade.ResultOrder();
  }

O método recebe os parâmetros da posição de compra a ser aberta, define o magic number da posição no objeto de negociação e envia uma ordem de compra com os parâmetros indicados. Em caso de erro ao abrir a posição, retorna zero; em caso de sucesso, retorna o ticket da ordem que originou a posição.

Método que abre uma posição de venda:

//+------------------------------------------------------------------+
//| CSymbolTrade::Open a short position                              |
//+------------------------------------------------------------------+
ulong CSymbolTrade::Sell(const double volume, const ulong magic, const double sl, const double tp, const string comment)
  {
   this.m_trade.SetExpertMagicNumber(magic);
   if(!this.m_trade.Sell(volume, this.m_symbol, 0, sl, tp, comment))
     {
      return 0;
     }
   return this.m_trade.ResultOrder();
  }

Semelhante ao método anterior, mas abre uma posição de venda.

Método que fecha a posição com base no ticket:

//+------------------------------------------------------------------+
//| CSymbolTrade::Close position by ticket                           |
//+------------------------------------------------------------------+
bool CSymbolTrade::ClosePos(const ulong ticket)
  {
   return this.m_trade.PositionClose(ticket);
  }

Retorna o resultado da chamada do método PositionClose() do objeto de negociação da classe CTrade.

A classe de negociação por símbolo está pronta. Agora vamos implementá-la no EA para trabalhar com as negociações históricas salvas no arquivo.


Analisando o histórico de negociações a partir de um arquivo no testador

Vamos ao arquivo do EA \MQL5\Experts\TradingByHistoryDeals\TradingByHistoryDeals.mq5 e declaramos um objeto temporário da classe de negociação por símbolo recém-criada, que será necessário para buscar o objeto correto na lista onde serão armazenados os ponteiros para esses objetos:

//+------------------------------------------------------------------+
//| Expert                                                           |
//+------------------------------------------------------------------+
//--- input parameters
input    string   InpTestedSymbol   =  "";      /* The symbol being tested in the tester        */ 
input    long     InpTestedMagic    =  -1;      /* The magic number being tested in the tester  */ 
sinput   bool     InpShowDataInLog  =  false;   /* Show collected data in the log               */ 

//--- global variables
CSymbolTrade   SymbTradeTmp;
SDeal          ExtArrayDeals[]={};
CArrayObj      ExtListSymbols;

Temos um array de negociações históricas, a partir do qual podemos criar uma lista de objetos de negociação, dentro dos quais estarão as listas de negociações pertencentes ao símbolo de cada objeto. No array de negociações estão armazenadas estruturas que descrevem as negociações. Como no objeto de negociação teremos listas de objetos de negociação, precisamos criar uma função que crie um novo objeto de negociação e preencha suas propriedades com os valores dos campos da estrutura que descreve a negociação:

//+------------------------------------------------------------------+
//| Create a deal object from the structure                          |
//+------------------------------------------------------------------+
CDeal *CreateDeal(SDeal &deal_str)
  {
//--- If failed to create an object, inform of the error in the journal and return NULL
   CDeal *deal=new CDeal(deal_str.ticket, deal_str.Symbol());
   if(deal==NULL)
     {
      PrintFormat("%s: Error. Failed to create deal object");
      return NULL;
     }
//--- fill in the deal properties from the structure fields
   deal.SetOrder(deal_str.order);               // Order the deal was based on
   deal.SetPositionID(deal_str.pos_id);         // Position ID
   deal.SetTimeMsc(deal_str.time_msc);          // Time in milliseconds
   deal.SetTime(deal_str.time);                 // Time
   deal.SetVolume(deal_str.volume);             // Volume
   deal.SetPrice(deal_str.price);               // Price
   deal.SetProfit(deal_str.profit);             // Profit
   deal.SetCommission(deal_str.commission);     // Deal commission
   deal.SetSwap(deal_str.swap);                 // Accumulated swap when closing
   deal.SetFee(deal_str.fee);                   // Fee for making a deal charged immediately after performing a deal
   deal.SetSL(deal_str.sl);                     // Stop Loss level
   deal.SetTP(deal_str.tp);                     // Take Profit level
   deal.SetType(deal_str.type);                 // Type
   deal.SetEntry(deal_str.entry);               // Position change method
   deal.SetReason(deal_str.reason);             // Deal execution reason or source
   deal.SetMagic(deal_str.magic);               // EA ID
   deal.SetComment(deal_str.Comment());         // Deal comment
   deal.SetExternalID(deal_str.ExternalID());   // Deal ID in an external trading system (on the exchange)
//--- Return the pointer to a created object
   return deal;
  }

A estrutura da negociação é passada para a função, um novo objeto de negociação é criado e suas propriedades são preenchidas com os valores da estrutura.
A função retorna o ponteiro para o objeto recém-criado. Em caso de erro na criação do objeto, retorna NULL.

Vamos escrever a função que cria a lista de objetos de negociação por símbolo:

//+------------------------------------------------------------------+
//| Create an array of used symbols                                  |
//+------------------------------------------------------------------+
bool CreateListSymbolTrades(SDeal &array_deals[], CArrayObj *list_symbols)
  {
   bool res=true;                      // result
   int total=(int)array_deals.Size();  // total number of deals in the array
   
//--- if the deal array is empty, return 'false'
   if(total==0)
     {
      PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__);
      return false;
     }
   
//--- in a loop through the deal array
   CSymbolTrade *SymbolTrade=NULL;
   for(int i=0; i<total; i++)
     {
      //--- get the next deal and, if it is neither buy nor sell, move on to the next one
      SDeal deal_str=array_deals[i];
      if(deal_str.type!=DEAL_TYPE_BUY && deal_str.type!=DEAL_TYPE_SELL)
         continue;
      
      //--- find a trading object in the list whose symbol is equal to the deal symbol
      string symbol=deal_str.Symbol();
      SymbTradeTmp.SetSymbol(symbol);
      list_symbols.Sort();
      int index=list_symbols.Search(&SymbTradeTmp);
      
      //--- if the index of the desired object in the list is -1, there is no such object in the list
      if(index==WRONG_VALUE)
        {
         //--- we create a new trading symbol object and, if creation fails,
         //--- add 'false' to the result and move on to the next deal
         SymbolTrade=new CSymbolTrade(symbol);
         if(SymbolTrade==NULL)
           {
            res &=false;
            continue;
           }
         //--- if failed to add a symbol trading object to the list,
         //--- delete the newly created object, add 'false' to the result
         //--- and we move on to the next deal
         if(!list_symbols.Add(SymbolTrade))
           {
            delete SymbolTrade;
            res &=false;
            continue;
           }
        }
      //--- otherwise, if the trading object already exists in the list, we get it by index
      else
        {
         SymbolTrade=list_symbols.At(index);
         if(SymbolTrade==NULL)
            continue;
        }
         
      //--- if the current deal is not yet in the list of deals of the symbol trading object
      if(SymbolTrade.GetDealByTime(deal_str.time)==NULL)
        {
         //--- create a deal object according to its sample structure
         CDeal *deal=CreateDeal(deal_str);
         if(deal==NULL)
           {
            res &=false;
            continue;
           }
         //--- add the result of adding the deal object to the list of deals of a symbol trading object to the result value
         res &=SymbolTrade.AddDeal(deal);
        }
     }
//--- return the final result of creating trading objects and adding deals to their lists
   return res;
  }

A lógica da função está explicada detalhadamente nos comentários. Em um loop sobre a lista de negociações históricas, analisamos cada negociação. Verificamos seu símbolo e, se ainda não houver um objeto de negociação para esse símbolo, criamos um novo objeto de negociação e o salvamos na lista. Se já houver, apenas obtemos o ponteiro para o objeto de negociação correspondente ao símbolo da negociação a partir da lista. Depois, verificamos se essa negociação já existe na lista de negociações do objeto de negociação e a adicionamos se ela ainda não estiver presente. Como resultado do loop sobre todas as negociações históricas, obtemos uma lista de objetos de negociação por símbolo, dentro dos quais estão listas contendo as negociações do respectivo símbolo.

A lista de objetos de negociação pode ser exibida no log com a ajuda da função:

//+------------------------------------------------------------------+
//| Display a list of symbol trading objects in the journal          |
//+------------------------------------------------------------------+
void SymbolsArrayPrint(CArrayObj *list_symbols)
  {
   int total=list_symbols.Total();
   if(total==0)
      return;
   Print("Symbols used in trading:");
   for(int i=0; i<total; i++)
     {
      string index=StringFormat("% 3d", i+1);
      CSymbolTrade *obj=list_symbols.At(i);
      if(obj==NULL)
         continue;
      PrintFormat("%s. %s",index, obj.Description());
     }
  }

Em um loop sobre a lista de objetos de negociação por símbolo, obtemos cada objeto e imprimimos sua descrição no log. No log, isso aparece mais ou menos assim:

Symbols used in trading:
  1. AUDUSD trade object. Total deals: 218
  2. EURJPY trade object. Total deals: 116
  3. EURUSD trade object. Total deals: 524
  4. GBPUSD trade object. Total deals: 352
  5. NZDUSD trade object. Total deals: 178
  6. USDCAD trade object. Total deals: 22
  7. USDCHF trade object. Total deals: 250
  8. USDJPY trade object. Total deals: 142
  9. XAUUSD trade object. Total deals: 118

Agora temos um objeto da classe de negociação. Vamos adicionar uma função que retorna a descrição da negociação:

//+------------------------------------------------------------------+
//| Return deal description                                          |
//+------------------------------------------------------------------+
string DealDescription(CDeal *deal, const int index)
  {
   string indexs=StringFormat("% 5d", index);
   if(deal.TypeDeal()!=DEAL_TYPE_BALANCE)
      return(StringFormat("%s: deal #%I64u %s, type %s, Position #%I64d %s (magic %I64d), Price %.*f at %s, sl %.*f, tp %.*f",
                          indexs, deal.Ticket(), DealEntryDescription(deal.Entry()), DealTypeDescription(deal.TypeDeal()),
                          deal.PositionID(), deal.Symbol(), deal.Magic(), deal.Digits(), deal.Price(),
                          TimeToString(deal.Time(), TIME_DATE|TIME_MINUTES|TIME_SECONDS), deal.Digits(), deal.SL(), deal.Digits(), deal.TP()));
   else
      return(StringFormat("%s: deal #%I64u %s, type %s %.2f %s at %s",
                          indexs, deal.Ticket(), DealEntryDescription(deal.Entry()), DealTypeDescription(deal.TypeDeal()),
                          deal.Profit(), AccountInfoString(ACCOUNT_CURRENCY), TimeToString(deal.Time())));
  }

Essa função repete exatamente a lógica da função que retorna a descrição da estrutura de negociação. Mas aqui, em vez da estrutura, é passado um ponteiro para o objeto da negociação.

Agora vamos finalizar logicamente o manipulador OnInit().

Adicionamos o tratamento da inicialização do EA no testador, criamos a lista de objetos de negociação e acessamos cada símbolo utilizado na negociação para carregar seu histórico e abrir os gráficos desses símbolos no testador:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- If the EA is not running in the tester
   if(!MQLInfoInteger(MQL_TESTER))
     {
      //--- prepare a file with all historical deals
      if(!PreparesDealsHistoryFile(ExtArrayDeals))
         return(INIT_FAILED);
         
      //--- print all deals in the journal after loading them from the file 
      if(InpShowDataInLog)
         DealsArrayPrint(ExtArrayDeals);
         
      //--- get the first balance deal, create the message text and display it using Alert
      SDeal    deal=ExtArrayDeals[0];
      long     leverage=AccountInfoInteger(ACCOUNT_LEVERAGE);
      double   start_money=deal.profit;
      datetime first_time=deal.time;
      string   start_time=TimeToString(deal.time, TIME_DATE);
      string   message=StringFormat("Now you can run testing\nInterval: %s - current date\nInitial deposit: %.2f, leverage 1:%I64u", start_time, start_money, leverage);
      
      //--- notify via alert of the recommended parameters of the strategy tester for starting the test
      Alert(message);
     }
//--- The EA has been launched in the tester
   else
     {
      //--- read data from the file into the array
      ulong file_size=0;
      ArrayResize(ExtArrayDeals, 0);
      if(!FileReadDealsToArray(ExtArrayDeals, file_size))
        {
         PrintFormat("Failed to read file \"%s\". Error %d", FILE_NAME, GetLastError());
         return(INIT_FAILED);
        }
         
      //--- report the number of bytes read from the file and writing the deals array in the journal.
      PrintFormat("%I64u bytes were read from the file \"%s\" and written to the deals array. A total of %u deals were received", file_size, FILE_NAME, ExtArrayDeals.Size());
     }
     
//--- Create a list of trading objects by symbols from the array of historical deals
   if(!CreateListSymbolTrades(ExtArrayDeals, &ExtListSymbols))
     {
      Print("Errors found while creating symbol list");
      return(INIT_FAILED);
     }
//--- Print the created list of deals in the journal
   SymbolsArrayPrint(&ExtListSymbols);
   
//--- Access each symbol to start downloading historical data
//--- and opening charts of traded symbols in the strategy tester
   datetime array[];
   int total=ExtListSymbols.Total();

   for(int i=0; i<total; i++)
     {
      CSymbolTrade *obj=ExtListSymbols.At(i);
      if(obj==NULL)
         continue;
      CopyTime(obj.Symbol(), PERIOD_CURRENT, 0, 1, array);
     }
     
//--- All is successful
   return(INIT_SUCCEEDED);
  }

No manipulador OnDeinit() do EA, limpamos os arrays e listas criados:

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- clear the created lists and arrays
   ExtListSymbols.Clear();
   ArrayFree(ExtArrayDeals);
  }

No manipulador OnTick() do EA, no testador, processamos a lista de negociações a partir do arquivo:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- work only in the strategy tester
   if(!MQLInfoInteger(MQL_TESTER))
      return;
      
//---  Handle the list of deals from the file in the tester
   TradeByHistory(InpTestedSymbol, InpTestedMagic);
  }

Vamos analisar essa função com mais detalhes. De modo geral, a lógica para o processamento das negociações do histórico foi inicialmente pensada assim:

  1. obtemos o tempo do tick,
  2. obtemos a negociação correspondente a esse tempo,
  3. processamos a negociação no testador.

À primeira vista, o esquema parece simples e lógico, mas sua implementação resultou em um completo fracasso. A razão é que, no testador, o tempo do tick nem sempre coincide com o tempo da negociação. Mesmo em milissegundos. E, durante testes com todos os ticks — baseados em ticks reais obtidos do mesmo servidor — negociações eram perdidas. Sabíamos exatamente o horário do tick, sabíamos com certeza que havia uma negociação nesse exato momento, mas o testador não a encontrava, pois não havia um tick com aquele horário exato da negociação. Em vez disso, havia ticks com tempo anterior e posterior ao tempo da negociação. Portanto, a lógica não pode se basear nos ticks e seus tempos, mas sim nas próprias negociações:

  1. as negociações estão ordenadas na lista por ordem cronológica em milissegundos. Definimos o índice da primeira negociação como o índice atual;
  2. selecionamos a negociação pelo índice atual e obtemos seu tempo;
  3. aguardamos um tick com esse tempo:
    1. se o tempo do tick for menor que o da negociação — aguardamos o próximo tick,
    2. se o tempo do tick for igual ou maior que o da negociação — processamos a negociação, registramos nela que já foi processada e definimos o índice da próxima negociação como o índice atual;
  4. enquanto o teste não terminar, repetimos a partir do passo 2.

Esse esquema permite aguardar o momento de cada negociação subsequente e executá-la no testador. Nesse processo, não damos atenção ao preço da negociação, apenas copiamos as negociações com base na chegada do seu tempo. Mesmo que o tempo do tick no testador já esteja um pouco à frente do da negociação real, isso não é um problema. O importante é replicar a negociação. O indicativo de que a negociação já foi processada pelo testador será um valor diferente de zero na propriedade "ticket da posição no testador". Se esse valor for zero, isso indica que a negociação ainda não foi processada no testador. Após a execução dessa negociação no testador, o ticket da posição correspondente é gravado nesse campo da negociação.

Vamos escrever uma função com a lógica acima descrita:

//+------------------------------------------------------------------+
//| Trading by history                                               |
//+------------------------------------------------------------------+
void TradeByHistory(const string symbol="", const long magic=-1)
  {
   datetime time=0;
   int total=ExtListSymbols.Total();   // number of trading objects in the list
   
//--- in a loop by all symbol trading objects
   for(int i=0; i<total; i++)
     {
      //--- get another trading object
      CSymbolTrade *obj=ExtListSymbols.At(i);
      if(obj==NULL)
         continue;
      
      //--- get the current deal pointed to by the deal list index
      CDeal *deal=obj.GetDealCurrent();
      if(deal==NULL)
         continue;
      
      //--- sort the deal by magic number and symbol
      if((magic>-1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol))
         continue;
      
      //--- sort the deal by type (only buy/sell deals)
      ENUM_DEAL_TYPE type=deal.TypeDeal();
      if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL)
         continue;
      
      //--- if this is a deal already handled in the tester, move on to the next one
      if(deal.TicketTester()>0)
         continue;
      
      //--- if the deal time has not yet arrived, move to the next trading object of the next symbol
      if(!obj.CheckTime(deal.Time()))
         continue;

      //--- in case of a market entry deal
      ENUM_DEAL_ENTRY entry=deal.Entry();
      if(entry==DEAL_ENTRY_IN)
        {
         //--- open a position by deal type
         double sl=0;
         double tp=0;
         ulong ticket=(type==DEAL_TYPE_BUY  ? obj.Buy(deal.Volume(), deal.Magic(), sl, tp, deal.Comment()) : 
                       type==DEAL_TYPE_SELL ? obj.Sell(deal.Volume(),deal.Magic(), sl, tp, deal.Comment()) : 0);
         
         //--- if a position is opened (we received its ticket)
         if(ticket>0)
           {
            //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object 
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket);
            //--- get the position ID in the tester and write it to the properties of the deal object
            long pos_id_tester=0;
            if(HistoryDealSelect(ticket))
              {
               pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
               deal.SetPosIDTester(pos_id_tester);
              }
           }
        }
      
      //--- in case of a market exit deal
      if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY)
        {
         //--- get a deal a newly opened position is based on
         CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID());
         if(deal_in==NULL)
            continue;

         //--- get the position ticket in the tester from the properties of the opening deal
         //--- if the ticket is zero, then most likely the position in the tester is already closed
         ulong ticket_tester=deal_in.TicketTester();
         if(ticket_tester==0)
           {
            PrintFormat("Could not get position ticket, apparently position #%I64d (#%I64d) is already closed \n", deal.PositionID(), deal_in.PosIDTester());
            obj.SetNextDealIndex();
            continue;
           }
         //--- if the position is closed by ticket
         if(obj.ClosePos(ticket_tester))
           {
            //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object 
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket_tester);
           }
        }
      //--- if a ticket is now set in the deal object, then the deal has been successfully handled -
      //--- set the deal index in the list to the next deal
      if(deal.TicketTester()>0)
        {
         obj.SetNextDealIndex();
        }
     }
  }

Esse código copia fielmente a negociação original que foi realizada na conta e cujas negociações foram salvas no arquivo. Todas as posições são abertas sem ordens de stop. Ou seja, os valores de StopLoss e TakeProfit não são copiados das negociações reais para os métodos de abertura de posições. Isso facilita o rastreamento das negociações, pois as ordens de fechamento também estão presentes na lista, e o testador as executa, independentemente de a posição ter sido encerrada por StopLoss ou por TakeProfit.

Compilamos o EA e o executamos no gráfico. Como resultado, será criado o arquivo HistoryDealsData.bin, na pasta comum dos terminais cliente, no caminho do tipo "C:\Users\UserName\AppData\Roaming\MetaQuotes\Terminal\Common\Files", dentro da subpasta TradingByHistoryDeals, e será exibido no gráfico um alerta com uma mensagem sobre as configurações recomendadas do testador:

Vamos agora executar o EA no testador, escolhendo nas configurações do testador o intervalo de datas especificado, o depósito inicial e a alavancagem:

O teste será executado para todos os símbolos e magic numbers negociados:

Verificamos que toda a negociação resultou em um prejuízo de 550 dólares. Interessante… e se tivéssemos usado outros stop orders?

Vamos verificar.


Vamos ajustar os stop orders

Salvamos o EA na mesma pasta \MQL5\Experts\TradingByHistoryDeals\ com um novo nome TradingByHistoryDeals_SLTP.mq5.

Adicionamos a enumeração dos modos de teste, dividimos os parâmetros de entrada em grupos, acrescentando um grupo para configuração dos parâmetros de stop order, e duas novas variáveis de nível global para transmitir os valores de StopLoss e TakeProfit aos objetos de negociação:

//+------------------------------------------------------------------+
//|                                   TradingByHistoryDeals_SLTP.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#include "SymbolTrade.mqh"

enum ENUM_TESTING_MODE
  {
   TESTING_MODE_ORIGIN,    /* Original trading                          */ 
   TESTING_MODE_SLTP,      /* Specified StopLoss and TakeProfit values  */ 
  };

//+------------------------------------------------------------------+
//| Expert                                                           |
//+------------------------------------------------------------------+
//--- input parameters
input    group             "Strategy parameters"
input    string            InpTestedSymbol   =  "";                  /* The symbol being tested in the tester        */ 
input    long              InpTestedMagic    =  -1;                  /* The magic number being tested in the tester  */ 
sinput   bool              InpShowDataInLog  =  false;               /* Show collected data in the log               */ 

input    group             "Stops parameters"
input    ENUM_TESTING_MODE InpTestingMode    =  TESTING_MODE_ORIGIN; /* Testing Mode                                 */ 
input    int               InpStopLoss       =  300;                 /* StopLoss in points                           */ 
input    int               InpTakeProfit     =  500;                 /* TakeProfit in points                         */ 

//--- global variables
CSymbolTrade   SymbTradeTmp;
SDeal          ExtArrayDeals[]={};
CArrayObj      ExtListSymbols;
int            ExtStopLoss;
int            ExtTakeProfit;


No manipulador OnInit(), ajustamos e gravamos nas variáveis os valores de stop orders definidos pelos parâmetros de entrada:

int OnInit()
  {
//--- Adjust the stops
   ExtStopLoss  =(InpStopLoss<1   ? 0 : InpStopLoss);
   ExtTakeProfit=(InpTakeProfit<1 ? 0 : InpTakeProfit);
   
//--- If the EA is not running in the tester

Adicionamos funções que calculam valores corretos para os preços de StopLoss e TakeProfit com base no nível StopLevel definido para o símbolo:

//+------------------------------------------------------------------+
//| Return correct StopLoss relative to StopLevel                    |
//+------------------------------------------------------------------+
double CorrectStopLoss(const string symbol_name, const ENUM_ORDER_TYPE order_type, const int stop_loss, const int spread_multiplier=2)
  {
   if(stop_loss==0 || (order_type!=ORDER_TYPE_BUY && order_type!=ORDER_TYPE_SELL))
      return 0;
   int lv=StopLevel(symbol_name, spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name, SYMBOL_DIGITS);
   double pt=SymbolInfoDouble(symbol_name, SYMBOL_POINT);
   double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name, SYMBOL_BID) : SymbolInfoDouble(symbol_name, SYMBOL_ASK));
   return
     (order_type==ORDER_TYPE_BUY ?
      NormalizeDouble(fmin(price-lv*pt, price-stop_loss*pt), dg) :
      NormalizeDouble(fmax(price+lv*pt, price+stop_loss*pt), dg)
     );
  }
//+------------------------------------------------------------------+
//| Return correct TakeProfit relative to StopLevel                  |
//+------------------------------------------------------------------+
double CorrectTakeProfit(const string symbol_name, const ENUM_ORDER_TYPE order_type, const int take_profit, const int spread_multiplier=2)
  {
   if(take_profit==0 || (order_type!=ORDER_TYPE_BUY && order_type!=ORDER_TYPE_SELL))
      return 0;
   int lv=StopLevel(symbol_name, spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name, SYMBOL_DIGITS);
   double pt=SymbolInfoDouble(symbol_name, SYMBOL_POINT);
   double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name, SYMBOL_BID) : SymbolInfoDouble(symbol_name, SYMBOL_ASK));
   return
     (order_type==ORDER_TYPE_BUY ?
      NormalizeDouble(fmax(price+lv*pt, price+take_profit*pt), dg) :
      NormalizeDouble(fmin(price-lv*pt, price-take_profit*pt), dg)
     );
  }
//+------------------------------------------------------------------+
//| Return StopLevel in points                                       |
//+------------------------------------------------------------------+
int StopLevel(const string symbol_name, const int spread_multiplier)
  {
   int spread=(int)SymbolInfoInteger(symbol_name, SYMBOL_SPREAD);
   int stop_level=(int)SymbolInfoInteger(symbol_name, SYMBOL_TRADE_STOPS_LEVEL);
   return(stop_level==0 ? spread*spread_multiplier : stop_level);
  }

Para definir os níveis de StopLoss e TakeProfit, o preço do stop order não pode estar a uma distância inferior ao StopLevel em relação ao preço atual. Se o nível StopLevel do símbolo for zero, isso significa que será usado um valor equivalente a duas, às vezes três vezes o tamanho do spread do símbolo. Nessas funções, é usado um multiplicador duplo do spread. Esse valor é passado como parâmetro formal para as funções, e tem valor padrão de 2. Se for necessário alterar o valor do multiplicador, basta informar outro valor ao chamar a função. As funções retornam preços válidos para StopLoss e TakeProfit.

Na função de negociação pelo histórico TradeByHistory(), incluímos novos blocos de código que consideram o modo de negociação no testador e a definição dos valores de StopLoss e TakeProfit caso tenha sido selecionado o teste com esses valores específicos. No bloco de fechamento de posições, as posições devem ser encerradas apenas no modo de teste "negociação original". Se for selecionado o teste com valores definidos de stop order, as negociações de fechamento devem ser ignoradas — o próprio testador encerrará as posições com base nos valores de StopLoss e TakeProfit configurados. A única ação necessária ao processar uma negociação de fechamento com stop order é marcá-la como processada e passar para a próxima negociação.

//+------------------------------------------------------------------+
//| Trading by history                                               |
//+------------------------------------------------------------------+
void TradeByHistory(const string symbol="", const long magic=-1)
  {
   datetime time=0;
   int total=ExtListSymbols.Total();   // number of trading objects in the list
   
//--- in a loop by all symbol trading objects
   for(int i=0; i<total; i++)
     {
      //--- get another trading object
      CSymbolTrade *obj=ExtListSymbols.At(i);
      if(obj==NULL)
         continue;
      
      //--- get the current deal pointed to by the deal list index
      CDeal *deal=obj.GetDealCurrent();
      if(deal==NULL)
         continue;
      
      //--- sort the deal by magic number and symbol
      if((magic>-1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol))
         continue;
      
      //--- sort the deal by type (only buy/sell deals)
      ENUM_DEAL_TYPE type=deal.TypeDeal();
      if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL)
         continue;
      
      //--- if this is a deal already handled in the tester, move on to the next one
      if(deal.TicketTester()>0)
         continue;
      
      //--- if the deal time has not yet arrived, move to the next trading object of the next symbol
      if(!obj.CheckTime(deal.Time()))
         continue;

      //--- in case of a market entry deal
      ENUM_DEAL_ENTRY entry=deal.Entry();
      if(entry==DEAL_ENTRY_IN)
        {
         //--- set the sizes of stop orders depending on the stop setting method
         double sl=0;
         double tp=0;
         if(InpTestingMode==TESTING_MODE_SLTP)
           {
            ENUM_ORDER_TYPE order_type=(deal.TypeDeal()==DEAL_TYPE_BUY ? ORDER_TYPE_BUY : ORDER_TYPE_SELL);
            sl=CorrectStopLoss(deal.Symbol(), order_type, ExtStopLoss);
            tp=CorrectTakeProfit(deal.Symbol(), order_type, ExtTakeProfit);
           }
         //--- open a position by deal type
         ulong ticket=(type==DEAL_TYPE_BUY  ? obj.Buy(deal.Volume(), deal.Magic(), sl, tp, deal.Comment()) : 
                       type==DEAL_TYPE_SELL ? obj.Sell(deal.Volume(),deal.Magic(), sl, tp, deal.Comment()) : 0);
         
         //--- if a position is opened (we received its ticket)
         if(ticket>0)
           {
            //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object 
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket);
            //--- get the position ID in the tester and write it to the properties of the deal object
            long pos_id_tester=0;
            if(HistoryDealSelect(ticket))
              {
               pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
               deal.SetPosIDTester(pos_id_tester);
              }
           }
        }
      
      //--- in case of a market exit deal
      if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY)
        {
         //--- get a deal a newly opened position is based on
         CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID());
         if(deal_in==NULL)
            continue;

         //--- get the position ticket in the tester from the properties of the opening deal
         //--- if the ticket is zero, then most likely the position in the tester is already closed
         ulong ticket_tester=deal_in.TicketTester();
         if(ticket_tester==0)
           {
            PrintFormat("Could not get position ticket, apparently position #%I64d (#%I64d) is already closed \n", deal.PositionID(), deal_in.PosIDTester());
            obj.SetNextDealIndex();
            continue;
           }
         //--- if we reproduce the original trading history in the tester,
         if(InpTestingMode==TESTING_MODE_ORIGIN)
           {
            //--- if the position is closed by ticket
            if(obj.ClosePos(ticket_tester))
              {
               //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object
               obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
               deal.SetTicketTester(ticket_tester);
              }
           }
         //--- otherwise, in the tester we work with stop orders placed according to different algorithms, and closing deals are skipped
         //--- accordingly, simply increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object
         else
           {
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket_tester);
           }
        }
      //--- if a ticket is now set in the deal object, then the deal has been successfully handled -
      //--- set the deal index in the list to the next deal
      if(deal.TicketTester()>0)
        {
         obj.SetNextDealIndex();
        }
     }
  }

No manipulador OnTester() do EA, calculamos e retornamos o total de negociações processadas no testador:

//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester(void)
  {
//--- calculate and return the total number of deals handled in the tester
   double ret=0.0;
   int total=ExtListSymbols.Total();
   for(int i=0; i<total; i++)
     {
      CSymbolTrade *obj=ExtListSymbols.At(i);
      if(obj!=NULL)
         ret+=obj.OnTester();
     }
   return(ret);
  }

Além disso, para cada objeto de negociação por símbolo, é chamado seu próprio manipulador OnTester(), que imprime seus dados no log. Ao final do teste, no log do testador, veremos mensagens como estas:

2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol AUDUSD: Total deals: 218, number of processed deals: 216
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol EURJPY: Total deals: 116, number of processed deals: 114
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol EURUSD: Total deals: 524, number of processed deals: 518
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol GBPUSD: Total deals: 352, number of processed deals: 350
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol NZDUSD: Total deals: 178, number of processed deals: 176
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol USDCAD: Total deals: 22, number of processed deals: 22
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol USDCHF: Total deals: 250, number of processed deals: 246
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol USDJPY: Total deals: 142, number of processed deals: 142
2025.01.22 23:49:15.951 Core 1  2025.01.21 23:54:59   Symbol XAUUSD: Total deals: 118, number of processed deals: 118
2025.01.22 23:49:15.951 Core 1  final balance 3591.70 pips
2025.01.22 23:49:15.951 Core 1  OnTester result 1902

Compilamos o EA e o executamos com as mesmas configurações de teste, mas agora selecionamos o tipo de teste como "Specified StopLoss and TakeProfit values", definindo os valores de StopLoss e TakeProfit como 100 e 500 pontos, respectivamente:

No teste anterior, com a negociação original, tivemos um prejuízo de 550 dólares. Agora, ao substituir o StopLoss de todas as posições por 100 pontos, e o TakeProfit por 500 pontos, tivemos um lucro de 590 pontos. E isso apenas substituindo os stop orders, sem considerar a especificidade de cada símbolo negociado. Se escolhermos valores de stop order individualmente para cada símbolo negociado, é bem possível que o gráfico do teste fique mais equilibrado.


Conclusão

Hoje fizemos um pequeno experimento com o histórico de negociação no estilo "E se...". Acredito que esse tipo de experimento pode realmente nos levar a ideias interessantes para ajustar nossa forma de negociar. E no próximo artigo, faremos mais um desses testes, dessa vez incluindo diferentes formas de trailing de posições. Vai ser interessante.

Acompanhando o artigo, estão os arquivos de todos os EAs e classes que analisamos hoje. Você pode baixá-los, estudá-los e experimentar com suas próprias contas de negociação. A pasta com o arquivo compactado pode ser extraída diretamente para o diretório MQL5 do terminal cliente, e todos os arquivos ficarão nas subpastas apropriadas.

Programas usados neste artigo:

#
Nome
 Tipo Descrição
1
SymbolTrade.mqh
Biblioteca de classe
Biblioteca da estrutura e da classe de negociação, classe de negociação por símbolo
2
TradingByHistoryDeals.mq5
Expert Advisor
EA para visualizar no testador as negociações e trades realizadas na conta
3
TradingByHistoryDeals_SLTP.mq5
Expert Advisor
EA para visualizar e alterar com StopLoss e TakeProfit, no testador, as negociações e trades realizadas na conta
4
MQL5.zip
Arquivo
Arquivo compactado contendo os arquivos acima para extração no diretório MQL5 do terminal cliente


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

Arquivos anexados |
SymbolTrade.mqh (53.86 KB)
MQL5.zip (22.3 KB)
Últimos Comentários | Ir para discussão (16)
fxsaber
fxsaber | 31 jan. 2025 em 15:32
Artyom Trishkin #:

Qual é o erro?

Se o arquivo for menor do que a matriz antes da leitura, o tamanho da matriz não será alterado.

Você pode ter um erro semelhante ao usar o ArrayCopy.
Artyom Trishkin
Artyom Trishkin | 31 jan. 2025 em 15:34
fxsaber #:
Você está ignorando um bom recurso

Qual é a vantagem?

fxsaber
fxsaber | 31 jan. 2025 em 15:36
Artyom Trishkin #:

Qual é a vantagem?

Concisão e velocidade de execução (totalmente do lado do MQ).

fxsaber
fxsaber | 31 jan. 2025 em 15:52
Artyom Trishkin #:

Mostre as estruturas padrão de auto-impressão e auto-preenchimento, por favor.

Quase padrão (campos MQ compartilhados).
Artyom Trishkin
Artyom Trishkin | 31 jan. 2025 em 16:11
fxsaber #:

Na brevidade e na velocidade de execução (totalmente no lado da MQ).

Obrigado. Perdeu
Métodos de discretização dos movimentos de preço em Python Métodos de discretização dos movimentos de preço em Python
Vamos explorar métodos de discretização de preços com Python + MQL5. Neste artigo, compartilho minha experiência prática no desenvolvimento de uma biblioteca em Python que implementa uma variedade de abordagens para formar barras, desde as clássicas Volume e Range bars até métodos mais exóticos como Renko e Kagi. Barras, candles de três linhas rompidas, range bars — qual é a sua estatística? De que outras formas podemos representar os preços de maneira discreta?
Do básico ao intermediário: Eventos em Objetos (I) Do básico ao intermediário: Eventos em Objetos (I)
Neste artigo irei ver três dos seis eventos que podem ser disparado pelo MetaTrader 5, quando algo acontece a um objeto presente no gráfico. Estes evento são muito uteis quando o assunto é interação com o usuário. Isto por que sem entender estes eventos, você irá ter muito mais trabalho para manter uma certa configuração no gráfico. Tentando controlar objetos com finalidades específicas.
Como Implementar Otimização Automática em Expert Advisors MQL5 Como Implementar Otimização Automática em Expert Advisors MQL5
Guia passo a passo para otimização automática em MQL5 para Expert Advisors. Vamos abordar uma lógica de otimização robusta, boas práticas para seleção de parâmetros e como reconstruir estratégias com backtesting. Além disso, métodos mais avançados como a otimização walk-forward serão discutidos para aprimorar sua abordagem de trading.
Simulação de mercado: Iniciando o SQL no MQL5 (IV) Simulação de mercado: Iniciando o SQL no MQL5 (IV)
Muitos costuma subutilizar o SQL, ou mesmo não fazer uso dele, devido a uma má compreensão de como ele realmente funciona. Quando pesquisamos dentro de um banco de dados SQL. Não queremos necessariamente saber de uma resposta genérica. Podemos em alguns casos, estar buscando uma resposta bastante objetiva e prática. Se você criar um banco de dados, com uma certa estruturação e modelagem. Poderá colocar, virtualmente qualquer tipo de informação dentro do banco de dados.