Русский Español
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 |
170 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:

//+------------------------------------------------------------------+
//|  Структура сделки. Используется для создания файла истории сделок|
//+------------------------------------------------------------------+
struct SDeal
  {
   ulong             ticket;                 // Тикет сделки
   long              order;                  // Ордер, на основании которого была открыта сделка
   long              pos_id;                 // Идентификатор позиции
   long              time_msc;               // Время в миллисекундах
   datetime          time;                   // Время
   double            volume;                 // Объём
   double            price;                  // Цена
   double            profit;                 // Прибыль
   double            commission;             // Комиссия по сделке
   double            swap;                   // Накопленный своп при закрытии
   double            fee;                    // Оплата за проведение сделки, начисляется сразу после совершения сделки
   double            sl;                     // Уровень Stop Loss
   double            tp;                     // Уровень Take Profit
   ENUM_DEAL_TYPE    type;                   // Тип
   ENUM_DEAL_ENTRY   entry;                  // Способ изменения позиции
   ENUM_DEAL_REASON  reason;                 // Причина или источник проведения сделки
   long              magic;                  // Идентификатор эксперта
   int               digits;                 // Digits символа
   ushort            symbol[16];             // Символ
   ushort            comment[64];            // Комментарий к сделке
   ushort            external_id[256];       // Идентификатор сделки во внешней торговой системе (на бирже)
   
//--- Установка строковых свойств
   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()); }
                       
//--- Возврат строковых свойств
   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:

//--- Типы сортировки сделок
enum ENUM_DEAL_SORT_MODE
  {
   SORT_MODE_DEAL_TICKET = 0,          // Режим сравнения/сортировки по тикету сделки
   SORT_MODE_DEAL_ORDER,               // Режим сравнения/сортировки по ордеру, на основание которого выполнена сделка
   SORT_MODE_DEAL_TIME,                // Режим сравнения/сортировки по времени совершения сделки
   SORT_MODE_DEAL_TIME_MSC,            // Режим сравнения/сортировки по времени совершения сделки в миллисекундах
   SORT_MODE_DEAL_TYPE,                // Режим сравнения/сортировки по типу сделки
   SORT_MODE_DEAL_ENTRY,               // Режим сравнения/сортировки по направлению сделки
   SORT_MODE_DEAL_MAGIC,               // Режим сравнения/сортировки по Magic number сделки
   SORT_MODE_DEAL_REASON,              // Режим сравнения/сортировки по причине или источнику проведения сделки
   SORT_MODE_DEAL_POSITION_ID,         // Режим сравнения/сортировки по идентификатору позиции
   SORT_MODE_DEAL_VOLUME,              // Режим сравнения/сортировки по объему сделки
   SORT_MODE_DEAL_PRICE,               // Режим сравнения/сортировки по цене сделки
   SORT_MODE_DEAL_COMMISSION,          // Режим сравнения/сортировки по комиссии
   SORT_MODE_DEAL_SWAP,                // Режим сравнения/сортировки по накопленному свопу при закрытии
   SORT_MODE_DEAL_PROFIT,              // Режим сравнения/сортировки по финансовому результату сделки
   SORT_MODE_DEAL_FEE,                 // Режим сравнения/сортировки по оплате за проведение сделки
   SORT_MODE_DEAL_SL,                  // Режим сравнения/сортировки по уровню Stop Loss
   SORT_MODE_DEAL_TP,                  // Режим сравнения/сортировки по уровню Take Profit
   SORT_MODE_DEAL_SYMBOL,              // Режим сравнения/сортировки по имени символа, по которому произведена сделка
   SORT_MODE_DEAL_COMMENT,             // Режим сравнения/сортировки по комментарию к сделке
   SORT_MODE_DEAL_EXTERNAL_ID,         // Режим сравнения/сортировки по идентификатору сделки во внешней торговой системе 
   SORT_MODE_DEAL_TICKET_TESTER,       // Режим сравнения/сортировки по тикету сделки в тестере
   SORT_MODE_DEAL_POS_ID_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:

//+------------------------------------------------------------------+
//| Сохраняет сделки из истории в массив                             |
//+------------------------------------------------------------------+
int SaveDealsToArray(SDeal &array[], bool logs=false)
  {
//--- структура сделки
   SDeal deal={};
   
//--- запросим историю сделок в интервале с самого начала по текущий момент 
   if(!HistorySelect(0, TimeCurrent()))
     {
      Print("HistorySelect() failed. Error ", GetLastError());
      return 0;
     }
   
//--- общее количество сделок в списке 
   int total=HistoryDealsTotal(); 

//--- обработаем каждую сделку 
   for(int i=0; i<total; i++) 
     { 
      //--- получаем тикет очередной сделки (сделка автоматически выбирается для получения её свойств)
      ulong ticket=HistoryDealGetTicket(i);
      if(ticket==0)
         continue;
      
      //--- сохраняем только балансовые и торговые сделки
      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;
      
      //--- сохраняем свойства сделки в структуре
      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);
      
      //--- увеличиваем массив и
      int size=(int)array.Size();
      ResetLastError();
      if(ArrayResize(array, size+1, total)!=size+1)
        {
         Print("ArrayResize() failed. Error ", GetLastError());
         continue;
        }
      //--- сохраняем в массиве сделку
      array[size]=deal;
      //--- если разрешено, выводим описание сохранённой сделки в журнал
      if(logs)
         DealPrint(deal, i);
     }
//--- возвращаем количество сохранённых в массиве сделок
   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:

//+------------------------------------------------------------------+
//| Выводит сделки из массива в журнал                               |
//+------------------------------------------------------------------+
void DealsArrayPrint(SDeal &array[])
  {
   int total=(int)array.Size();
//--- если передан пустой массив - сообщаем об этом и возвращаем false
   if(total==0)
     {
      PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__);
      return;
     }
//--- В цикле по массиву сделок распечатаем описание каждой сделки
   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:

//+------------------------------------------------------------------+
//| Возвращает описание типа сделки                                  |
//+------------------------------------------------------------------+
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:

//+------------------------------------------------------------------+
//| Возвращает описание способа изменения позиции                    |
//+------------------------------------------------------------------+
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:

//+------------------------------------------------------------------+
//| Возвращает описание сделки                                       |
//+------------------------------------------------------------------+
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:

//+------------------------------------------------------------------+
//| Распечатывает в журнале данные сделки                            |
//+------------------------------------------------------------------+
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:

//+------------------------------------------------------------------+
//| Открывает файл для записи, возвращает хэндл                      |
//+------------------------------------------------------------------+
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;
     }
