请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化策略 帖子:3364712 新帖:0

MetaTrader 5 中的交易事件

专门套利发表于:4 月 17 日 17:56回复(1)

简介

所有执行交易操作的命令都从 MetaTrader 5 客户端通过发送请求的形式被传递到交易服务器。每个请求都应依据请求的操作正确提交;否则它不会通过初步验证并且不会被服务器接受以进一步处理。

被交易服务器接受的请求既能够以挂单的形式,也能够以按市场价立即执行的订单的形式存储在服务器中。订单存储在服务器中,直到它们被执行或被撤销。订单执行的结果即为成交。

成交按给定交易品种改变交易持仓,它可以建仓、平仓、增仓、减仓或反向仓位。因此,一个未平持仓始终是执行一个或多个成交的结果。在《 Orders, Positions, and Deals in MetaTrader 5》(MetaTrader 5 中的订单、持仓和成交)一文中提供了更为详细的信息。

本文说明了从发送请求到在处理之后将其移到交易历史记录的时间内的概念、术语和处理过程。


将请求从客户端传递到交易服务器

要执行交易操作,您应将订单发送到交易系统。请求始终是通过从客户端提交一个订单来发送到交易服务器的。无论您如何进行交易,是手动还是使用 MQL5 程序,都必须正确填写请求的结构。

要手动执行交易操作,您应通过按 F9 键来打开填写交易请求的对话窗口。当通过 MQL5 自动交易时,使用 OrderSend() 函数发送请求。因为很多不正确的请求会导致交易服务器出现不希望的过载,在发送之前,必须使用 OrderCheck() 函数检查每个请求。请求的检查结果被放入 MqlTradeCheckresult 结构所描述的一个变量中。

重要须知:在发送到交易服务器之前,每个请求是在客户端中检查是否正确的。不正确的故意请求(买数百万手或以负的价格买入)不会被发送到客户端以外。这样做旨在保护交易服务器不会受到 MQL5 程序中的错误导致的大量错误请求的干扰。

一旦请求到达交易服务器,则将对它进行初步检查:

  • 您是否有足够的资产来执行交易操作;
  • 指定的价格是否正确:开盘价、止损价、止盈价等;
  • 指定价格是否存在于供立即执行的价格流中;
  • 市场执行模式中是否缺少止损价和止盈价;
  • 数量是否正确:最小数量和最大数量、步长、持仓的最大数量(SYMBOL_VOLUME_MIN、SYMBOL_VOLUME_MAX、SYMBOL_VOLUME_STEPSYMBOL_VOLUME_LIMIT);
  • 交易品种的状态:报价或交易会话、按交易品种进行交易的可能性、交易的具体模式(例如仅限于平仓)等;
  • 交易帐户的状态:具体帐户类型的不同限制;
  • 视请求的交易操作而定的其他检查。
在服务器上未通过初步检查的不正确请求将被拒绝。系统始终通过发送一个响应,向客户端通知请求的检查结果。交易服务器的响应可来自 MqlTradeResult 类型的变量,该变量在发送请求时通过 OrderSend() 函数的第二个参数传递。

从客户端向交易服务器发送交易请求

如果一个请求通过了针对正确与否的初步检查,则它将被放入等待处理的请求队列中。作为请求的处理结果,在交易服务器的资料库中创建一个订单(执行交易操作的命令)。但是,有两种类型的请求不会导致订单的创建:

  1. 更改仓位的请求(更改其止损和/或止盈);
  2. 修改挂单的请求(其价格水平和到期时间)。

客户端收到一条指出请求已被接受并且放入 MetaTrader 5 平台的交易子系统中的信息。服务器将接受的请求放入请求队列供进一步处理,这可能导致:

  • 生成挂单;
  • 按市场价格立即执行订单;
  • 修改订单或持仓。

请求在服务器的请求队列中的寿命限制为三分钟。一旦过期,则从请求队列中删除该请求。


从交易服务器向客户端发送交易事件

事件模型和事件处理函数是以 MQL5 语言实施的。这意味着在对任何预定义事件的响应中,MQL5 执行环境调用相应的函数 - 事件处理程序。有一个预定义函数 OnTrade() 用于处理交易事件;用于处理订单、持仓和成交的代码必须放在此函数中。此函数仅供 EA 交易程序调用,即使您在指标和脚本中添加具有相同名称和类型的函数,也不能在这些指标和脚本中使用此函数。

