
Visual assessment and adjustment of trading in MetaTrader 5
Contents
- Introduction
- Saving deal history to file
- Analyzing deal history from the file in the tester
- Adjusting stop orders
- Conclusion
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:
- get the tick time,
- get a deal with such a time,
- 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:
- 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,
- select a deal by the current deal index and get its time;
- wait for a tick with this time:
- if the tick time is less than the deal time, wait for the next tick,
- 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;
- 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
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
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.You're ignoring a good feature
What's the advantage?
What's the advantage?
Conciseness and speed of execution (totally on the MQ side).
Show the standard self-printing and self-filling structures, please.
In brevity and speed of execution (totally on the MQ side).