Русский Español Português
preview
Visual assessment and adjustment of trading in MetaTrader 5

Visual assessment and adjustment of trading in MetaTrader 5

MetaTrader 5Examples |
386 16
Artyom Trishkin
Artyom Trishkin

Contents



Introduction

Let's imagine a situation: on some account, more or less active trading has been conducted for a fairly long time on various instruments using different EAs and, in some cases, even manually. And now, after some time, we want to see the results of all this work. Naturally, we can view standard trading reports in the terminal by pressing the Alt+E key combination. We can also load deal icons onto our chart and see entry and exit times for our positions. But what if we want to see the dynamics of our trading, where and how positions were opened and closed? We can view each symbol separately, or all at once, including the opening and closing of positions, the levels at which stop orders were placed, and whether their size was justified. What if we then ask ourselves the question "what would have happened if..." (and there are many options here — different stops, using different algorithms and criteria, using trailing positions, or moving stops to breakeven, etc.) and then test all of our "ifs" with a clear, visible result. How might trading change if...

It turns out that everything is already in place to solve such issues. All we need to do is load the account history into a file — all completed deals — and then run an EA in the strategy tester that reads deals from the file and opens/closes positions in the client terminal's strategy tester. With such an EA, we can add code to it to change the conditions for exiting positions and compare how the trading changes, and what would have happened if...

How can that be of use for us? Another tool for finding the best results and making adjustments to trading that has been running on an account for some time. Visual testing allows us to dynamically see whether positions on a particular instrument were opened correctly, whether they were closed at the right time, etc. And most importantly, a new algorithm can simply be added to the EA's code, tested, the results obtained, and adjustments made to the EAs working on this account.

Let's create the following logic for the EA's behavior:

  • if the EA is launched on the chart of any instrument, it will collect the entire history of deals on the current account, save all deals in one file, and then do nothing;
  • If the EA is launched in the tester, it will read the deal history recorded in the file and, during the test, will repeat all deals from the file, opening and closing positions.

Thus, the EA first prepares a file of trading history (when running it on a chart), and then executes deals from the file, completely repeating trading on the account (when running it in the strategy tester).

Next, we will make modifications to the EA to be able to set different StopLoss and TakeProfit values for positions opened in the tester.



Saving deal history to file

In the \MQL5\Experts\ terminal directory, create a new folder TradingByHistoryDeals containing a new EA file named TradingByHistoryDeals.mq5.

The EA should have the ability to select which symbol and which magic number to test. If several EAs for several symbols or magic numbers were working on the account, then we can select in the settings which symbol or magic number we are interested in (or all at once).

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

The default values for the symbol and magic number are the empty string and -1. With these values, the EA will not sort the trading history by symbol or magic number — the entire trading history will be tested. The third string tells the EA to output (or not) descriptions of all deals saved in the file to the journal so that we can clearly verify the accuracy of the saved data.

Each deal is a whole set of different parameters described by different deal properties. The simplest thing is to write down all the deal properties in a structure. To write a large number of deals to a file, it is necessary to use an array of structures. Then we save this array to the file. The MQL5 language has everything for this. The logic for saving the deal history to file will be as follows:

  • move through a loop of historical deals;
  • receive the next deal and write its data into the structure;
  • save the created deal structure in the deal array;
  • at the end of the loop, save the prepared array of structures to the file.

All additional codes — structures, classes, enumerations — will be written in a separate file. Let's name it after the future class of the symbol trading object.

In the same folder, create a new included file named SymbolTrade.mqh.

Let's implement the macro substitutions for the name of the folder to contain the history file, file name and file path, and include all the necessary Standard Library files to it:

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

Next, we will write the deal structure:

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

Since we will save the deal structures to a file, and only simple type structures can be written to a file (see FileWriteArray()), then all string variables should be replaced with ushort arrays and methods for writing and returning structure string properties should be created.

The created structure is only necessary for saving the deal history to a file and reading the recorded history from the file. In the EA itself, a list of objects will be created, in which the objects of the deal class will be stored. To search for the required deal in the list and sort the array, we will need to specify the deal property, by which it will be searched in the list. To search for a deal, the list of objects should be sorted by the desired property.

