Español Português
preview
Визуальная оценка и корректировка торговли в MetaTrader 5

Визуальная оценка и корректировка торговли в MetaTrader 5

MetaTrader 5Примеры |
613 16
Artyom Trishkin
Artyom Trishkin

Содержание



Введение

Представим ситуацию: на каком-либо счёте достаточно долгое время ведётся более-менее активная торговля на разных инструментах разными советниками и где-то даже вручную. И вот, по истечении некоторого времени, мы хотим видеть результаты работы. Естественно, можно посмотреть стандартные отчёты по торговле в терминале, нажав сочетание клавиш Alt+E. Можно загрузить значки сделок на график и посмотреть входы и выходы позиций. Но что, если хочется в динамике посмотреть как велась торговля, где и как открывались и закрывались позиции? Посмотреть раздельно по каждому символу, либо сразу все вместе, открытие и закрытие позиций, на каких уровнях ставились стоп-приказы, и обоснован ли был их размер. А что если далее мы зададимся вопросом "что бы было, если бы ..." (и тут много вариантов — другие стопы, по иным алгоритмам и критериям, использование трейлинга позиций, либо перенос стопов в безубыток и т.д.); а потом ещё и протестировать все свои "если" с наглядным видимым результатом. Как бы могла измениться торговля, если бы...

Для решения таких вопросов, оказывается, всё уже есть. Нам достаточно лишь загрузить историю счёта в файл — все проведённые сделки, а затем в тестере стратегий прогнать советник, который читает сделки из файла и открывает/закрывает позиции в тестере стратегий клиентского терминала. При наличии такого советника, мы сможем добавить в него код для изменения условий выхода из позиций и сравнить, как при этом изменилась торговля, и что бы было, если бы...

Что нам это даст? Ещё один инструмент для поиска лучших результатов, для внесения корректировок в торговлю, которая некоторое время уже велась на счёте; визуальное тестирование позволит в динамике видеть верно ли открывались позиции на том, или ином инструменте, и в нужное ли время они закрывались, и т.д. И главное — какой-то новый алгоритм можно просто добавить в код советника, протестировать, получить результат и внести корректировки в советники, работающие на этом счёте.

Сделаем такую логику поведения советника:

  • если советник запущен на графике любого инструмента, то он соберёт всю историю сделок на текущем счёте, сохранит все сделки в один файл и далее ничего делать не будет;
  • если советник запущен в тестере, то он прочитает записанную в файл историю сделок и, во время прохождения теста, будет повторять все сделки из файла, открывая и закрывая позиции.

Таким образом, советник сначала подготавливает файл истории сделок (при запуске его на графике), а затем выполняет сделки из файла, полностью повторяя торговлю на счёте (при запуске его в тестере стратегий).

Далее, внесём доработки в советник, чтобы иметь возможность выставить иные размеры StopLoss и TakeProfit для открываемых в тестере позиций.



Сохраняем историю сделок в файл

В каталоге терминала \MQL5\Experts\ создадим новую папку TradingByHistoryDeals, а в ней — новый файл советника с именем TradingByHistoryDeals.mq5.

В советнике должна быть возможность выбора по какому символу и по какому магику проводить тестирование. В случае, если на счёте работало несколько советников по нескольким символам или магикам, то можно будет выбрать в настройках какой символ или магик нас интересует, ну либо сразу все вместе.

//+------------------------------------------------------------------+
//|                                        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               */ // Показать собранные данные сделок в журнале

Значения по умолчанию для символа и магика — пустая строка и -1. При этих значениях советник не будет фильтровать историю торговли ни по символу, ни по магику — тестироваться будет полностью вся торговая история. Третьей строкой мы указываем советнику вывести (либо нет) в журнал описания всех сделок, сохранённых в файле — чтобы можно было наглядно проконтролировать верность сохранённых данных.

Каждая сделка — это целый набор различных параметров, описываемых разными свойствами сделки. Самое простое — записать все свойства сделки в структуру. Для записи в файл большого количества сделок, необходимо использовать массив структур. И далее этот массив сохранять в файл. В языке MQL5 для этого всё есть. Логика сохранения истории сделок в файл будет такой:

  • идём в цикле по историческим сделкам;
  • получаем очередную сделку и записываем её данные в структуру;
  • созданную структуру сделки сохраняем в массив сделок;
  • по окончании цикла подготовленный массив структур сохраняем в файл.