//--- успешно
   return true;
  }

Função que abre o arquivo para leitura:

//+------------------------------------------------------------------+
//| Открывает файл для чтения, возвращает хэндл                      |
//+------------------------------------------------------------------+
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;
     }
//--- успешно
   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:

//+------------------------------------------------------------------+
//| Сохраняет в файл данные сделок из массива                        |
//+------------------------------------------------------------------+
bool FileWriteDealsFromArray(SDeal &array[], ulong &file_size)
  {
//--- если передан пустой массив - сообщаем об этом и возвращаем false
   if(array.Size()==0)
     {
      PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__);
      return false;
     }
     
//--- откроем файл для записи, получим его хэндл
   int handle=INVALID_HANDLE;
   if(!FileOpenToWrite(handle))
      return false;
   
//--- переместим файловый указатель на конец файла
   bool res=true;
   ResetLastError();
   res&=FileSeek(handle, 0, SEEK_END);
   if(!res)
      PrintFormat("%s: FileSeek(SEEK_END) failed. Error %d",__FUNCTION__, GetLastError());
   
//--- запишем данные массива в конец файла 
   file_size=0;
   res&=(FileWriteArray(handle, array)==array.Size());
   if(!res)
      PrintFormat("%s: FileWriteArray() failed. Error ",__FUNCTION__, GetLastError());
   else
      file_size=FileSize(handle);

//--- закрываем файл 
   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:

//+------------------------------------------------------------------+
//| Загружает в массив данные сделок из файла                        |
//+------------------------------------------------------------------+
bool FileReadDealsToArray(SDeal &array[], ulong &file_size)
  {
//--- откроем файл для чтения, получим его хэндл
   int handle=INVALID_HANDLE;
   if(!FileOpenToRead(handle))
      return false;
   
//--- переместим файловый указатель на конец файла 
   bool res=true;
   ResetLastError();
   
//--- прочитаем данные из файла в массив
   file_size=0;
   res=(FileReadArray(handle, array)>0);
   if(!res)
      PrintFormat("%s: FileWriteArray() failed. Error ",__FUNCTION__, GetLastError());
   else
      file_size=FileSize(handle);

//--- закрываем файл 
   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:

//+------------------------------------------------------------------+
//| Подготавливает файл со сделками истории                          |
//+------------------------------------------------------------------+
bool PreparesDealsHistoryFile(SDeal &deals_array[])
  {
//--- сохраним все сделки счёта в массив сделок
   int total=SaveDealsToArray(deals_array);
   if(total==0)
      return false;
      
//--- запишем данные массива сделок в файл
   ulong file_size=0;
   if(!FileWriteDealsFromArray(deals_array, file_size))
      return false;
      
//--- распечатаем в журнале сколько сделок было прочитано и сохранено в файл, путь к файлу и его размер
   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);
   
//--- теперь для проверки прочитаем данные из файла в массив
   ArrayResize(deals_array, 0, total);
   if(!FileReadDealsToArray(deals_array, file_size))
      return false;
      
//--- распечатаем в журнале сколько байт было прочитано из файла и количество полученных в массив сделок
   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(!MQLInfoInteger(MQL_TESTER))
     {
      //--- подготовим файл со всеми историческими сделками
      if(!PreparesDealsHistoryFile(ExtArrayDeals))
         return(INIT_FAILED);
         
      //--- распечатаем в журнале все сделки после загрузки их из файла
      if(InpShowDataInLog)
         DealsArrayPrint(ExtArrayDeals);
         
      //--- получаем первую балансовую сделку, создаём текст сообщения в выводим его при помощи 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);
      
      //--- сообщим алертом рекомендуемые параметры тестера стратегий для запуска тестирования
      Alert(message);
     }
     
//--- Всё успешно
   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.

//+------------------------------------------------------------------+
//| Класс сделки. Используется для торговли в тестере стратегий      |
//+------------------------------------------------------------------+
class CDeal : public CObject
  {
protected:
//--- Целочисленные свойства
   ulong             m_ticket;            // Тикет сделки. Уникальное число, которое присваивается каждой сделке
   long              m_order;             // Ордер, на основание которого выполнена сделка
   datetime          m_time;              // Время совершения сделки
   long              m_time_msc;          // Время совершения сделки в миллисекундах с 01.01.1970
   ENUM_DEAL_TYPE    m_type;              // Тип сделки
   ENUM_DEAL_ENTRY   m_entry;             // Направление сделки – вход в рынок, выход из рынка или разворот
   long              m_magic;             // Magic number для сделки (смотри ORDER_MAGIC)
   ENUM_DEAL_REASON  m_reason;            // Причина или источник проведения сделки
   long              m_pos_id;            // Идентификатор позиции, в открытии, изменении или закрытии которой участвовала эта сделка
   
//--- Вещественные свойства
   double            m_volume;            // Объем сделки
   double            m_price;             // Цена сделки
   double            m_commission;        // Комиссия по сделке
   double            m_swap;              // Накопленный своп при закрытии
   double            m_profit;            // Финансовый результат сделки
   double            m_fee;               // Оплата за проведение сделки, начисляется сразу после совершения сделки
   double            m_sl;                // Уровень Stop Loss
   double            m_tp;                // Уровень Take Profit

//--- Строковые свойства
   string            m_symbol;            // Имя символа, по которому произведена сделка
   string            m_comment;           // Комментарий к сделке
   string            m_external_id;       // Идентификатор сделки во внешней торговой системе (на бирже)
   
//--- Дополнительные свойства
   int               m_digits;            // Digits символа
   double            m_point;             // Point символа
   ulong             m_ticket_tester;     // Тикет позиции в тестере
   long              m_pos_id_tester;     // Идентификатор позиции в тестере
   
public:
//--- Установка свойств сделки
   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;   }
   
//--- Возврат свойств сделки
   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;   }
   
//--- Сравнивает два объекта между собой по указанному в 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);
                          }
                       }
   
//--- Конструкторы/деструктор
                     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:

//+------------------------------------------------------------------+
//|  Класс торговли по символу                                       |
//+------------------------------------------------------------------+
CDeal DealTmp; // Временный объект сделки для поиска по свойствам