Let's write a list of all the properties of the deal object, by which it will be possible to perform a search:

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

Here, in addition to the standard deal properties, there are two more: the deal ticket in the tester and the position ID in the tester. The point is that we will be trading in the tester based on data from real deals, while positions opened in the tester and, accordingly, their corresponding deals have a completely different ticket and ID in the tester. In order to be able to compare a real deal with a deal in the tester (as well as the ID), we will need to save the ticket and the position ID in the tester in the properties of the deal object, and then, using this saved data, compare the deal in the tester with the real deal in the history.

Let's stop with this file for now and move on to the EA file created a little earlier. Let's add the array of structures where we will add the structures of all deals in the history:

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

And we will write functions for working with historical deals.

Function that saves the deal history to an array:

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

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

The function code is thoroughly commented. Select the entire trading history from the beginning to the current time, obtain each successive historical deal, save its properties in the structure fields, and save the structure variable in an array. At the end of the loop through the deal history, return the size of the resulting array of deals. To monitor the progress of recording deals into an array, we can print each handled deal in the journal. To do this, we need to specify the logs flag equal to true when calling the function.

The function that prints all deals from an array of deals to the journal:

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

Let's implement several functions to display the deal description in the journal.

The function that returns a description of the transaction type:

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

Depending on the deal type passed to the function, the corresponding string is displayed.

The function returning the position change method description:

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

Depending on the method of changing the position passed to the function, the corresponding string is displayed.

The function that returns a deal description:

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

If this is a balance sheet deal, then a description is displayed in the form

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

Otherwise, the deal description is displayed in a different format:

    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

The function that prints a deal description in the journal:

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

Everything is clear here - we simply print the string obtained from the DealDescription() function.

Let's write functions for writing and reading an array of deals to/from a file.

The function to open a file for writing:

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

The function opening a file for reading:

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

The functions open a file for reading/writing. In formal parameters, a variable the file handle is written to is passed by reference. Return true upon successful opening of the file and false in case of an error.

The function that saves deal data from an array to the file:

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

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

The function receives an array of structures, which must be saved in a file. The variable for receiving the size of the created file is passed by reference in the formal parameters of the function. We open the file, move the file pointer to the end of the file, and write data from the array of structures to the file, starting from the pointer. When writing is complete, the file is closed.

After the array of deal structures is saved to the file, all deals can be read back into the array from this file, and then used to create lists of deals and work with them in the tester.

The function that loads deal data from the file into the array:

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

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

Based on the functions created above, we will write a function for reading the history of deals and writing them to the file.

The function that prepares a file with historical deals:

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

The comments in the code make the logic clear. The function is launched in the OnInit() handler and prepares the file with trades for further work:

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

In addition to saving all historical trades to a file, an alert is also displayed with a message about the recommended tester settings — initial balance, leverage, and the test start time, corresponding to the date of the first balance trade. For example, it may look like this:

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

Such tester settings will provide the final result in the tester closest to that obtained in reality.

The structure of the deal set in the \MQL5\Experts\TradingByHistoryDeals\SymbolTrade.mqh file is meant only for saving the deal to the file and for reading the saved history from the file. To continue our work, we need to create a deal class, the objects of which will be stored in lists. The lists themselves are stored in the trade class objects for the tester. In turn, trading objects are also class objects that will be stored in their own list. Each trading object will be determined by its belonging to a specific symbol - the number of symbols involved in trading will determine the number of trading objects. The trading objects themselves will contain a list of transactions only for their symbol and their own CTrade class objects of the Standard Library. This will allow each CTrade class trading object to be customized to the conditions of the symbol being traded.

Let's write a deal class in the \MQL5\Experts\TradingByHistoryDeals\SymbolTrade.mqh file.

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

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

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