Все дополнительные коды — структуры, классы, перечисления, будем писать в отдельном файле. Назовём его по имени будущего класса торгового объекта символа.

В той же папке создадим новый подключаемый файл с именем SymbolTrade.mqh.

Сразу же напишем макроподстановки для имени каталога, в который будет записываться файл истории, имя файла и путь к файлу, и подключим к созданному файлу все необходимые для разработки файлы Стандартной Библиотеки:

//+------------------------------------------------------------------+
//|                                                  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>

Далее напишем структуру сделки:

//+------------------------------------------------------------------+
//|  Структура сделки. Используется для создания файла истории сделок|
//+------------------------------------------------------------------+
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));                                              }
  };

Так как структуры сделок будем сохранять в файл, а в файл можно записывать только структуры простых типов (см. FileWriteArray()), то все строковые переменные необходимо заменить на ushort-массивы и создать методы для записи и возврата строковых свойств структуры.

Созданная структура необходима только для сохранения истории сделок в файл и чтения записанной истории из файла. В самом же советнике далее будет создан список объектов, в котором будут храниться объекты класса сделки. Для поиска нужной сделки в списке и сортировки массива, необходимо будет указывать свойство сделки, по которому она будет искаться в списке. Для поиска сделки список объектов должен быть отсортирован по искомому свойству.

Напишем перечисление всех свойств объекта сделки, по которым возможно будет осуществлять поиск:

//--- Типы сортировки сделок
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,       // Режим сравнения/сортировки по идентификатору позиции в тестере
  };

Здесь, помимо стандартных свойств сделки, есть ещё два: тикет сделки в тестере и идентификатор позиции в тестере. Дело в том, что мы будем торговать в тестере на основе данных реальных сделок, а открываемые в тестере позиции и, соответственно, те сделки, на основе которых были позиции открыты, в тестере имеют совсем иные тикет и идентификатор. Для того, чтобы мы могли сопоставить реальную сделку со сделкой в тестере (так же и идентификатор), нам нужно будет тикет и идентификатор позиции в тестере сохранять в свойствах объекта сделки, и уже по этим сохранённым данным сопоставлять сделку в тестере с реальной сделкой в истории.

На этом пока остановимся с этим файлом и перейдём к файлу советника, созданного чуть ранее. Добавим массив структур, куда будем добавлять структуры всех сделок истории:

//--- 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[]={};

И напишем функции для работы с историческими сделками.

Функция, сохраняющая историю сделок в массив:

//+------------------------------------------------------------------+
//| Сохраняет сделки из истории в массив                             |
//+------------------------------------------------------------------+
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();
  }

Код функции подробно прокомментирован. Выбираем всю торговую историю от начала и до текущего времени, получаем каждую очередную историческую сделку, сохраняем её свойства в полях структуры и переменную структуры сохраняем в массиве. По окончании цикла по истории сделок, возвращаем размер полученного массива сделок. Для контроля за ходом записи сделок в массив, можно каждую обработанную сделку распечатать в журнале. Для этого, при вызове функции нужно указать флаг logs в формальных параметрах функции равным true.

Функция, распечатывающая в журнале все сделки из массива сделок:

//+------------------------------------------------------------------+
//| Выводит сделки из массива в журнал                               |
//+------------------------------------------------------------------+
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);
     }
  }

Для вывода описания сделки в журнал, сделаем несколько функций.

Функция, возвращающая описание типа сделки:

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

В зависимости от переданного в функцию типа сделки, выводится соответствующая строка.

Функция, возвращающая описание способа изменения позиции:

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

В зависимости от переданного в функцию способа изменения позиции, выводится соответствующая строка.

Функция, возвращающая описание сделки:

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

Если это балансовая сделка, то выводится описание в виде

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

Иначе, выводится описание сделки в ином формате:

    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

Функция, распечатывающая в журнале описание сделки:

//+------------------------------------------------------------------+
//| Распечатывает в журнале данные сделки                            |
//+------------------------------------------------------------------+
void DealPrint(SDeal &deal, const int index)
  {
   Print(DealDescription(deal, index));
  }