如果出现以下情况,则服务器生成交易事件:

  • 活动订单改变
  • 仓位改变
  • 成交改变
  • 交易历史记录改变

注意,一个操作可能导致几个事件发生。例如,触发挂单导致两个事件的发生:

  1. 写入交易历史记录的成交的出现;
  2. 将挂单从活动订单列表移到历史订单列表(订单被移到历史记录)。

不能从相对的报价获得需要的数量时依据一个订单执行几个成交也是导致多个事件的例子。交易服务器创建事件,并将有关信息发送到客户端。这是为什么可以针对表面上是一个的事件多次调用 OnTrade() 函数的原因。这是在 MetaTrader 5 平台的交易子系统中订单处理过程的一个简单例子。

以下是一个示例:在一个要买入 10 手 EURUSD 的挂单等待执行时,卖出 1 手、4 手和 5 手的三个相反报价出现。这三个请求加起来提供所需的 10 手,因此,如果平仓政策允许部分执行交易操作,则会逐个执行它们。

作为 4 个订单的执行结果,服务器将依据现有相反请求执行 1 手、4 手和 5 手的三笔成交。在这个例子中将生成多少交易事件?第一个要卖出 1 手的相反请求将执行 1 手成交的执行。这是第一个交易事件(1 手成交)。但是要买入 10 手的挂单也改变了;现在,它变成要买入 9 手 EURUSD 的订单。等办订单的数量的改变是第二个交易事件(等办订单的数量的改变)。

交易事件的生成

对于第二笔 4 手成交,将生成另外两个交易事件,有关它们的消息将被发送到发起要买入 10 手 EURUSD 的初始挂单的客户端。

最后 5 手成交将导致三个交易事件的发生:

  1. 5 手成交
  2. 数量改变
  3. 订单移到交易历史记录

作为成交的执行结果,客户端依次收到 7 个交易事件 Trade(假定客户端和交易服务器之间的连接是稳定的,没有消息丢失)。必须在 EA 交易程序中使用 OnTrade() 函数处理这些消息。

重要须知:关于交易事件 Trade 的每条消息可作为一个或多个交易请求的结果出现。每个请求都可以导致几个交易事件的发生。您不能依赖“一个请求,一个交易事件”的声明,因为事件的处理可能分为几个阶段执行,并且每个操作可能更改订单的状态、仓位和交易历史记录。


交易服务器对订单的处理

所有等待执行的订单最终都将被移到历史记录,无论是否满足它们的执行条件,或者是否撤消了它们。订单的撤消有几种情形:

  • 依据订单执行成交;
  • 交易商拒绝订单;
  • 按交易者的要求撤销订单(手动请求或从 MQL5 程序自动请求);
  • 订单过期,这由交易者在发送请求时确定,或由给定交易系统的交易条件确定;
  • 满足执行条件时交易帐户缺少执行成交所需的资产;
  • 平仓政策导致的订单撤消(撤消了部分平仓订单)。

无论何种原因导致活动订单被移到历史记录,有关改变的消息都会被发送到客户端。有关交易事件的消息并不会发送到所有客户端,而是发送到连接到对应帐户的客户端。



重要须知:交易服务器接受请求的事实并不会始终都会导致所请求操作的执行。它表示在到达交易服务器之后,请求通过了验证。

这是为什么 OrderSend() 函数的说明文档指出:

返回值

如果请求成功通过基本检查,则 OrderSend() 函数返回 true - 这并不是成功执行交易操作的标志。欲知函数执行结果的更为详细的说明,请分析结构 MqlTradeResult 的字段。


客户端中交易和历史记录的更新

有关交易事件和交易历史记录的改变的消息通过几个渠道传送。使用 OrderSend() 函数发送买入请求时,您可以获得订单单证,该单证是作为请求成功通过验证的结果创建的。同时,订单本身可能并没有出现在客户端中,并且尝试使用 OrderSelect() 选择该订单会导致失败。


所有来自交易服务器的消息都是独立到达客户端的

在上图中,您可以看到交易服务器如何将订单单证传递给 MQL5 程序,但是有关交易事件 Trade 的消息(新订单的出现)尚未到达。有关活动订单列表改变的消息也没有到达。

有可能存在这样的情形,当有关新订单的出现的 Trade(交易)消息到达程序时,据该订单进行的成交已经执行,因此,该订单已经不在活动订单列表中,而是在历史记录中。这是真实存在的情形,因为相对于通过网络提交消息的当前速度,请求的处理速度要快得多。