The class almost completely repeats the previously created deal structure. In addition to the deal properties, additional properties have been added - Digits and Point of the symbol the deal was carried out for. This simplifies the output of the deal description, since this data is set in the deal constructor immediately when the object is created, which eliminates the need to obtain these properties for each deal (if they are needed) when accessing it.
Also, a virtual method Compare() is created here for comparing two deal objects - it will be used when sorting the lists of deals to find the desired deal by the specified property.

Now let's create a trading symbol class. The class will store a list of deals carried out using the symbol set in the object properties, and the tester will request these deals from it for copying. In general, this class will be the basis for copying trades made on an account by symbol in the strategy tester:

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

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

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

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

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

Let's look at some methods.

  • SetNumProcessedDeals() and NumProcessedDeals() set and return the number of historical deals already handled by the tester from the list of deals obtained from the file. They are necessary to handle the validity of handling historical deals and to obtain final statistics on the number of deals handled by the tester;
  • GetDealCurrent() returns a pointer to the current historical deal that needs to be handled by the tester and marked as handled;
  • DealCurrentIndex() returns the index of the historical deal currently selected for handling by the tester;
  • SetNextDealIndex() after completing the handling of the current historical deal sets the index of the next deal to be handled by the tester. Since all historical deals in the list are sorted by time in milliseconds, this will set the index of the next deal after the tester has completed handling the previous one. In this way, we will sequentially select all the deals in history that will be handled by the tester at the moment of time set in the properties of the currently selected deal;
  • CheckTime() checks the moment of occurrence of the time, set in the properties of the current historical deal, in the tester. The logic is as follows: there is a selected deal that needs to be handled in the tester. As long as the time in the tester is less than the time recorded in the deal, we do nothing - we just go to the next tick. As soon as the time in the tester becomes equal to or greater than the time in the currently selected deal (the time in the tester may not coincide with the time in the deal, so the time is also checked for "greater"), the deal is handled by the tester depending on its type and the way the deal changes the position. Next, this transaction is marked as handled, the index of the next deal is set, and waiting, managed by the method, continues, albeit for the next deal:
  • The OnTester() handler is called from the standard EA OnTester() handler, displays the symbol name in the journal, as well as the number of historical deals as well as the ones handled by the tester, and returns the number of handled deals by trading object symbol.

The class has two constructors - default and parametric.

In the formal parameters of the parametric constructor, the trading object symbol name is passed to be used when creating the object, while CTrade class trading object receives the margin calculation mode, according to the current account settings, as well as the order filling type, according to the settings of the trading object symbol:

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

The method that adds a deal to the deals array:

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

The pointer to the deal object is passed to the method. If a deal with such a ticket is already in the list, true is returned. Otherwise, the list is sorted by the deal time in milliseconds, and the deal is added to the list in sorting order by time in ms.

The method that returns a pointer to a deal object by time in seconds:

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

The method receives the required time. We set the time passed to the method to the temporary deal object, the list is sorted by time in milliseconds, and the index of the deal whose time is equal to the time passed to the method (set to the temporary object) is searched for. Next, a pointer to the deal in the list by the found index is returned. If there is no deal with such time in the list, then the index will be equal to -1, while NULL will be returned from the list.

It is interesting that the deal is searched by time in seconds, but we sort the list by time in milliseconds. Tests have shown that if the list is also sorted in seconds, then some deals are not included in it, although they definitely exist. This is most likely due to the fact that there are several trades in one second with time in milliseconds. Besides, the pointer to a previously handled deal is returned, since several transactions have the same time in seconds.

The method returning the pointer to the open trade by position ID:

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

The method receives the ID of the position whose opening trade needs to be found. Next, in a loop through the list of deals, we get the deal whose position ID is equal to the one passed to the method, and return a pointer to the deal whose position change method is equal to "Market Entry" (DEAL_ENTRY_IN).

The method that returns the pointer to the deal object by index in the list:

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

We simply return the pointer to the object in the list by the index passed to the method. If the index is incorrect, NULL is returned.

The method returning the pointer to the deal the current deal index points at:

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

The list of deals is sorted by time in milliseconds, and the pointer to the deal, whose index is written to the m_index_next_deal class variable, is returned.

The method returning the current Bid price:

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

