English Русский Español Deutsch 日本語 Português
preview
在 MetaTrader 5 中交易的可视评估和调整

在 MetaTrader 5 中交易的可视评估和调整

MetaTrader 5示例 |
72 16
Artyom Trishkin
Artyom Trishkin

内容



概述

我们想象一种情况:在某些账户上,使用不同的 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);
  }

我们来更详尽地研究该函数。一般情况,处理历史成交的逻辑最初呈现如下:

  1. 获取即刻报价时间,
  2. 得到该时刻的成交,
  3. 在测试器中处理这笔成交。

初看,简单而合乎逻辑的结构在实现时完全失败了。问题是,在测试器中,即刻报价时间并不总是与成交时间一致。甚至以毫秒为单位。如是结果,当基于来自同一服务器的真实即刻报价进行测试时,交易亏损了。我们或许知道即刻报价时间,我们或许肯定知道此刻有一笔成交,但测试器看不到它 — 没有与成交时间相同的即刻报价。但在成交时间前后,即刻报价会随时间而变。相应地,逻辑能够不围绕即刻报价、及其时序构建,而是围绕成交:

  1. 成交在列表中按出现时间(以毫秒为单位)排序。将第一笔成交的索引设置为当前的索引,
  2. 按当前成交索引选择成交,并获取其时间;
  3. 等待这个时间的即刻报价:
    1. 如果即刻报价时间小于成交时间,则等待下一次即刻报价,
    2. 如果即刻报价时间等于或大于成交时间,我们处理成交,注册它已被处理的事实,并将下一笔成交的索引设置为当前的索引;
  4. 从第 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

附加的文件 |
SymbolTrade.mqh (53.86 KB)
MQL5.zip (22.3 KB)
最近评论 | 前往讨论 (16)
fxsaber
fxsaber | 31 1月 2025 在 15:32
Artyom Trishkin #:

错在哪里?

如果文件小于读取前的数组,数组大小不会改变。

使用 ArrayCopy 时也会出现类似错误。
Artyom Trishkin
Artyom Trishkin | 31 1月 2025 在 15:34
fxsaber #:
你忽略了一个很好的功能

有什么好处?

fxsaber
fxsaber | 31 1月 2025 在 15:36
Artyom Trishkin #:

有什么好处?

执行的简洁性和速度(完全在 MQ 方面)。

fxsaber
fxsaber | 31 1月 2025 在 15:52
Artyom Trishkin #:

请展示标准的自印和自填结构。

几乎是标准的(共享 MQ 字段)。
Artyom Trishkin
Artyom Trishkin | 31 1月 2025 在 16:11
fxsaber #:

在简洁性和执行速度方面(完全在 MQ 方面)。

谢谢。错过
MQL5 交易工具包(第 5 部分):使用仓位函数扩展历史管理 EX5 库 MQL5 交易工具包(第 5 部分):使用仓位函数扩展历史管理 EX5 库
了解如何创建可导出的 EX5 函数,以高效查询和保存历史仓位数据。在本分步指南中,我们将通过开发检索最近平仓的关键属性的模块来扩展历史管理 EX5 库。这些属性包括净利润、交易持续时间、基于点的止损、止盈、利润值以及其他各种重要细节。
MQL5自动化交易策略(第九部分):构建亚洲盘突破策略的智能交易系统(EA) MQL5自动化交易策略(第九部分):构建亚洲盘突破策略的智能交易系统(EA)
在本文中,我们将在MQL5中开发一款适用于亚洲盘突破策略的智能交易系统(EA),用来计算亚洲时段的高低价以及使用移动平均线(MA)进行趋势过滤。同时实现动态对象样式、用户自定义时间输入和完善的风险管理。最后演示回测与优化技术,进一步打磨策略表现。
MQL5 交易策略自动化(第十部分):开发趋势盘整动量策略 MQL5 交易策略自动化(第十部分):开发趋势盘整动量策略
在本文中,我们将基于MQL5开发趋势盘整动量策略EA。我们将结合双移动平均线交叉与 RSI 和 CCI 动量过滤器来生成交易信号。我们还将对EA进行回测,以及为提升其在真实交易环境下的表现而进行的优化。
价格行为分析工具包开发(第十五部分):引入四分位理论(1)——四分位绘图脚本 价格行为分析工具包开发(第十五部分):引入四分位理论(1)——四分位绘图脚本
支撑位与阻力位是预示潜在趋势反转和延续的关键价位。尽管识别这些价位颇具挑战性,但一旦精准定位,您便能从容应对市场波动。如需进一步辅助,请参阅本文介绍的四分位绘图工具,该工具可帮助您识别主要及次要支撑位与阻力位。