Здесь всё наглядно — просто распечатываем строку, полученную из функции DealDescription().

Напишем функции для записи и чтения массива сделок в файл/из файла.

Функция, открывающая файл для записи:

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

Функция, открывающая файл для чтения:

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

Функции открывают файл для чтения/записи. В формальных параметрах по ссылке передаётся переменная, в которую записывается хэндл файла. Возвращают true при успешном открытии файла и false — при ошибке.

Функция, сохраняющая в файл данные сделок из массива:

//+------------------------------------------------------------------+
//| Сохраняет в файл данные сделок из массива                        |
//+------------------------------------------------------------------+
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;
  }

В функцию передаётся массив структур, который необходимо сохранить в файле. Переменная для получения размера созданного файла передаётся по ссылке в формальных параметрах функции. Открываем файл, перемещаем файловый указатель в конец файла и записываем данные из массива структур в файл, начиная от указателя. По окончании записи файл закрывается.

После того, как массив структур сделок сохранён в файл, из этого файла можно обратно считать все сделки в массив, и далее его использовать для создания списков сделок и работы с ними в тестере.

Функция, загружающая в массив данные сделок из файла:

//+------------------------------------------------------------------+
//| Загружает в массив данные сделок из файла                        |
//+------------------------------------------------------------------+
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;
  }

На основе созданных выше функций напишем функцию для чтения истории сделок и записи их в файл.

Функция, подготавливающая файл со сделками истории:

//+------------------------------------------------------------------+
//| Подготавливает файл со сделками истории                          |
//+------------------------------------------------------------------+
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;
  }

По комментариям в коде вся логика здесь понятна. Функция запускается в обработчике OnInit() и подготавливает файл со сделками для дальнейшей работы с ним:

//+------------------------------------------------------------------+
//| 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);
  }

Помимо сохранения всех исторических сделок в файл, здесь ещё выводится алерт с сообщением о рекомендуемых настройках тестера — начальный баланс, плечо и время начала тестирования, соответствующее дате первой балансовой сделки. Например так:

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

Такие настройки тестера дадут итоговый результат в тестере наиболее близким к тому, который получен в реальности.

Структура сделки, написанная в файле \MQL5\Experts\TradingByHistoryDeals\SymbolTrade.mqh, предназначена только для сохранения истории сделок в файл и для чтения сохранённой истории из файла. Для дальнейшей работы нам необходимо создать класс сделки, объекты которого будут храниться в списках. А сами списки — в объектах торгового класса для тестера. В свою очередь, торговые объекты — это тоже объекты класса, которые будут храниться в своём списке. Каждый торговый объект будет определяться по принадлежности его к определённому своему символу — сколько символов участвовало в торговле, столько будет и торговых объектов. Сами торговые объекты будут содержать в себе список сделок только по своему символу и свои объекты класса CTrade Стандартной Библиотеки. Это позволит каждый торговый объект класса CTrade настроить в соответствии с условиями торгуемого символа.

Напишем класс сделки в файле \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) {}
  };

Класс практически повторяет ранее созданную структуру сделки. Кроме свойств сделки добавлены дополнительные свойства — Digits и Point символа, по которому была проведена сделка. Это упрощает вывод описания сделки, так как эти данные устанавливаются в конструкторе сделки сразу при создании объекта, что освобождает от необходимости получать эти свойства для каждой сделки (если они нужны) при обращении к ней.
Также здесь создан виртуальный метод Compare() для сравнения двух объектов сделок — он будет использоваться при сортировке списков сделок для поиска нужной сделки по указанному свойству.

Теперь создадим торговый класс символа. В классе будет храниться список сделок, проведённых по символу, записанному в свойствах объекта, из него же эти сделки будут запрашиваться тестером для их копирования. В общем, этот класс будет являться основой для копирования сделок, проведённых на счёте по символу, в тестере стратегий:

//+------------------------------------------------------------------+
//|  Класс торговли по символу                                       |
//+------------------------------------------------------------------+
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) {}
  };