class CSymbolTrade : public CObject
  {
private:
   int               m_index_next_deal;                  // Индекс очередной ещё не обработанной сделки
   int               m_deals_processed;                  // Количество обработанных сделок
protected:
   MqlTick           m_tick;                             // Структура тика
   CArrayObj         m_list_deals;                       // Список сделок, проведённых по символу
   CTrade            m_trade;                            // Торговый класс
   string            m_symbol;                           // Наименование символа
public:
//--- Возвращает список сделок
   CArrayObj        *GetListDeals(void)                  { return(&this.m_list_deals);       }
   
//--- Устанавливает символ
   void              SetSymbol(const string symbol)      { this.m_symbol=symbol;             }
   
//--- (1) Устанавливает, (2) возвращает количество обработанных сделок
   void              SetNumProcessedDeals(const int num) { this.m_deals_processed=num;       }
   int               NumProcessedDeals(void)       const { return this.m_deals_processed;    }
   
//--- Добавляет сделку в массив сделок
   bool              AddDeal(CDeal *deal);
   
//--- Возвращает сделку (1) по времени в секундах, (2) по индексу в списке,
//--- (3) сделку открытия по идентификатору позиции, (4) текущую сделку в списке
   CDeal            *GetDealByTime(const datetime time);
   CDeal            *GetDealByIndex(const int index);
   CDeal            *GetDealInByPosID(const long pos_id);
   CDeal            *GetDealCurrent(void);
   
//--- Возвращает (1) количество сделок в списке, (2) индекс текущей сделки в списке
   int               DealsTotal(void)              const { return this.m_list_deals.Total(); }
   int               DealCurrentIndex(void)        const { return this.m_index_next_deal;    }
   
//--- Возвращает (1) символ, (2) описание объекта
   string            Symbol(void)                  const { return this.m_symbol;             }
   string            Description(void) const
                       {
                        return ::StringFormat("%s trade object. Total deals: %d", this.Symbol(), this.DealsTotal() );
                       }

//--- Возвращает текущую цену (1) Bid, (2) Ask, время в (3) секундах, (4) миллисекундах
   double            Bid(void);
   double            Ask(void);
   datetime          Time(void);
   long              TimeMsc(void);
   
//--- Открывает (1) длинную, (2) короткую позицию, (3) закрывает позицию по тикету
   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);

//--- Возвращает результат сравнения текущего времени с указанным
   bool              CheckTime(const datetime time)      { return(this.Time()>=time);        }
//--- Устанавливает индекс следующей сделки
   void              SetNextDealIndex(void)              { this.m_index_next_deal++;         }
   
//--- Обработчик OnTester. Возвращает количество обработанных тестером сделок
   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;
                       }

//--- Сравнивает два объекта между собой (сравнение только по символу)
   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);
                       }
//--- Конструкторы/деструктор
                     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:

//--- Конструкторы/деструктор
                     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::Добавляет сделку в массив сделок                   |
//+------------------------------------------------------------------+
bool CSymbolTrade::AddDeal(CDeal *deal)
  {
//--- Если в списке уже есть сделка с тикетом сделки, переданной в метод - возвращаем true
   this.m_list_deals.Sort(SORT_MODE_DEAL_TICKET);
   if(this.m_list_deals.Search(deal)>WRONG_VALUE)
      return true;
   
//--- Добавляем указатель на сделку в список в порядке сортировки по времени в миллисекундах
   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;
     }
//--- Всё успешно
   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::Возвращает объект сделки по времени в секундах     |
//+------------------------------------------------------------------+
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::Возвращает сделку открытия по идентификатору позиции|
//+------------------------------------------------------------------+
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::Возвращает объект сделки по индексу в списке       |
//+------------------------------------------------------------------+
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:

//+------------------------------------------------------------------+
//| Возвращает сделку, на которую указывает индекс текущей сделки    |
//+------------------------------------------------------------------+
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::Возвращает текущую цену Bid                        |
//+------------------------------------------------------------------+
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::Возвращает текущую цену Ask                        |
//+------------------------------------------------------------------+
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::Возвращает текущее время в секундах                |
//+------------------------------------------------------------------+
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::Возвращает текущее время в миллисекундах           |
//+------------------------------------------------------------------+
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::Открывает длинную позицию                          |
//+------------------------------------------------------------------+
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::Открывает короткую позицию                         |
//+------------------------------------------------------------------+
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::Закрывает позицию по тикету                        |
//+------------------------------------------------------------------+
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:

//+------------------------------------------------------------------+
//| Создаёт объект сделки из структуры                               |
//+------------------------------------------------------------------+
CDeal *CreateDeal(SDeal &deal_str)
  {
//--- Если объект сделки создать не удалось - сообщаем в журнале об ошибке и возвращаем NULL
   CDeal *deal=new CDeal(deal_str.ticket, deal_str.Symbol());
   if(deal==NULL)
     {
      PrintFormat("%s: Error. Failed to create deal object");
      return NULL;
     }
//--- заполняем свойства сделки из полей структуры
   deal.SetOrder(deal_str.order);               // Ордер, на основании которого была открыта сделка
   deal.SetPositionID(deal_str.pos_id);         // Идентификатор позиции
   deal.SetTimeMsc(deal_str.time_msc);          // Время в миллисекундах
   deal.SetTime(deal_str.time);                 // Время
   deal.SetVolume(deal_str.volume);             // Объём
   deal.SetPrice(deal_str.price);               // Цена
   deal.SetProfit(deal_str.profit);             // Прибыль
   deal.SetCommission(deal_str.commission);     // Комиссия по сделке
   deal.SetSwap(deal_str.swap);                 // Накопленный своп при закрытии
   deal.SetFee(deal_str.fee);                   // Оплата за проведение сделки, начисляется сразу после совершения сделки
   deal.SetSL(deal_str.sl);                     // Уровень Stop Loss
   deal.SetTP(deal_str.tp);                     // Уровень Take Profit
   deal.SetType(deal_str.type);                 // Тип
   deal.SetEntry(deal_str.entry);               // Способ изменения позиции
   deal.SetReason(deal_str.reason);             // Причина или источник проведения сделки
   deal.SetMagic(deal_str.magic);               // Идентификатор эксперта
   deal.SetComment(deal_str.Comment());         // Комментарий к сделке
   deal.SetExternalID(deal_str.ExternalID());   // Идентификатор сделки во внешней торговой системе (на бирже)
//--- Возвращаем указатель на созданный объект
   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:

//+------------------------------------------------------------------+
//| Создаёт массив используемых символов                             |
//+------------------------------------------------------------------+
bool CreateListSymbolTrades(SDeal &array_deals[], CArrayObj *list_symbols)
  {
   bool res=true;                      // результат
   int total=(int)array_deals.Size();  // общее количество сделок в массиве
   
//--- если массив сделок пустой - возвращаем false
   if(total==0)
     {
      PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__);
      return false;
     }
   
//--- в цикле по массиву сделок
   CSymbolTrade *SymbolTrade=NULL;
   for(int i=0; i<total; i++)
     {
      //--- получаем очередную сделку и, если это не покупка и не продажа - идём к следующей
      SDeal deal_str=array_deals[i];
      if(deal_str.type!=DEAL_TYPE_BUY && deal_str.type!=DEAL_TYPE_SELL)
         continue;
      
      //--- найдём торговый объект в списке, у которого символ равен символу сделки
      string symbol=deal_str.Symbol();
      SymbTradeTmp.SetSymbol(symbol);
      list_symbols.Sort();
      int index=list_symbols.Search(&SymbTradeTmp);
      
      //--- если индекс искомого объекта в списке равен -1 - такого объекта в списке нет
      if(index==WRONG_VALUE)
        {
         //--- создаём новый торговый объект символа и, если создать не получилось -
         //--- добавляем к результату значение false и идём к следующей сделке
         SymbolTrade=new CSymbolTrade(symbol);
         if(SymbolTrade==NULL)
           {
            res &=false;
            continue;
           }
         //--- если торговый объект символа не удалось добавить в список -
         //--- удаляем вновь созданный объект, добавляем к результату значение false
         //--- и идём к следующей сделке
         if(!list_symbols.Add(SymbolTrade))
           {
            delete SymbolTrade;
            res &=false;
            continue;
           }
        }
      //--- иначе, если торговый объект уже существует в списке - получаем его по индексу
      else
        {
         SymbolTrade=list_symbols.At(index);
         if(SymbolTrade==NULL)
            continue;
        }
         
      //--- если текущей сделки ещё нет в списке сделок торгового объекта символа
      if(SymbolTrade.GetDealByTime(deal_str.time)==NULL)
        {
         //--- создаём объект сделки по её образцу-структуре
         CDeal *deal=CreateDeal(deal_str);
         if(deal==NULL)
           {
            res &=false;
            continue;
           }
         //--- к значению результата добавляем результат добавления объекта сделки в список сделок торгового объекта символа
         res &=SymbolTrade.AddDeal(deal);
        }
     }
//--- возвращаем итоговый результат создания торговых объектов и добавления сделок в их списки
   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:

//+------------------------------------------------------------------+
//| Выводит в журнал список торговых объектов символов               |
//+------------------------------------------------------------------+
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:

//+------------------------------------------------------------------+
//| Возвращает описание сделки                                       |
//+------------------------------------------------------------------+
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(!MQLInfoInteger(MQL_TESTER))
     {
      //--- подготовим файл со всеми историческими сделками
      if(!PreparesDealsHistoryFile(ExtArrayDeals))
         return(INIT_FAILED);
         
      //--- распечатаем в журнале все сделки после загрузки их из файла
      if(InpShowDataInLog)
         DealsArrayPrint(ExtArrayDeals);
         
      //--- получаем первую балансовую сделку, создаём текст сообщения в выводим его при помощи 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);
      
      //--- сообщим алертом рекомендуемые параметры тестера стратегий для запуска тестирования
      Alert(message);
     }
//--- Советник запущен в тестере
   else
     {
      //--- прочитаем данные из файла в массив
      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);
        }
         
      //--- сообщим в журнале о количестве прочитанных байт из файла и о записи массива сделок.
      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());
     }
     