MQL5 中交易事件的处理

交易服务器上的所有操作和交易事件的相关消息的发送是不对称的。找出交易帐户出现什么确切变化只有一种确信的方法。这种方法就是记住交易状态和交易历史记录,然后将其与新状态进行比较。

在 EA 交易程序中跟踪交易事件的算法如下所述:

  1. 在全局范围内声明订单、持仓和成交的计数器;
  2. 确定将被载入到 MQL5 程序缓存的交易历史记录的深度。我们载入到缓存的历史记录越多,客户端和计算机消耗的资源就越多;
  3. 在 OnInit 函数中对订单、持仓和成交的计数器进行初始化;
  4. 确定在其中将交易历史记录载入到缓存的处理函数;
  5. 在载入交易历史记录之后,我们将通过比较记住的状态和当前状态找出交易帐户有何改变。

这是最简单的算法,它允许发现未平持仓(订单、成交)的数量是否改变以及改变的方向。如果存在改变,则我们可以进一步获取更为详细的信息。如果订单的数量没有改变,但是订单本身出现改变,则需要不同的方法;因此,这种情形不在本文的讨论范围之内。

可以在 EA 交易程序的 OnTrade() 和 OnTick() 函数中检查计数器的改变。

让我们一步接一步地编写一个程序示例。

1. 在全局范围内声明订单、成交和持仓的计数器。

int          orders;            // 活动订单数量
int          positions;         // 开启仓位数量
int          deals;             // 交易历史缓存中的交易数量
int          history_orders;    // 交易历史缓存中的订单数量
bool         started=false;     // 计数器初始化的标志


2. 在输入变量 days 中设置要载入缓存的交易历史记录的深度(载入在此变量中指定的天数的交易历史记录)。

input    int days=7;            // 交易历史深度,以天数为单位

//--- 设置全局范围内交易历史的限制
datetime     start;             // 缓存中交易历史的起始日期
datetime     end;               // 缓存中交易历史的结束日期


3. 对交易历史记录的计数器和限制进行初始化。在 InitCounters() 函数以外对计数器进行初始化可以使代码具有更好的可读性:

int OnInit()
  {
//---
   end=TimeCurrent();
   start=end-days*PeriodSeconds(PERIOD_D1);
   PrintFormat("将要载入的交易历史限制为: 起始日期 - %s, 结束日期 - %s",
               TimeToString(start),TimeToString(end));
   InitCounters();
//---
   return(0);
  }

InitCounters() 函数试图将交易历史记录载入缓存,并且在成功时,对所有计数器进行初始化。同样的,如果成功载入了历史记录,则全局变量 'started' 的值将被设置为 'true',这表示计数器已成功初始化。

//+------------------------------------------------------------------+
//|  初始化仓位,订单和交易的计数器                                       |
//+------------------------------------------------------------------+
void InitCounters()
  {
   ResetLastError();
//--- 载入历史
   bool selected=HistorySelect(start,end);
   if(!selected)
     {
      PrintFormat("%s. 从缓存中载入历史失败,时间从 %s 到 %s. 错误代码: %d",
                  __FUNCTION__,TimeToString(start),TimeToString(end),GetLastError());
      return;
     }
//--- 读取当前数值
   orders=OrdersTotal();
   positions=PositionsTotal();
   deals=HistoryDealsTotal();
   history_orders=HistoryOrdersTotal();
   started=true;
   Print("订单,仓位和交易计数器初始化成功");
  }


4. 在 OnTick() 和 OnTrade() 处理程序中对交易帐户的状态改变进行检查。首先检查 'started' 变量 - 如果其值为 'true',则调用 SimpleTradeProcessor() 函数,否则调用计数器的初始化函数 InitCounters()。

//+------------------------------------------------------------------+
//| EA订单函数                                                        |
//+------------------------------------------------------------------+
void OnTick()
  {
   if(started) SimpleTradeProcessor();
   else InitCounters();
  }
//+------------------------------------------------------------------+
//| 当 Trade 事件发生时调用                                            |
//+------------------------------------------------------------------+
void OnTrade()
  {
   if(started) SimpleTradeProcessor();
   else InitCounters();
  }