Рассмотрим некоторые методы.

  • SetNumProcessedDeals() и NumProcessedDeals() устанавливают и возвращают количество уже обработанных тестером исторических сделок из полученного из файла списка сделок. Необходимы для контроля правильности обработки исторических сделок и получения итоговой статистики количества обработанных сделок тестером;
  • GetDealCurrent() возвращает указатель на текущую историческую сделку, которую необходимо обработать тестером и пометить её обработанной;
  • DealCurrentIndex() возвращает индекс исторической сделки, выбранной для обработки тестером на текущий момент;
  • SetNextDealIndex() после завершения обработки текущей исторической сделки, устанавливает индекс следующей сделки для обработки её тестером. Так как все исторические сделки в списке отсортированы по времени в миллисекундах, то это будет установка индекса следующей сделки после завершения обработки тестером предыдущей. Таким образом мы будем выбирать последовательно все сделки истории, которые будут обрабатываться тестером в момент наступления времени, записанного в свойствах текущей выбранной сделки;
  • CheckTime() проверяет момент наступления в тестере времени, записанного в свойствах текущей исторической сделки. Логика такова: есть выбранная сделка, которую нужно обработать в тестере. До тех пор, пока время в тестере меньше времени, записанного в сделке, ничего не делаем — просто идём на следующий тик. Как только время в тестере становится равным, либо больше времени в текущей выбранной сделке (время в тестере может не совпасть со временем в сделке, поэтому время проверяется и на "больше"), сделка обрабатывается тестером в зависимости от её типа и способа изменения позиции сделкой. Далее эта сделка помечается обработанной, устанавливается индекс следующей сделки, и процесс ожидания времени, контролируемый этим методом, продолжается, но уже для следующей сделки:
  • Обработчик OnTester() вызывается из стандартного обработчика OnTester() советника, выводит в журнал имя символа, количество исторических и обработанных тестером сделок, и возвращает количество обработанных сделок по символу торгового объекта;

Класс имеет два конструктора — по умолчанию и параметрический.

В параметрическом конструкторе, в формальных параметрах, передаётся наименование символа торгового объекта, которое устанавливается при его создании, и в торговый объект класса CTrade устанавливаются режим расчёта маржи,в соответствии с настройками текущего счета, и тип ордера по исполнению, согласно настройкам символа торгового объекта:

//--- Конструкторы/деструктор
                     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::Добавляет сделку в массив сделок                   |
//+------------------------------------------------------------------+
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;
  }

В метод передаётся указатель на объект сделки. Если сделка с таким тикетом уже есть в списке — просто возвращается true. Иначе — список сортируется по времени сделок в миллисекундах, и сделка добавляется в список в порядке сортировки по времени в мс.

Метод, возвращающий указатель на объект сделки по времени в секундах:

//+------------------------------------------------------------------+
//| 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);
  }

В метод передаётся искомое время. Временному объекту сделки устанавливаем переданное в метод время, список сортируется по времени в миллисекундах, и в нём ищется индекс сделки, у которой время равно переданному в метод (установленному во временный объект). Далее возвращается указатель на сделку в списке по найденному индексу. Если сделки с таким временем нет в списке, то индекс будет равен -1, и из списка будет возвращён NULL.

Интересно, что сделка ищется по времени в секундах, но список мы сортируем по времени в миллисекундах. Тесты показали, что если список сортировать тоже в секундах, то некоторые сделки в нём не находятся, хотя они точно есть. Скорее всего, это связано с тем, что в одной секунде есть несколько сделок со временем в миллисекундах. И возвращается указатель на уже обработанную ранее сделку, так как несколько сделок имеют одинаковое время в секундах.

Метод, возвращающий указатель на сделку открытия по идентификатору позиции:

//+------------------------------------------------------------------+
//|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;
  }

В метод передаётся идентификатор позиции, открывающую сделку которой необходимо найти. Далее в цикле по списку сделок получаем сделку, у которой идентификатор позиции равен переданному в метод, и возвращаем указатель на сделку, у которой способ изменения позиции равен "Вход в рынок" (DEAL_ENTRY_IN).

Метод, возвращающий указатель на объект сделки по индексу в списке:

//+------------------------------------------------------------------+
//| CSymbolTrade::Возвращает объект сделки по индексу в списке       |
//+------------------------------------------------------------------+
CDeal *CSymbolTrade::GetDealByIndex(const int index)
  {
   return this.m_list_deals.At(index);
  }