We get the data of the last tick into the m_tick price structure and return the Bid price from it.

The method returning the current Ask price:

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

We get the data of the last tick into the m_tick price structure and return the Ask price from it.

The method returning the current time in seconds:

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

We get the data of the last tick into the m_tick price structure and return time from it.

The method returning the current time in milliseconds:

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

We get the data of the last tick into the m_tick price structure and return time in milliseconds from it.

The method for opening a long position:

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

The method receives the parameters of the opened long position, the required position magic number is set to the trading object and the order for opening a long position with the specified parameters is sent. Failure to open a position returns zero, while success yields the order ticket the opened position is based on.

The method for opening a short position:

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

Similar to the previous method but a short position is opened.

The method for closing a position by ticket:

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

Return the result of calling the PositionClose() method of the CTrade class trading object.

The trading symbol class is ready. Now let's implement it in the EA for handling historical deals saved in the file.


Analyzing deal history from the file in the tester

Let's move on to the \MQL5\Experts\TradingByHistoryDeals\TradingByHistoryDeals.mq5 EA file and add a temporary object of the newly created trading symbol class — it will be needed to find the desired object in the list storing the pointers to such objects:

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

We have an array of historical deals, based on which we can create a list of trading objects, inside which there will be lists of deals belonging to the object symbol. The deal array stores structures that describe deals. Since the trading object will contain lists of deal objects, we need to create a function that creates a new deal object and populates the deal properties from the fields of the structure describing the deal:

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

The function receives the deal structure, a new deal object is created, and its properties are filled with values from the structure fields.
The function returns a pointer to the newly created object. If an error occurs in creating the object, it returns NULL.

Let's write a function that creates a list of trading symbol objects:

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

The logic of the function is described in detail in the comments. Analyze each successive deal in a loop through the list of historical deals. Check its symbol and, if there is no trading object for this symbol yet, create a new trading object and save it in the list. If it already exists, we simply get the pointer to the symbol trading object of a deal from the list. Next, check in the same way for the presence of such a deal in the list of deals of the trading object and add it to the list if it is not there. As a result of looping through all historical deals, obtain a list of trading objects by symbols, which contain lists of deals belonging to the object symbol.

The list of trading objects can be sent to the journal using the following function:

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

In a loop through the list of trading symbol objects, obtain the next object and display its description in the journal. In the journal, this looks something like this:

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

We now have a deal class object. Add the function that returns a deal description:

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

This function completely repeats the logic of the exact same function that returns a description of the deal structure. But here the pointer to the deal object is passed to the function instead of the deal structure.

Now let's create the OnInit() handler up to the logical conclusion.

Add handling the EA launch in the tester, create the list of trading objects and access each symbol that was used in trading to load their history and open chart windows for these symbols in the tester:

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

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

In the EA OnDeinit() handler, clear the created arrays and lists using the EA:

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

Handle the list of deals from the file in the EA's OnTick() handler in the tester:

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

Let's consider this function in more details. In general, the logic for handling historical deals was initially presented as follows:

  1. get the tick time,
  2. get a deal with such a time,
  3. handle a deal in the tester.

At first glance, the simple and logical structure failed completely when implemented. The thing is that in the tester the tick time does not always coincide with the deal time. Even in milliseconds. As a result, when testing on all ticks, based on real ticks taken from the same server, trades were lost. We might know the tick time, we might know for sure that there is a deal at this very time, but the tester does not see it - there is no tick with the same time as the deal. But there is a tick with time before and with time after the deal time. Accordingly, the logic can be built not around ticks and their timing, but around deals:

  1. deals are sorted in the list by the time they appeared in milliseconds. Set the index of the very first deal as the index of the current one,
  2. select a deal by the current deal index and get its time;
  3. wait for a tick with this time:
    1. if the tick time is less than the deal time, wait for the next tick,
    2. if the tick time is equal to or greater than the deal time, we handle the deal, register the fact that it has already been handled, and set the index of the next deal as the index of the current one;
  4. repeat from point 2 until the test is finished.