//--- Из массива исторических сделок создаём список торговых объектов по символам
   if(!CreateListSymbolTrades(ExtArrayDeals, &ExtListSymbols))
     {
      Print("Errors found while creating symbol list");
      return(INIT_FAILED);
     }
//--- Распечатаем в журнале созданный список сделок
   SymbolsArrayPrint(&ExtListSymbols);
   
//--- Обратимся к каждому символу для начала закачки исторических данных
//--- и открытия графиков проторгованных символов в тестере стратегий
   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);
     }
     
//--- Всё успешно
   return(INIT_SUCCEEDED);
  }

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

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- очищаем созданные списки и массивы
   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()
  {
//--- работаем только в тестере стратегий
   if(!MQLInfoInteger(MQL_TESTER))
      return;
      
//---  Обрабатываем в тестере список сделок из файла
   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:

//+------------------------------------------------------------------+
//| Торговля по истории                                              |
//+------------------------------------------------------------------+
void TradeByHistory(const string symbol="", const long magic=-1)
  {
   datetime time=0;
   int total=ExtListSymbols.Total();   // количество торговых объектов в списке
   
//--- в цикле по всем торговым объектам символов
   for(int i=0; i<total; i++)
     {
      //--- получаем очередной торговый объект
      CSymbolTrade *obj=ExtListSymbols.At(i);
      if(obj==NULL)
         continue;
      
      //--- получаем текущую сделку, на которую указывает индекс списка сделок
      CDeal *deal=obj.GetDealCurrent();
      if(deal==NULL)
         continue;
      
      //--- фильтруем сделку по магику и символу
      if((magic>-1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol))
         continue;
      
      //--- фильтруем сделку по типу (только сделки покупки/продажи)
      ENUM_DEAL_TYPE type=deal.TypeDeal();
      if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL)
         continue;
      
      //--- если это уже обработанная в тестере сделка - идём к следующей
      if(deal.TicketTester()>0)
         continue;
      
      //--- если время сделки ещё не настало - идём к следующему торговому объекту следующего символа
      if(!obj.CheckTime(deal.Time()))
         continue;

      //--- если сделка входа в рынок
      ENUM_DEAL_ENTRY entry=deal.Entry();
      if(entry==DEAL_ENTRY_IN)
        {
         //--- открываем позицию по типу сделки
         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(ticket>0)
           {
            //--- увеличиваем количество обработанных тестером сделок и записываем тикет сделки в тестере в свойства объекта сделки
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket);
            //--- получаем идентификатор позиции в тестере и записываем его в свойства объекта сделки
            long pos_id_tester=0;
            if(HistoryDealSelect(ticket))
              {
               pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
               deal.SetPosIDTester(pos_id_tester);
              }
           }
        }
      
      //--- если сделка выхода из рынка
      if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY)
        {
         //--- получаем сделку, на основании которой была открыта позиция
         CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID());
         if(deal_in==NULL)
            continue;

         //--- получаем тикет позиции в тестере из свойств открывающей сделки
         //--- если тикет равен нулю, значит скорее всего позиция в тестере уже закрыта
         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(obj.ClosePos(ticket_tester))
           {
            //--- увеличиваем количество обработанных тестером сделок и записываем тикет сделки в тестере в свойства объекта сделки
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket_tester);
           }
        }
      //--- если теперь в объекте сделки записан тикет - значит сделка успешно обработана -
      //--- устанавливаем индекс сделки в списке на следующую сделку
      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  */ // Указанные значения StopLoss и TakeProfit
  };