Просто возвращаем указатель на объект в списке по переданному в метод индексу. Если индекс некорректен, то будет возвращён NULL.

Метод, возвращающий указатель на сделку, на которую указывает индекс текущей сделки:

//+------------------------------------------------------------------+
//| Возвращает сделку, на которую указывает индекс текущей сделки    |
//+------------------------------------------------------------------+
CDeal *CSymbolTrade::GetDealCurrent(void)
  {
   this.m_list_deals.Sort(SORT_MODE_DEAL_TIME_MSC);
   return this.GetDealByIndex(this.m_index_next_deal);
  }

Список сделок сортируется по времени в миллисекундах, и возвращается указатель на сделку, индекс которой записан в переменную класса m_index_next_deal.

Метод, возвращающий текущую цену 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;
  }

Получаем данные последнего тика в структуру цен m_tick и возвращаем из неё цену Bid.

Метод, возвращающий текущую цену 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;
  }

Получаем данные последнего тика в структуру цен m_tick и возвращаем из неё цену Ask.

Метод, возвращающий текущее время в секундах:

//+------------------------------------------------------------------+
//| 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;
  }

Получаем данные последнего тика в структуру цен m_tick и возвращаем из неё время.

Метод, возвращающий текущее время в миллисекундах:

//+------------------------------------------------------------------+
//| 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;
  }

Получаем данные последнего тика в структуру цен m_tick и возвращаем из неё время в миллисекундах.

Метод, открывающий длинную позицию:

//+------------------------------------------------------------------+
//| 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();
  }

В метод передаются параметры открываемой длинной позиции, в торговый объект устанавливается требуемый магик позиции и отсылается приказ на открытие длинной позиции с указанными параметрами. При ошибке открытия позиции, возвращается ноль, в случае успеха — тикет ордера, на основе которого была открыта позиция.

Метод, открывающий короткую позицию:

//+------------------------------------------------------------------+
//| 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();
  }

Аналогично предыдущему методу, но открывается короткая позиция.

Метод, закрывающий позицию по тикету:

//+------------------------------------------------------------------+
//| CSymbolTrade::Закрывает позицию по тикету                        |
//+------------------------------------------------------------------+
bool CSymbolTrade::ClosePos(const ulong ticket)
  {
   return this.m_trade.PositionClose(ticket);
  }

Возвращает результат вызова метода PositionClose() торгового объекта класса CTrade.

Торговый класс символа готов. Теперь реализуем его в советнике для работы с историческими сделками, сохранёнными в файле.


Разбираем в тестере историю сделок из файла

Перейдём к файлу советника \MQL5\Experts\TradingByHistoryDeals\TradingByHistoryDeals.mq5 и впишем временный объект только что созданного торгового класса символа — он потребуется для поиска нужного объекта в списке, в котором будут храниться указатели на такие объекты:

//+------------------------------------------------------------------+
//| 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;

У нас есть массив исторических сделок, на основе которого мы можем создать список торговых объектов, внутри которых будут списки сделок, принадлежащих символу объекта. В массиве сделок хранятся структуры, описывающие сделки. Так как в торговом объекте будут содержаться списки объектов сделок, нам нужно создать функцию, создающую новый объект сделки, и заполняющий свойства сделки из полей структуры, описывающей сделку:

//+------------------------------------------------------------------+
//| Создаёт объект сделки из структуры                               |
//+------------------------------------------------------------------+
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;
  }

В функцию передаётся структура сделки, создаётся новый объект сделки, и его свойства заполняются значениями из полей структуры.
Функция возвращает указатель на вновь созданный объект. При ошибке создания объекта, возвращается NULL.

Напишем функцию, создающую список торговых объектов символов:

//+------------------------------------------------------------------+
//| Создаёт массив используемых символов                             |
//+------------------------------------------------------------------+
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;
  }

Логика функции подробно расписана в комментариях. В цикле по списку исторических сделок анализируем каждую очередную сделку. Проверяем её символ и, если торгового объекта для этого символа ещё нет, — создаём новый торговый объект и сохраняем его в списке. Если уже есть — просто получаем указатель на торговый объект символа сделки из списка. Далее, точно так же проверяем наличие такой сделки в списке сделок торгового объекта и добавляем её в список при отсутствии. В результате прохода в цикле по всем историческим сделкам, получаем список торговых объектов по символам, в которых содержатся списки сделок, принадлежащих символу объекта.