This structure allows us to wait for the time of each subsequent deal and execute it in the tester. In this case, we do not pay attention to the deal price – we simply copy deals when their time comes. Even if the tick time in the tester is a little later than that of the real deal, it is fine. The main thing is to copy trading. The fact that the deal has already been handled by the tester will be indicated by a non-zero value of the deal "position ticket in tester" property. If this value is zero, it means that this deal has not yet been handled in the tester. After this deal is executed in the tester, the ticket of the position this deal belongs to in the tester is entered into this property.

Let's add the function that implements the logic described above:

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

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

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

This code completely copies the original trading conducted on the account whose deals are registered in the file. All positions are opened without stop orders. In other words, the values for StopLoss and TakeProfit are not copied from real deals to the position opening methods. This simplifies tracking deals, as closing deals are also listed, and the tester handles them, regardless of how the position was closed — by StopLoss or TakeProfit.

Compile the EA and launch it on the chart. As a result, the HistoryDealsData.bin file will be created in the shared folder of the client terminals, along the path looking like "C:\Users\UserName\AppData\Roaming\MetaQuotes\Terminal\Common\Files", in the TradingByHistoryDeals subfolder, and an alert will be displayed on the chart with a message about the desired tester settings:

Let's now run the EA in the tester, selecting the specified date range, initial deposit, and leverage in the tester settings:

Run the test on all traded symbols and magic numbers:

It turns out that the entire trading brought us a loss of USD 550. I wonder what would happen if we set different stop orders?

Let's check this out.


Adjusting stop orders

Save the EA in the same folder \MQL5\Experts\TradingByHistoryDeals\ as TradingByHistoryDeals_SLTP.mq5.

Add enumeration of testing methods and divide inputs by groups by adding a group for setting stop order parameters, as well as two new variables of global level for passing StopLoss and TakeProfit values to trading objects through them:

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

#include "SymbolTrade.mqh"

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

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

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

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


In the OnInit() handler, adjust and write into variables the values of stop orders set by the inputs:

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

Add the functions that calculate the correct values for the StopLoss and TakeProfit prices relative to the StopLevel set for the symbol:

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

To set StopLoss and TakeProfit levels, the stop order price should not be closer to the current price by the StopLevel distance. If the StopLevel for a symbol has zero value, then the StopLevel size used is equal to two or sometimes three spreads set for the symbol. These features use a double spread multiplier. This value is passed in formal parameters of functions, and has a default value of 2. If it is necessary to change the value of the multiplier, we need to pass a different required value to the functions when calling them. The functions return the correct prices for StopLoss and TakeProfit.

In the TradeByHistory() deal history trading function, insert new code blocks considering trading mode in the tester and setting StopLoss and TakeProfit values if testing with the specified stop order values is selected. In the position closing block, we only need to close positions if the testing type is "original trading". If testing with specified stop order values is selected, closing deals should be ignored - the tester will automatically close positions based on the specified StopLoss and TakeProfit values. The only thing we need to do when trading with stop orders if closing trades are handled is to mark them as handled and move on to the next deal.

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

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

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

In the Ea's OnTester() handler, calculate and return the total number of deals handled in the tester:

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

In addition, each symbol trading object has its own OnTester() handler called here, which prints its data in the journal. At the end of testing, we will receive the following messages in the tester log:

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

Compile the EA and run it with the same testing settings, but specify the testing type as "Specified StopLoss and TakeProfit values", setting the StopLoss and TakeProfit values to 100 and 500 points, respectively:

In the last test, when testing the original trading, we had a loss of USD 550. Now, by replacing StopLoss for all positions with a value of 100 points, and TakeProfit with 500 points, we received a profit of 590 points. This was achieved with a simple replacement of stop orders, without looking at the specifics of the different symbols being traded. If we select our own stop order sizes for each of the traded symbols, then the test graph can most likely be leveled out.


Conclusion

In this article, we conducted a small experiment with a trading history in the style of "What if...". I think such experiments could well lead to interesting solutions aimed at changing one's trading style. In the next article, we will conduct another experiment of that sort by including various position trailing stops. Things are about to get more interesting.