5. SimpleTradeProcessor() 函数检查订单、成交和持仓的数量是否改变。在进行所有检查之后,我们调用 CheckStartDateInTradeHistory() 函数,该函数在需要时将变量 'start' 的值移到更接近当前时刻的值。

//+------------------------------------------------------------------+
//| 处理交易历史改变的简单实例                                           |
//+------------------------------------------------------------------+
void SimpleTradeProcessor()
  {
   end=TimeCurrent();
   ResetLastError();
//--- 载入历史
   bool selected=HistorySelect(start,end);
   if(!selected)
     {
      PrintFormat("%s. 从缓存中载入历史失败,时间从 %s 到 %s. 错误代码: %d",
                  __FUNCTION__,TimeToString(start),TimeToString(end),GetLastError());
      return;
     }

//--- 读取当前数值
   int curr_orders=OrdersTotal();
   int curr_positions=PositionsTotal();
   int curr_deals=HistoryDealsTotal();
   int curr_history_orders=HistoryOrdersTotal();

//--- 检查活动订单数是否改变
   if(curr_orders!=orders)
     {
      //--- 活动订单数改变了
      PrintFormat("订单数改变了. 之前数量为 %d, 目前数量为 %d",
                  orders,curr_orders);
     /*
       其他与订单改变关联的行为
     */
      //--- 更新数值
      orders=curr_orders;
     }

//--- 开启仓位数量改变
   if(curr_positions!=positions)
     {
      //--- number of open positions has been changed
      PrintFormat("仓位数量改变了. 之前数量为 %d, 目前数量为 %d",
                  positions,curr_positions);
      /*
      其他与仓位改变关联的行为
      */
      //--- 更新数值
      positions=curr_positions;
     }

//--- 交易历史缓存中交易数量的改变
   if(curr_deals!=deals)
     {
      //--- 交易历史缓存中交易数量已经改变
      PrintFormat("交易数量改变了. 之前数量为 %d, 目前数量为 %d",
                  deals,curr_deals);
      /*
       其他与交易数量改变关联的行为
       */
      //--- 更新数值
      deals=curr_deals;
     }

//--- 交易历史缓存中订单数量的改变
   if(curr_history_orders!=history_orders)
     {
      //--- 交易历史缓存中订单数量已经改变
      PrintFormat("历史中订单数量已经改变. 之前数量为 %d, 目前数量为 %d",
                  history_orders,curr_history_orders);
     /*
       其他与交易历史缓存中订单数量改变关联的行为
      */
     //--- 更新数值
     history_orders=curr_history_orders;
     }
//--- 检查是否有必要改变在缓存中请求交易历史的限制
   CheckStartDateInTradeHistory();
  }

CheckStartDateInTradeHistory() 函数计算当前时刻的交易历史记录请求的开始日期 (curr_start),并将其与变量 'start' 进行比较。如果两者之差大于 1 天,则 'start' 是正确的,并且更新历史订单和成交的计数器。

//+------------------------------------------------------------------+
//|  修改请求交易历史的起始日期                                          |
//+------------------------------------------------------------------+
void CheckStartDateInTradeHistory()
  {
//--- 初始时间段,就像我们马上开始工作
   datetime curr_start=TimeCurrent()-days*PeriodSeconds(PERIOD_D1);
//--- 确认交易历史起始限制的时间离 
//--- 计划时间多于一天
   if(curr_start-start>PeriodSeconds(PERIOD_D1))
     {
      //--- 我们需要改正载入历史缓存的起始日期
      start=curr_start;
      PrintFormat("将要载入交易历史的新的起始限制为: 起始日期 => %s",
                  TimeToString(start));

      //--- 现在再次根据改正过的时间间隔载入交易历史
      HistorySelect(start,end);

      //--- 改正历史中交易和订单的数量,以便用于将来的比较
      history_orders=HistoryOrdersTotal();
      deals=HistoryDealsTotal();
     }
  }

本文附带了 Expert Advisor DemoTradeEventProcessing.mq5 的完整代码。


总结

在线交易平台 MetaTrader 5 的所有操作都是异步的,有关交易帐户任何改变的消息都是相互独立发送的。因此,试图依据“一个请求,一个交易事件”的原则跟踪单个事件是不正确的。如果您需要在一个交易事件发生时精确地确定出现什么改变,则您应在每次调用 OnTrade 处理程序时通过将它们的当前状态与以前的状态进行比较来分析您的所有成交、持仓和订单。

全部回复

0/140

量化课程

    移动端课程