Список торговых объектов можно вывести в журнал при помощи функции:

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

В цикле по списку торговых объектов символов получаем очередной объект и выводим в журнал его описание. Выглядит в журнале это примерно так:

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

У нас появился объект класса сделки. Добавим функцию, возвращающую описание сделки:

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

Эта функция полностью повторяет логику точно такой же функции, возвращающей описание структуры сделки. Но здесь вместо структуры сделки в функцию передаётся указатель на объект сделки.

Теперь оформим обработчик OnInit() до логического завершения.

Добавим обработку запуска советника в тестере, создадим список торговых объектов и обратимся к каждому символу, которые использовались в торговле для подгрузки их истории и открытия окон графиков этих символов в тестере:

//+------------------------------------------------------------------+
//| 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);
  }

В обработчике OnDeinit() советника очистим созданные массивы и списки:

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- очищаем созданные списки и массивы
   ExtListSymbols.Clear();
   ArrayFree(ExtArrayDeals);
  }

В обработчике OnTick() советника в тестере обрабатываем список сделок из файла:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- работаем только в тестере стратегий
   if(!MQLInfoInteger(MQL_TESTER))
      return;
      
//---  Обрабатываем в тестере список сделок из файла
   TradeByHistory(InpTestedSymbol, InpTestedMagic);
  }

Рассмотрим эту функцию подробнее. Вообще, логика обработки сделок истории изначально представлялась такой:

  1. получили время тика,
  2. получили сделку с таким временем,
  3. обрабатываем сделку в тестере.

На первый взгляд, простая и логичная схема при реализации потерпела полное фиаско. Дело в том, что в тестере не всегда время тика совпадает с временем сделки. Даже в миллисекундах. И при тестировании по всем тикам, на основе реальных тиков, взятых с того же самого сервера, сделки терялись. Время тика знаем, точно знаем, что на этом самом времени есть сделка, но тестер её не видит — нет тика с таким временем, как у сделки. Но есть тик со временем до-, и со временем после времени сделки. Соответственно, логика может быть построена не на тиках с их временем, а на сделках:

  1. сделки отсортированы в списке по времени их появления в миллисекундах. Устанавливаем индекс самой первой сделки как индекс текущей,
  2. выбираем сделку по индексу текущей сделки и получаем её время;
  3. ждём тик с таким временем:
    1. если время тика меньше времени сделки — ждём следующий тик,
    2. если время тика равно или больше времени сделки — обрабатываем сделку, записываем в неё факт того, что она уже обработана, и устанавливаем индекс следующей сделки как индекс текущей;
  4. пока тест не окончен, повторяем с пункта 2.

Такая схема позволяет дождаться времени каждой последующей сделки и исполнить её в тестере. При этом, на цену сделки внимания не обращаем — просто копируем сделки по наступлению их времени. Даже если время тика в тестере уже чуть позже, чем у реальной сделки — ничего страшного. Главное — скопировать торговлю. Фактом того, что сделка уже обработана тестером, будет ненулевое значение свойства сделки "тикет позиции в тестере". Если это значение равно нулю — значит, эта сделка ещё не обрабатывалась в тестере. После исполнения этой сделки в тестере, в это её свойство вписывается тикет позиции, к которой относится эта сделка в тестере.

Напишем функцию, в которой реализована описанная выше логика:

//+------------------------------------------------------------------+
//| Торговля по истории                                              |
//+------------------------------------------------------------------+
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();
        }
     }
  }

Этот код полностью копирует оригинальную торговлю, которая велась на счёте, и сделки которой записаны в файл. Все позиции открываются без стоп-приказов. То есть, значения для StopLoss и TakeProfit не копируются из реальных сделок в методы открытия позиций. Это упрощает отслеживание сделок, так как сделки закрытия тоже есть в списке, и тестер их отрабатывает, независимо от того, как была закрыта позиция — по StopLoss или по TakeProfit.