All the EAs and classes discussed here are attached below. You can download and study them, as well as experiment with them on your own trading account. You can immediately unzip the archive folder into your MQL5 client terminal directory, and all files will be placed in the required subfolders.

Programs used in the article:

#
Name
 Type Description
1
SymbolTrade.mqh
Class library
Deal structure and class library, symbol trading class
2
TradingByHistoryDeals.mq5
Expert Advisor
The EA for viewing deals and trades performed on an account in the tester
3
TradingByHistoryDeals_SLTP.mq5
Expert Advisor
The EA for viewing and modifying deals and trades performed on the account in the tester using StopLoss and TakeProfit
4
MQL5.zip
ZIP archive
The archive of files presented above can be unpacked into the MQL5 directory of the client terminal


Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/16952

Attached files |
SymbolTrade.mqh (53.86 KB)
MQL5.zip (22.3 KB)
Last comments | Go to discussion (16)
fxsaber
fxsaber | 31 Jan 2025 at 15:32
Artyom Trishkin #:

What's the mistake?

If the file is smaller than the array before reading, the array size will not change.

You can get into a similar error when using ArrayCopy.
Artyom Trishkin
Artyom Trishkin | 31 Jan 2025 at 15:34
fxsaber #:
You're ignoring a good feature

What's the advantage?

fxsaber
fxsaber | 31 Jan 2025 at 15:36
Artyom Trishkin #:

What's the advantage?

Conciseness and speed of execution (totally on the MQ side).

fxsaber
fxsaber | 31 Jan 2025 at 15:52
Artyom Trishkin #:

Show the standard self-printing and self-filling structures, please.

Almost standard (MQ fields shared).
Artyom Trishkin
Artyom Trishkin | 31 Jan 2025 at 16:11
fxsaber #:

In brevity and speed of execution (totally on the MQ side).

Thanks. Missed
Reimagining Classic Strategies (Part 16): Double Bollinger Band Breakouts Reimagining Classic Strategies (Part 16): Double Bollinger Band Breakouts
This article walks the reader through a reimagined version of the classical Bollinger Band breakout strategy. It identifies key weaknesses in the original approach, such as its well-known susceptibility to false breakouts. The article aims to introduce a possible solution: the Double Bollinger Band trading strategy. This relatively lesser known approach supplements the weaknesses of the classical version and offers a more dynamic perspective on financial markets. It helps us overcome the old limitations defined by the original rules, providing traders with a stronger and more adaptive framework.
Automating Trading Strategies in MQL5 (Part 35): Creating a Breaker Block Trading System Automating Trading Strategies in MQL5 (Part 35): Creating a Breaker Block Trading System
In this article, we create a Breaker Block Trading System in MQL5 that identifies consolidation ranges, detects breakouts, and validates breaker blocks with swing points to trade retests with defined risk parameters. The system visualizes order and breaker blocks with dynamic labels and arrows, supporting automated trading and trailing stops.
Building a Professional Trading System with Heikin Ashi (Part 2): Developing an EA Building a Professional Trading System with Heikin Ashi (Part 2): Developing an EA
This article explains how to develop a professional Heikin Ashi-based Expert Advisor (EA) in MQL5. You will learn how to set up input parameters, enumerations, indicators, global variables, and implement the core trading logic. You will also be able to run a backtest on gold to validate your work.
From Novice to Expert: Backend Operations Monitor using MQL5 From Novice to Expert: Backend Operations Monitor using MQL5
Using a ready-made solution in trading without concerning yourself with the internal workings of the system may sound comforting, but this is not always the case for developers. Eventually, an upgrade, misperformance, or unexpected error will arise, and it becomes essential to trace exactly where the issue originates to diagnose and resolve it quickly. Today’s discussion focuses on uncovering what normally happens behind the scenes of a trading Expert Advisor, and on developing a custom dedicated class for displaying and logging backend processes using MQL5. This gives both developers and traders the ability to quickly locate errors, monitor behavior, and access diagnostic information specific to each EA.