在 MetaTrader 5 中交易的可视评估和调整
内容
概述
我们想象一种情况:在某些账户上,使用不同的 EA 针对各种金融产品或多或少地进行活跃交易,在某些情况下,甚至要手动。现在,一段时间后,我们打算看看所有这些工作的成果。自然,我们能在终端中按下 Alt+E 组合键来查看标准交易报告。我们还能将成交图标加载到图表上,查看我们持仓的入场和离场时间。但如果我们打算看看交易的动态,如何处、以及如何开仓和平仓的,该怎么办呢?我们能够分别审查每个品种,也可以一次查看所有,包括开仓和平仓、下止损单的价位、以及它们的规模是否合理。如果我们问自己一个问题,“如果这样...会发生什么”。(此处有很多选择 — 不同的止损、运用不同的算法和准则、使用持仓尾随、或将止损移动到盈亏平衡点、等等。然后测试我们所有的“如果”,得到清晰、直观的结果。交易或许如何变化,如果...
事实证明,解决这种问题的一切都已就位。所有我们要做的就是将账户历史记录加载到一个文件当中 — 所有已完结的成交 — 然后在策略测试器中运行一个 EA,其从文件中读取成交,并在客户端的策略测试器中开仓/平仓。依靠这样的 EA,我们能够往其中添加代码来更改持仓离场的条件,并比较交易如何变化,以及会发生什么,如果...
这能为我们起什么作用?有另一款的工具,能够在账户上寻找运行了一段时间交易的最佳结果,并进行调整。可视测试令我们能够动态查看特定金融产品的持仓是否正确开仓、是否在正确的时间平仓等等。最重要的是,新算法能被简单地将添加到 EA 的代码之中,测试、获得结果,并调整正在该帐户内工作的 EA。
我们为 EA 的行为创建以下逻辑:
- 如果 EA 在任何金融产品的图表上启动,它将收集当前账户上的全部成交历史记录,把所有成交保存到一个文件当中,然后什么都不做;
- 如果在测试器中启动 EA,它将读取文件中记录的成交历史记录,并在测试期间重现文件中的所有成交,开仓和平仓。
因此,EA 首先准备一个交易历史文件(当在图表上运行时),然后从该文件中执行成交,完全重现账户上的交易(当在策略测试器中运行时)。
接下来,我们将修改 EA,以便能够在测试器中为所开持仓设置不同的止损和止盈值。
保存成交历史记录到文件
在终端目录 \MQL5\Experts\,创建一个新文件夹 TradingByHistoryDeals,其中包含一个名为 TradingByHistoryDeals.mq5 的新 EA 文件。
EA 应当有能力选择测试品种和魔幻数字。如果交易若干品种或魔幻数字的多个 EA 正在账户上工作,那么我们可以在设置中选择我们感兴趣的品种或魔幻数字(或一次性全部)。
//+------------------------------------------------------------------+ //| TradingByHistoryDeals.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| Expert | //+------------------------------------------------------------------+ //--- input parameters input string InpTestedSymbol = ""; /* The symbol being tested in the tester */ input long InpTestedMagic = -1; /* The magic number being tested in the tester */ sinput bool InpShowDataInLog = false; /* Show collected data in the log */
品种和魔幻数字的默认值是空字符串和 -1。按照默认值,EA 就不会按品种或魔幻数字对交易历史排序 — 即整个交易历史都要测试。第三个字符串告诉 EA 将文件中保存的所有成交的描述输出(或不输出)到日志中,如此这般我们就能清晰地验证所保存数据的准确性。
每笔成交都是一整套不同参数,由不同成交属性描述。最简单的事情就是在结构中写下所有成交属性。为了将大量成交写入文件,必须用到结构数组。然后我们将该数组保存到文件之中。MQL5 语言拥有应对这些的一切。将成交历史保存到文件的逻辑如下:
- 循环移动遍历历史成交;
- 接收下一笔成交,并将其数据写入结构;
- 将曾创建的成交结构保存在成交数组之中;
- 在循环结束时,将准备就绪的结构数组保存到文件之中。
所有其它代码(结构、类、枚举)— 都被写入分离的文件之中。我们稍后会以正交易品种对象的名字命名它。
在同一文件夹中,创建一个名为 SymbolTrade.mqh 的新包含文件。
我们来实现文件夹名称的宏替换,以包含历史文件、文件名和文件路径,并包含所有必要的标准库文件:
//+------------------------------------------------------------------+ //| SymbolTrade.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #define DIRECTORY "TradingByHistoryDeals" #define FILE_NAME "HistoryDealsData.bin" #define PATH DIRECTORY+"\\"+FILE_NAME #include <Arrays\ArrayObj.mqh> #include <Trade\Trade.mqh>
接下来,我们将编写成交结构:
//+------------------------------------------------------------------+ //| 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)); } };
鉴于我们会把成交结构保存到一个文件之中,并且仅能将简单的类型结构写入文件(参见 FileWriteArray()),因此所有字符串变量都应替换为 ushort 数组,并且还应创建编写和返回结构字符串属性的方法。
创建的结构仅保存成交历史记录到文件,并从文件中读取记录在案的成交历史。在 EA 本身中,将创建一个对象列表,其中存储成交类的对象。为了在列表中搜索所需的成交,并对数组排序,我们需要指定成交属性,按该属性在列表中搜索它。为了搜索一笔成交,对象列表应按所需属性排序。
我们编写一个成交对象所有属性的列表,按该列表可以执行搜索:
//--- 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 };
此处,除了标准成交属性外,还多了两个属性:测试器中的成交票根,和仓位 ID。关键点是我们将基于真实成交的数据在测试器中进行交易,而在测试器中开仓,相应地,它们的成交在测试器中拥有完全不同的票根和 ID。为了能够将真实成交与测试器中的成交(以及 ID)进行比较,我们需要将票根和仓位 ID 保存在测试器中的成交对象属性当中,然后用这些保存的数据,将测试器中的成交,与历史记录中的真实成交进行比较。
我们先暂停这个文件,转至稍早前创建的 EA 文件。我们来添加结构数组,我们将在其中添加历史中所有成交的结构:
//--- 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[]={};
我们将编写处理历史成交的函数。
将成交历史保存到数组的函数:
//+------------------------------------------------------------------+ //| 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(); }
函数代码通体注释。选择从开始到当前时间的整个交易历史,获取逐笔历史成交,将其属性保存在结构字段之中,并将结构变量保存到数组。在循环遍历交易成交结束时,返回生成的成交数组的大小。为了监控成交记录到数组中的进度,我们可在流水账中打印每笔处理过的成交。为此,我们需要在调用函数时指定 logs 标志等于 true。
从交易数组打印所有成交到流水账的函数:
//+------------------------------------------------------------------+ //| 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); } }
我们实现若干函数,在流水账中显示成交描述。
返回业务类型描述的函数:
//+------------------------------------------------------------------+ //| 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; } }
根据传递给函数的成交类型,显示相应的字符串。
该函数返回仓位变更的说明:
//+------------------------------------------------------------------+ //| 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; } }
根据传递给函数的仓位变更方法,显示相应的字符串。
返回成交描述的函数:
//+------------------------------------------------------------------+ //| 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))); }
如果这是余额单据成交,则描述将显示在表格中
0: deal #190715988 Entry In, type Balance 3000.00 USD at 2024.09.13 21:48
否则,成交描述将以不同的格式显示:
1: deal #190724678 Entry In, type Buy, Position #225824633 USDCHF (magic 600), Price 0.84940 at 2024.09.13 23:49:03, sl 0.84811, tp 0.84983
在流水账中打印成交描述的函数:
//+------------------------------------------------------------------+ //| Print deal data in the journal | //+------------------------------------------------------------------+ void DealPrint(SDeal &deal, const int index) { Print(DealDescription(deal, index)); }
于处一切都很清晰 — 我们简单地打印自 DealDescription() 函数获得的字符串。
我们编写成交数组至文件相互写入/读取的函数。
该函数打开写入文件:
//+------------------------------------------------------------------+ //| 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; }
该函数打开读取文件:
//+------------------------------------------------------------------+ //| 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; }
该函数打开一个可读/可写文件。在形式参数中,准备写入的文件句柄变量通过引用传递。成功打开文件后返回 true,如果出现错误,则返回 false。
该函数将成交数据从数组保存到文件:
//+------------------------------------------------------------------+ //| 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; }
该函数接收一个结构数组,必须将其保存在文件之中。接收已创建文件大小的变量在函数的形参中通过引用传递。我们打开文件,将文件指针移到文件末尾,然后从指针开始处将数据结构数组写入文件。写入完成后,文件将被关闭。
将成交结构数组保存到文件后,所有成交都可从该文件读回至数组,然后创建成交列表,并在测试器中据其操作。
该函数将成交数据从文件加载到数组当中:
//+------------------------------------------------------------------+ //| 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; }
基于上面创建的函数,我们将编写一个函数来读取成交历史,并将它们写入文件。
该函数准备历史成交文件:
//+------------------------------------------------------------------+ //| 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; }
代码中的注释令逻辑更清晰。该函数在 OnInit() 事件处理程序中启动,并准备包含交易的文件,以备进一步的操作:
//+------------------------------------------------------------------+ //| 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); }
除了将所有历史交易保存到文件之中,还会显示一条警报,其中包含有关测试器推荐设置的消息 — 初始余额、杠杆、和测试开始时间,与第一次余额交易的日期相对应。例如,它或许看似这样:
Alert: Now you can run testing Interval: 2024.09.13 - current date Initial deposit: 3000.00, leverage 1:500
这样的测试器设置,提供的测试器最终结果最接近实际所得。
在 \MQL5\Experts\TradingByHistoryDeals\SymbolTrade.mqh 文件中,成交集合的结构仅为保存成交到文件,并从文件读取保存的历史。为了继续我们的工作,我们需要创建一个成交类,其对象将存储在列表之中。列表本身存储在测试器的交易类对象之中。反过来,交易对象也是存储在自己列表中的类对象。每个交易对象将按照其所属的特定品种判定 — 交易中涉及的品种数量将决定交易对象的数量。交易对象本身将仅包含其所涉业务的品种列表,和它们自身的标准库的 CTrade 类对象。这将允许每个 CTrade 类交易对象根据所交易品种的条件进行自定义。
我们在 \MQL5\Experts\TradingByHistoryDeals\SymbolTrade.mqh 文件中编写一个成交类。
//+------------------------------------------------------------------+ //| 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) {} };
该类几乎完全重复了之前创建的成交结构。除了成交属性之外,还添加了其它属性 — 运营成交时品种的小数位和点数。这简化了成交描述的输出,因为该数据在创建对象时立即在成交构造函数中设置,这样就无需在访问时再为每笔成交获取这些属性(如果需要)。
此外,此处还创建了一个虚拟方法 Compare() 来比较两个成交对象 — 它会在成交列表排序时用到,以便按指定属性查找所望成交。
现在我们创建一个交易品种类。该类将存储一份成交列表,在对象属性中有所经营的品种集合,测试器将从中请求这些成交进行复制。一般来说,该类是在策略测试器中按品种复制账户上所完成交易的基础:
//+------------------------------------------------------------------+ //| 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) {} };
我们考察一些方法。
- SetNumProcessedDeals() 和 NumProcessedDeals() 设置并返回测试器从文件中得到的成交列表后,已处理的历史成交数量。它们对于处理历史成交的有效性,以及获取由测试器处理的成交数量的最终统计是必要的;
- GetDealCurrent() 返回一个指向当前历史成交的指针,需由测试器处理,之后要标记为已处理;
- DealCurrentIndex() 返回当前选定的历史成交索引,需由测试器处理;
- SetNextDealIndex() 当前历史成交处理完成后,设置测试器要处理的下一笔成交的索引。由于列表中的所有历史成交都是按时间(以毫秒为单位)排序的,故这将在测试器完成前一笔成交的处理后,设置下一笔成交的索引。依此方式,我们将依次选择历史上所有成交交,按当前所选成交属性中设置的时刻,交由测试器处理;
- CheckTime() 在测试器中检查当前历史成交属性中设置的发生时刻。逻辑如下:有一笔选定的成交需在测试器中处理。只要测试器的时间小于记录在成交里的时间,我们就什么都不做 — 我们只是迈入下一个即刻报价。一旦测试器中的时间等于或大于当前所选成交中的时间(测试器中的时间可能与成交中的时间不一致,因此也会检查时间是否“大于”),测试器就会根据其类型,和改变持仓的方式来处理成交。接下来,该笔成交被标记为已处理,设置下一笔交易的索引,并继续等待,由该方法管控,是否执行下一笔交易:
- OnTester() 处理程序是从标准 EA OnTester() 处理程序调用的,在流水账中显示品种名称,历史成交数量,以及由测试器已处理过的数量,并按交易对象品种返回已处理成交的数量。
该类有两个构造函数 — 默认和参数化。
在参数化构造函数的形参中,传递成交品种名称,其在创建对象时使用,而 CTrade 类交易对象接收保证金计算模式,当前账户设置,以及订单填充类型,交易对象品种的设置:
//--- 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::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; }
指向成交对象的指针被传递给至方法。如果列表中已存在该笔票根的成交,则返回 true。否则,将该笔成交添加到按成交时间(以毫秒为单位)排序的列表之中。
该方法按时间(以秒为单位)返回指向成交对象的指针:
//+------------------------------------------------------------------+ //| 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); }
该方法接收所需的时间。我们取传递给方法的时间来设置临时成交对象,列表按时间(以毫秒为单位)排序,并搜索时间等于所传递时间(设置为临时对象)的成交索引。接下来,按已找到的索引返回指向列表中成交的指针。如果列表中没有该时间的成交,则索引将等于 -1,并从列表中返回 NULL。
有趣的是,成交是按时间(以秒为单位)搜索的,但我们是按时间(以毫秒为单位)对列表进行排序。测试表明,如果列表也以秒为单位排序,那么有些成交就不包含在其中,尽管它们肯定存在。这很可能是因为一秒钟内有若干笔相差毫秒级的成交。此外,还返回指向先前处理的成交指针,因为多笔成交具有相同的时间(以秒为单位)。
该方法按仓位 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; }
该方法接收仓位 ID,需找到它才能交易。接下来,在循环中遍历交易列表,我们取得仓位 ID 等于传递给该方法的成交,并返回一个指向仓位变更方法等于 “入场”(DEAL_ENTRY_IN)的成交指针。
该方法按索引返回在列表中指向成交对象的指针:
//+------------------------------------------------------------------+ //| CSymbolTrade::Return the deal object by index in the list | //+------------------------------------------------------------------+ CDeal *CSymbolTrade::GetDealByIndex(const int index) { return this.m_list_deals.At(index); }
我们简单地按传递给方法的索引返回指向列表中对象的指针。如果索引不正确,则返回 NULL。
该方法返回指向当前成交索引处的指针:
//+------------------------------------------------------------------+ //| 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); }
成交列表按时间(以毫秒为单位)排序,且指向的成交,其索引写入 m_index_next_deal 类变量,并返回。
该方法返回当前出价:
//+------------------------------------------------------------------+ //| 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; }
我们取最后一次即刻报价的数据放入 m_tick 价格结构之中,并从其返回出价。
该方法返回当前要价:
//+------------------------------------------------------------------+ //| 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; }
我们取最后一次即刻报价的数据放入 m_tick 价格结构之中,并从其返回要价。
该方法返回当前时间(以秒为单位):
//+------------------------------------------------------------------+ //| 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; }
我们取最后一次即刻报价的数据放入 m_tick 价格结构中,并从其返回时间。
该方法返回当前时间(以毫秒为单位):
//+------------------------------------------------------------------+ //| 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; }
我们取最后一次即刻报价的数据放入 m_tick 价格结构之中,并从其返回时间(以毫秒为单位)。
该方法开多仓:
//+------------------------------------------------------------------+ //| 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(); }
该方法接收开多仓的参数,为交易对象设置所需的仓位魔幻数字,并按指定参数发送开多仓的订单。开仓失败返回零,而成功则生成持仓所依据的订单票根。
该方法开空仓:
//+------------------------------------------------------------------+ //| 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(); }
与前一种方法类似,但开空仓。
该方法据票根平仓:
//+------------------------------------------------------------------+ //| CSymbolTrade::Close position by ticket | //+------------------------------------------------------------------+ bool CSymbolTrade::ClosePos(const ulong ticket) { return this.m_trade.PositionClose(ticket); }
返回调用 CTrade 类交易对象的 PositionClose() 方法的结果。
交易品种类已准备就绪。现在我们在 EA 中实现它,以便处理保存在文件中的历史成交。
在测试器里分析来自文件的成交历史记录
我们转到 \MQL5\Experts\TradingByHistoryDeals\TradingByHistoryDeals.mq5 EA 文件,并添加新创建的交易品种类的临时对象 — 在存储指向该类对象指针的列表中查找所需对象时需要它:
//+------------------------------------------------------------------+ //| 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;
我们有一个历史成交数组,有基于此,我们能够创建一个成交对象列表,内含属于对象品种的成交列表。成交数组存储描述成交的结构。由于交易对象将包含成交对象列表,故我们需要创建一个函数来创建一个新的成交对象,并据描述成交的结构字段来填充成交属性:
//+------------------------------------------------------------------+ //| 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; }
该函数接收成交结构,创建新的成交对象,并据结构字段中的数值填充其属性。
该函数返回指向新创建对象的指针。如果在创建对象时发生错误,则返回 NULL。
我们编写一个函数来创建交易品种对象列表:
//+------------------------------------------------------------------+ //| 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; }
该函数的逻辑已在注释中详述。循环遍历历史成交列表,分析每笔连续的成交。检查其品种,如果该品种还没有交易对象,则创建一个新的交易对象,并将其保存在列表当中。如果它已经存在,我们只需从列表中获取指向品种的交易对象指针。接下来,按相同的方式检查交易对象的成交列表中是否存在此类成交,如果不存在,则将其添加到列表当中。如是结果,循环遍历所有历史成交,按品种获取交易对象列表,其中包含属于对象品种的成交列表。
交易对象列表可调用以下函数发送到流水账:
//+------------------------------------------------------------------+ //| 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()); } }
在交易品种对象列表的循环遍历中,获取下一个对象,并在流水账中显示其描述。在流水账中,这看起来像这样:
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
我们现已拥有一个成交类对象。添加返回一笔成交描述的函数:
//+------------------------------------------------------------------+ //| 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()))); }
该函数完全重复返回成交结构描述的相同逻辑。但于此,传递给函数的是指向成交对象的指针,取代了成交结构。
现在我们来创建 OnInit() 处理程序,紧随逻辑结论。
在测试器中添加 EA 启动处理,创建交易对象列表,并访问交易所用的每个品种,以便加载其历史记录,并在测试器中打开这些品种的图表窗口:
//+------------------------------------------------------------------+ //| 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); }
在 EA OnDeinit() 处理程序中,用 EA 清除创建的数组和列表:
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- clear the created lists and arrays ExtListSymbols.Clear(); ArrayFree(ExtArrayDeals); }
在测试器中,由 EA 的 OnTick() 程序处理文件中的成交列表:
//+------------------------------------------------------------------+ //| 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); }
我们来更详尽地研究该函数。一般情况,处理历史成交的逻辑最初呈现如下:
- 获取即刻报价时间,
- 得到该时刻的成交,
- 在测试器中处理这笔成交。
初看,简单而合乎逻辑的结构在实现时完全失败了。问题是,在测试器中,即刻报价时间并不总是与成交时间一致。甚至以毫秒为单位。如是结果,当基于来自同一服务器的真实即刻报价进行测试时,交易亏损了。我们或许知道即刻报价时间,我们或许肯定知道此刻有一笔成交,但测试器看不到它 — 没有与成交时间相同的即刻报价。但在成交时间前后,即刻报价会随时间而变。相应地,逻辑能够不围绕即刻报价、及其时序构建,而是围绕成交:
- 成交在列表中按出现时间(以毫秒为单位)排序。将第一笔成交的索引设置为当前的索引,
- 按当前成交索引选择成交,并获取其时间;
- 等待这个时间的即刻报价:
- 如果即刻报价时间小于成交时间,则等待下一次即刻报价,
- 如果即刻报价时间等于或大于成交时间,我们处理成交,注册它已被处理的事实,并将下一笔成交的索引设置为当前的索引;
- 从第 2 点开始重复,直至测试完成。
这种结构允许我们等待每笔后续成交的时间,并在测试器中执行它。在这种情况下,我们不关注交易价格 — 我们简单地在时机来临时复制成交。即使测试器中的即刻报价时间比真实成交的时间晚一点,也行。最主要的是复制交易。成交已由测试器处理的事实,会由成交的“测试器内部仓位票根”属性的非零值指示。如果该值为零,则表示该成交尚未在测试器中处理。在测试器中执行该笔成交后,其在测试器中所属持仓的票根将输入到该属性当中。
我们加入实现上述逻辑的函数:
//+------------------------------------------------------------------+ //| 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(); } } }
该段代码据文件登记的原始交易,完全复现了账户上进行的成交。所有仓位均无止损单。换言之,止损和止盈值不会从真实成交复制到开仓方法。这简化了成交跟踪,这在于文件还列出了平仓成交,而测试器会处理它们,无关持仓是如何平仓的 — 通过止损或止盈。
编译 EA,并在图表上启动它。如是结果,HistoryDealsData.bin 文件将在客户端的共享文件夹中创建,路径类似于 “C:\Users\UserName\AppData\Roaming\MetaQuotes\Terminal\Common\Files”,在 TradingByHistoryDeals 子文件夹中,图表上将显示一条警报,其中包含有关所需测试器设置的消息:

现在我们在测试器中运行 EA,在测试器设置中选择指定的日期范围、初始存款和杠杆:

针对所有交易品种和魔幻数字运行测试:

事实证明,整个交易给我们带来了 550 美元的亏损。我想知道如果我们设置不同的止损单会发生什么?
我们来检查这个。
调整止损单
将 EA 保存在与 TradingByHistoryDeals_SLTP.mq5 相同的文件夹 \MQL5\Experts\TradingByHistoryDeals\ 中。
添加测试方法的枚举,并通过添加一个用于设置止损单参数的组,以及两个全局级别的新变量,通过它们将止损和止盈值传递给交易对象,所有输入将按组划分:
//+------------------------------------------------------------------+ //| 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;
在 OnInit() 处理程序中,调整输入设置的止损值,并将其写入变量:
int OnInit() { //--- Adjust the stops ExtStopLoss =(InpStopLoss<1 ? 0 : InpStopLoss); ExtTakeProfit=(InpTakeProfit<1 ? 0 : InpTakeProfit); //--- If the EA is not running in the tester
添加函数,计算相对于品种设置 StopLevel 的止损和止盈的正确价格值:
//+------------------------------------------------------------------+ //| 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); }
为了设置止损和止盈价位,止损单价格不应接近当前价格的 StopLevel 距离。如果品种的 StopLevel 值为零,则用大小等于该值两个或三个点差,为交易品种设置止损/止盈这些功能均使用双精度点差乘数。该值在函数的形参中传递,默认值为 2。如果需要更改乘数的值,我们需要在调用函数时向函数传递所需的不同值。这些函数返回止损和止盈的正确价格。
在 TradeByHistory() 成交历史交易函数中,插入新的代码模块,参考测试器中的交易模式,如果选用指定的止损单值进行测试,则设置止损和止盈值。在平仓模块中,我们仅需在测试类型为“原始交易”时才需要平仓。如果选用指定的止损单值进行测试,则应忽略平仓成交 — 测试器将根据指定的止损和止盈值自动平仓。当交易按止损单进行时,如果平仓交易完成,我们唯一需要做的就是将它们标记为已处理,并继续下一笔成交。
//+------------------------------------------------------------------+ //| 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(); } } }
在 EA 的 OnTester() 处理程序中,计算并返回由测试器处理的成交总数:
//+------------------------------------------------------------------+ //| 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); }
此外,每个品种交易对象都有自己的 OnTester() 处理程序,其会在流水账中打印其数据。测试结束时,我们将在测试器日志中收到以下消息:
2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol AUDUSD: Total deals: 218, number of processed deals: 216 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol EURJPY: Total deals: 116, number of processed deals: 114 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol EURUSD: Total deals: 524, number of processed deals: 518 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol GBPUSD: Total deals: 352, number of processed deals: 350 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol NZDUSD: Total deals: 178, number of processed deals: 176 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol USDCAD: Total deals: 22, number of processed deals: 22 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol USDCHF: Total deals: 250, number of processed deals: 246 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol USDJPY: Total deals: 142, number of processed deals: 142 2025.01.22 23:49:15.951 Core 1 2025.01.21 23:54:59 Symbol XAUUSD: Total deals: 118, number of processed deals: 118 2025.01.22 23:49:15.951 Core 1 final balance 3591.70 pips 2025.01.22 23:49:15.951 Core 1 OnTester result 1902
编译 EA,并采用相同的测试设置运行它,但将测试类型指定为“指定的止损和止盈值”,将止损和止盈值分别设置为 100 和 500 点:

在上一次测试中,在测试原始交易时,我们损失了 550 美元。现在,通过将所有持仓的止损替换为 100 点,将止盈替换为 500 点,我们获得了 590 点的盈利。这是通过简单地替换止损单来达成的,无需查看所交易的不同品种的具体情况。如果我们为每个交易的品种选择各自的止损单大小,那么测试图形很可能会趋于平稳。
结束语
在本文中,我们以“如果...”的风格对交易历史进行了一个小型实验。我认为这样的实验很可能会带来改变一个人的交易风格的有趣解决方案。在下一篇文章中,我们将进行另一项此类实验,包括各种持仓尾随止损。事情会变得更加有趣。
下面附上了此处讨论的所有 EA 和类。您可下载并研究它们,也可在您自己的交易账户上尝试它们。您能够立即将存档文件夹解压缩到 MQL5 客户端的终端目录之中,所有文件都被放置在所需的子文件夹当中。
文章中用到的程序:
| # | 名称 | 类型 | 描述 |
|---|---|---|---|
| 1 | SymbolTrade.mqh | 类库 | 交易结构和类库,品种交易类 |
| 2 | TradingByHistoryDeals.mq5 | 智能交易系统 | 在测试器中查看账户上执行的成交和交易的 EA |
| 3 | TradingByHistoryDeals_SLTP.mq5 | 智能交易系统 | 在测试器中查看和修改账户上执行的成交和交易,并使用止损和止盈的 EA |
| 4 | MQL5.zip | ZIP 存档 | 上面显示的文件存档可以解压到客户端的 MQL5 目录之中 |
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/16952
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
MQL5 交易工具包(第 5 部分):使用仓位函数扩展历史管理 EX5 库
MQL5自动化交易策略(第九部分):构建亚洲盘突破策略的智能交易系统(EA)
MQL5 交易策略自动化(第十部分):开发趋势盘整动量策略
价格行为分析工具包开发(第十五部分):引入四分位理论(1)——四分位绘图脚本
错在哪里?
如果文件小于读取前的数组,数组大小不会改变。
使用 ArrayCopy 时也会出现类似错误。你忽略了一个很好的功能
有什么好处?
有什么好处?
执行的简洁性和速度(完全在 MQ 方面)。
请展示标准的自印和自填结构。
在简洁性和执行速度方面(完全在 MQ 方面)。