Скомпилируем советник и запустим его на графике. В итоге, будет создан файл HistoryDealsData.bin, в общей папке клиентских терминалов, по пути типа "C:\Users\UserName\AppData\Roaming\MetaQuotes\Terminal\Common\Files", в подпапке TradingByHistoryDeals, и будет выведен на график алерт с сообщением о желательных настройках тестера:

Давайте теперь запустим советник в тестере, выбрав в настройках тестера указанный диапазон дат, начальный депозит и плечо:

Тест запустим по всем проторгованным символам и магикам:

Получается, что вся торговля принесла нам минус 550 долларов. Интересно, а если бы мы ставили иные стоп-приказы?

Давайте проверим.


Скорректируем стоп-приказы

Сохраним советник в той же папке \MQL5\Experts\TradingByHistoryDeals\ под новым именем TradingByHistoryDeals_SLTP.mq5.

Добавим перечисление методов тестирования, входные параметры разобьём по группам, добавив группу для установки параметров стоп-приказов, и две новые переменные глобального уровня для передачи через них значений StopLoss и TakeProfit в торговые объекты:

//+------------------------------------------------------------------+
//|                                   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;

В обработчике OnInit() скорректируем и запишем в переменные значения стоп-приказов, устанавливаемые входными параметрами:

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

Добавим функции, рассчитывающие корректные значения для цен StopLoss и TakeProfit относительно уровня StopLevel, установленного для символа:

//+------------------------------------------------------------------+
//| Возвращает корректный 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);
  }

Для установки уровней StopLoss и TakeProfit, цена стоп-приказа не должна находиться ближе к текущей цене на расстояние StopLevel. Если уровень StopLevel для символа имеет нулевое значение, значит используется размер StopLevel, равный двум, иногда трём размерам спреда, установленного для символа. В этих функциях используется двойной множитель спреда. Эта величина передаётся в формальных параметрах функций, и имеет значение по умолчанию 2. При необходимости изменения значения множителя, нужно передать в функции, при их вызове, иное требуемое значение. Функции возвращают корректные цены для StopLoss и TakeProfit.

В функции торговли по истории сделок TradeByHistory() впишем новые блоки кода, где учитывается режим торговли в тестере и установка значений StopLoss и TakeProfit в случае, если выбрано тестирование с указанными значениями стоп-приказов. В блоке закрытия позиций закрывать их нужно только при типе тестирования "оригинальная торговля". Если выбрано тестирование с указанными значениями стоп-приказов, то сделки закрытия должны игнорироваться — тестер сам будет закрывать позиции по выставленным значениям StopLoss и TakeProfit. Единственное, что нужно делать при торговле со стоп-приказами в случае обработки сделок закрытия — это помечать их обработанными и переходить к следующей сделке.

//+------------------------------------------------------------------+
//| Торговля по истории                                              |
//+------------------------------------------------------------------+
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();
        }
     }
  }

В обработчике OnTester() советника посчитаем и вернём общее количество обработанных в тестере сделок:

//+------------------------------------------------------------------+
//| 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);
  }

Кроме того, здесь для каждого торгового объекта символа вызывается его собственный обработчик OnTester(), которые распечатывают в журнале свои данные. По окончании тестирования, в журнале тестера получим такие сообщения:

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

Скомпилируем советник и запустим его с теми же настройками тестирования, но указав тип тестирования как "Specified StopLoss and TakeProfit values", установив размеры StopLoss и TakeProfit равными 100 и 500 пунктов соответственно:

В прошлом тесте, при тестировании оригинальной торговли, мы получили убыток в 550 долларов. Сейчас, заменив StopLoss всех позиций на значение в 100 пунктов, а TakeProfit — на 500 пунктов, мы получили прибыль в 590 пунктов. И это при простой замене стоп-приказов, не глядя на специфику разных торгуемых символов. Если для каждого из проторгованных символов подобрать свои размеры стоп-приказов, то график тестирования, вполне вероятно, можно и выровнять.


Заключение

Сегодня мы провели небольшой эксперимент с торговой историей в стиле "А что бы было, если...". Думаю, такие эксперименты вполне могут натолкнуть на интересные решения по изменению своей торговли. И в следующей статье мы проведём ещё один такой эксперимент — подключим сюда различные трейлинги позиций. Будет интересно.