//+------------------------------------------------------------------+
//| 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                           */ // Отступ StopLoss в пунктах
input    int               InpTakeProfit     =  500;                 /* TakeProfit in points                         */ // Отступ TakeProfit в пунктах

//--- 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()
  {
//--- Корректируем размеры стопов
   ExtStopLoss  =(InpStopLoss<1   ? 0 : InpStopLoss);
   ExtTakeProfit=(InpTakeProfit<1 ? 0 : InpTakeProfit);
   
//--- Если советник запущен не в тестере

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:

//+------------------------------------------------------------------+
//| Возвращает корректный StopLoss относительно 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)
     );
  }
//+------------------------------------------------------------------+
//| Возвращает корректный TakeProfit относительно 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)
     );
  }
//+------------------------------------------------------------------+
//| Возвращает размер StopLevel в пунктах                            |
//+------------------------------------------------------------------+
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.

//+------------------------------------------------------------------+
//| Торговля по истории                                              |
//+------------------------------------------------------------------+
void TradeByHistory(const string symbol="", const long magic=-1)
  {
   datetime time=0;
   int total=ExtListSymbols.Total();   // количество торговых объектов в списке
   
//--- в цикле по всем торговым объектам символов
   for(int i=0; i<total; i++)
     {
      //--- получаем очередной торговый объект
      CSymbolTrade *obj=ExtListSymbols.At(i);
      if(obj==NULL)
         continue;
      
      //--- получаем текущую сделку, на которую указывает индекс списка сделок
      CDeal *deal=obj.GetDealCurrent();
      if(deal==NULL)
         continue;
      
      //--- фильтруем сделку по магику и символу
      if((magic>-1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol))
         continue;
      
      //--- фильтруем сделку по типу (только сделки покупки/продажи)
      ENUM_DEAL_TYPE type=deal.TypeDeal();
      if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL)
         continue;
      
      //--- если это уже обработанная в тестере сделка - идём к следующей
      if(deal.TicketTester()>0)
         continue;
      
      //--- если время сделки ещё не настало - идём к следующему торговому объекту следующего символа
      if(!obj.CheckTime(deal.Time()))
         continue;

      //--- если сделка входа в рынок
      ENUM_DEAL_ENTRY entry=deal.Entry();
      if(entry==DEAL_ENTRY_IN)
        {
         //--- устанавливаем размеры стоп-приказов в зависимости от метода установки стопов
         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);
           }
         //--- открываем позицию по типу сделки
         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(ticket>0)
           {
            //--- увеличиваем количество обработанных тестером сделок и записываем тикет сделки в тестере в свойства объекта сделки
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket);
            //--- получаем идентификатор позиции в тестере и записываем его в свойства объекта сделки
            long pos_id_tester=0;
            if(HistoryDealSelect(ticket))
              {
               pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
               deal.SetPosIDTester(pos_id_tester);
              }
           }
        }
      
      //--- если сделка выхода из рынка
      if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY)
        {
         //--- получаем сделку, на основании которой была открыта позиция
         CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID());
         if(deal_in==NULL)
            continue;

         //--- получаем тикет позиции в тестере из свойств открывающей сделки
         //--- если тикет равен нулю, значит скорее всего позиция в тестере уже закрыта
         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(InpTestingMode==TESTING_MODE_ORIGIN)
           {
            //--- если позиция закрыта по тикету
            if(obj.ClosePos(ticket_tester))
              {
               //--- увеличиваем количество обработанных тестером сделок и записываем тикет сделки в тестере в свойства объекта сделки
               obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
               deal.SetTicketTester(ticket_tester);
              }
           }
         //--- иначе - в тестере работаем со стоп-приказами, выставляемыми по различным алгоритмам, и сделки закрытия пропускаются
         //--- соответственно, просто увеличиваем количество обработанных тестером сделок и записываем тикет сделки в тестере в свойства объекта сделки
         else
           {
            obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1);
            deal.SetTicketTester(ticket_tester);
           }
        }
      //--- если теперь в объекте сделки записан тикет - значит сделка успешно обработана -
      //--- устанавливаем индекс сделки в списке на следующую сделку
      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)
  {
//--- посчитаем и вернём общее количество обработанных в тестере сделок
   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 (54.77 KB)
MQL5.zip (23.57 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.