К статье прикреплены файлы всех рассмотренных сегодня советников и классов. Вы можете их скачать, изучить и самостоятельно поэкспериментировать со своими торговыми счетами. Папку с архивом можно сразу распаковать в свой каталог MQL5 клиентского терминала, и все файлы будут размещены в нужных подпапках.

Программы, используемые в статье:

#
Имя
 Тип Описание
1
SymbolTrade.mqh
Библиотека класса
Библиотека структуры и класса сделки, торговый класс символа
2
TradingByHistoryDeals.mq5
Советник
Советник для просмотра в тестере сделок и трейдов, осуществляемых на счёте
3
TradingByHistoryDeals_SLTP.mq5
Советник
Советник для просмотра и изменения при помощи StopLoss и TakeProfit в тестере сделок и трейдов, осуществляемых на счёте
4
MQL5.zip
Архив
Архив файлов, представленных выше, для распаковки в каталог MQL5 клиентского терминала


Прикрепленные файлы |
SymbolTrade.mqh (54.77 KB)
MQL5.zip (23.57 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (16)
fxsaber
fxsaber | 31 янв. 2025 в 15:32
Artyom Trishkin #:

В чём ошибка?

Если файл меньше, чем массив до чтения, то размер массива не изменится.

На подобном можно попасть и при использовании ArrayCopy.
Artyom Trishkin
Artyom Trishkin | 31 янв. 2025 в 15:34
fxsaber #:
Хорошую функцию игнорируете

В чём преимущество?

fxsaber
fxsaber | 31 янв. 2025 в 15:36
Artyom Trishkin #:

В чём преимущество?

В лаконичности и быстроте выполнения (полностью на стороне MQ).

fxsaber
fxsaber | 31 янв. 2025 в 15:52
Artyom Trishkin #:

Покажите стандартные самораспечатывающиеся и самозаполняющиеся структуры, пожалуйста.

Почти стандартная (полями MQ поделились).
Artyom Trishkin
Artyom Trishkin | 31 янв. 2025 в 16:11
fxsaber #:

В лаконичности и быстроте выполнения (полностью на стороне MQ).

Спасибо. Пропустил
Построение модели ограничения тренда свечей (Часть 7): Улучшаем нашу модель для разработки советника Построение модели ограничения тренда свечей (Часть 7): Улучшаем нашу модель для разработки советника
В этой статье мы подробно рассмотрим подготовку нашего индикатора для разработки советника. В ходе обсуждения будут рассмотрены дальнейшие усовершенствования текущей версии индикатора с целью повышения его точности и функциональности. Кроме того, мы внедрим новые функции, которые будут отмечать точки выхода, устранив ограничение предыдущей версии, которая определяла только точки входа.
От начального до среднего уровня: Операторы От начального до среднего уровня: Операторы
В этой статье мы рассмотрим основных операторов. Хотя тема проста для понимания, есть определенные моменты, которые имеют большое значение, когда речь идет о включении математических выражений в формат кода. Без адекватного понимания этих деталей, программисты с небольшим опытом или вообще без него в итоге отказываются от попыток создать собственных решений.
Нейросети в трейдинге: Контекстно-зависимое обучение, дополненное памятью (Окончание) Нейросети в трейдинге: Контекстно-зависимое обучение, дополненное памятью (Окончание)
Мы завершаем реализацию фреймворка MacroHFT для высокочастотной торговли криптовалютами, который использует контекстно-зависимое обучение с подкреплением и памятью для адаптации к динамичным рыночным условиям. И в завершении данной статьи будет проведено тестирование реализованных подходов, на реальных исторических данных, для оценки их эффективности.
Алгоритм циклического партеногенеза — Cyclic Parthenogenesis Algorithm (CPA) Алгоритм циклического партеногенеза — Cyclic Parthenogenesis Algorithm (CPA)
В данной статье рассмотрим новый популяционный алгоритм оптимизации CPA (Cyclic Parthenogenesis Algorithm), вдохновленный уникальной репродуктивной стратегией тлей. Алгоритм сочетает два механизма размножения — партеногенез и половое, а также использует колониальную структуру популяции с возможностью миграции между колониями. Ключевыми особенностями алгоритма являются адаптивное переключение между различными стратегиями размножения и система обмена информацией между колониями через механизм перелета.