OnTrade 事件

OnTrade 事件发生在更改已下订单和未结仓位列表、订单历史和交易历史时。任何交易行为(下单/激活/删除挂单、开仓/平仓、设置保护水平等)都会相应地改变订单和交易的历史和/或仓位和当前订单列表。动作的发起者可以是用户、程序或服务器。

要在程序中接收事件,应说明相应的处理程序。

void OnTrade (void)

在使用 OrderSend/OrderSendAsync 发送交易请求的情况下,一个请求会触发多个 OnTrade 事件,因为处理通常发生在几个阶段,每个运算都可以改变订单、仓位和交易历史的状态。

一般情况下,OnTradeOnTradeTransaction 的调用次数没有确切比例。 OnTrade 在相应的 OnTradeTransaction 调用之后被调用。

由于 OnTrade 事件具有一般化的特性,并且不指定操作的本质,所以 MQL 程序开发人员不太喜欢使用。通常需要检查代码中交易账户状态的所有方面,并将其与一些保存的状态进行比较,即与交易策略中使用的交易实体的应用缓存进行比较。例如,在最简单的情况下,你可以在 OnTrade 处理程序中记住已创建订单的订单号,以查询其所有特性。但是,这可能意味着对大量与特定订单无关的附带事件进行“不必要的”分析。

我们将在关于 多货币 EA 交易的章节中介绍交易环境和历史的应用缓存。

为了进一步研究 OnTrade,我们来看一个 EA 交易对两个 OCO(“互损订单”)挂单实现策略。该策略会下达一对突破止损订单,等待其中一个触发,之后第二个会被删除。为了清楚起见,我们可提供对两种类型交易事件 OnTradeOnTradeTransaction 的支持,这样工作逻辑将根据用户的选择从一个处理程序或另一个处理程序运行。

OCO2.mq5 文件中提供了源代码。其输入参数包括手数大小 Volume(默认值为 0 表示最小)和设在每个订单处的 Distance2SLTP 距离(以点为单位),它还可以确定保护水平、从设置时间开始的到期日期 Expiration (以秒为单位)以及事件切换器 ActivationBy(默认为 OnTradeTransaction)。由于 Distance2SLTP 设置了当前价格的偏移量和到止损点的距离,两个订单的止损是相同的,等于设置时的价格。

enum EVENT_TYPE
{
   ON_TRANSACTION// OnTradeTransaction
   ON_TRADE        // OnTrade
};
   
input double Volume;            // Volume (0 - minimal lot)
input uint Distance2SLTP = 500// Distance Indent/SL/TP (points)
input ulong Magic = 1234567890;
input ulong Deviation = 10;
input ulong Expiration = 0;     // Expiration (seconds in future, 3600 - 1 hour, etc)
input EVENT_TYPE ActivationBy = ON_TRANSACTION;

为了简化请求结构体的初始化,我们将说明从 MqlTradeRequestSync 派生的我们自己的 MqlTradeRequestSyncOCO 结构体。

struct MqlTradeRequestSyncOCOpublic MqlTradeRequestSync
{
   MqlTradeRequestSyncOCO()
   {
      symbol = _Symbol;
      magic = Magic;
      deviation = Deviation;
      if(Expiration > 0)
      {
         type_time = ORDER_TIME_SPECIFIED;
         expiration = (datetime)(TimeCurrent() + Expiration);
      }
   }
};

在全局水平,我们介绍几个对象和变量。

OrderFilter orders;        // object for selecting orders
PositionFilter trades;     // object for selecting positions
bool FirstTick = false;    // or single processing of OnTick at start
ulong ExecutionCount = 0;  // counter of trading strategy calls RunStrategy()

所有的交易逻辑,除了开始时刻,均会被交易事件触发。在 OnInit 处理程序中,我们设置筛选对象并等待第一个分时报价(将 FirstTick 设置为 true)。

int OnInit()
{
   FirstTick = true;
   
   orders.let(ORDER_MAGICMagic).let(ORDER_SYMBOL_Symbol)
      .let(ORDER_TYPE, (1 << ORDER_TYPE_BUY_STOP) | (1 << ORDER_TYPE_SELL_STOP),
      IS::OR_BITWISE);
   trades.let(POSITION_MAGICMagic).let(POSITION_SYMBOL_Symbol);
      
   return INIT_SUCCEEDED;
}

我们只对止损订单(买入/卖出)、有特定魔术编号以及当前交易品种的仓位感兴趣。

OnTick 函数中,我们曾经调用设计为 RunStrategy 的算法的主要部分(在下文中说明)。此外,只能从 OnTradeOnTradeTransaction 调用此函数。

void OnTick()
{
   if(FirstTick)
   {
      RunStrategy();
      FirstTick = false;
   }
}

例如,当 OnTrade 模式被启用时,这个片段起作用。

void OnTrade()
{
   static ulong count = 0;
   PrintFormat("OnTrade(%d)", ++count);
   if(ActivationBy == ON_TRADE)
   {
      RunStrategy();
   }
}

注意,不管此处是否激活了该策略,OnTrade 处理程序的调用均进行计数。类似地,在 OnTradeTransaction 处理程序中会对相关事件进行计数(即使它们的发生无效)。这是为了能够同时在日志中看到事件及其计数器。

OnTradeTransaction 模式打开时,显然 RunStrategy 就是从那里启动的。

void OnTradeTransaction(const MqlTradeTransaction &transaction,
   const MqlTradeRequest &request,
   const MqlTradeResult &result)
{
   static ulong count = 0;
   PrintFormat("OnTradeTransaction(%d)", ++count);
   Print(TU::StringOf(transaction));
   
   if(ActivationBy != ON_TRANSACTIONreturn;
   
   if(transaction.type == TRADE_TRANSACTION_ORDER_DELETE)
   {
      // why not here? for answer, see the text
      /* // this won't work online: m.isReady() == false because order temporarily lost
      OrderMonitor m(transaction.order);
      if(m.isReady() && m.get(ORDER_MAGIC) == Magic && m.get(ORDER_SYMBOL) == _Symbol)
      {
         RunStrategy();
      }
      */
   }
   else if(transaction.type == TRADE_TRANSACTION_HISTORY_ADD)
   {
      OrderMonitor m(transaction.order);
      if(m.isReady() && m.get(ORDER_MAGIC) == Magic && m.get(ORDER_SYMBOL) == _Symbol)
      {
         // the ORDER_STATE property does not matter - in any case, you need to remove the remaining
         // if(transaction.order_state == ORDER_STATE_FILLED
         // || transaction.order_state == ORDER_STATE_CANCELED ...)
         RunStrategy();
      }
   }
}

应当注意的是,在线交易时,由于从现有订单转移到历史订单,触发的挂单可能会从交易环境中消失一段时间。当我们收到 TRADE_TRANSACTION_ORDER_DELETE 事件时,该订单已经从活跃订单中删除但还没有在历史订单中出现。只有当我们接收到 TRADE_TRANSACTION_HISTORY_ADD 事件时,该订单才会出现在历史订单中。在 测试程序中不会观察到这种行为,即,被删除的订单会立即添加到历史中,并可用于选择和读取已经处于 TRADE_TRANSACTION_ORDER_DELETE 阶段的特性。

在这两个交易事件处理程序中,我们可计数并记录调用次数。对于使用 OnTrade 的情况,调用次数必须匹配 ExecutionCount,我们将很快在RunStrategy 中看到这一点。但是,对于 OnTradeTransaction,其计数器和 ExecutionCount 会有很大差异,因为此处的策略是针对一种类型的事件非常有选择性地调用的。基于此,我们可以得出结论,OnTradeTransaction 通过仅在适当的时候调用算法,允许更有效地使用资源。

卸载 EA 交易时,ExecutionCount 计数器会输出到日志中。

void OnDeinit(const int r)
{
   Print("ExecutionCount = "ExecutionCount);
}

现在,最后,我们介绍一下 RunStrategy 函数。承诺计数器从一开始就递增计数。

void RunStrategy()
{
   ExecutionCount++;
   ...

接下来,说明两个用于从 orders 筛选对象接收订单号及其状态的数组。

   ulong tickets[];
   ulong states[];

首先,我们可请求符合我们条件的订单。如果有两个订单,一切都好,什么都不用做。

   orders.select(ORDER_STATEticketsstates);
   const int n = ArraySize(tickets);
   if(n == 2return// OK - standard state
   ...

如果剩余一个订单,则另一个订单会被触发,剩余的订单必须删除。

   if(n > 0)          // 1 or 2+ orders is an error, you need to delete everything
   {
      // delete all matching orders, except for partially filled ones
      MqlTradeRequestSyncOCO r;
      for(int i = 0i < n; ++i)
      {
         if(states[i] != ORDER_STATE_PARTIAL)
         {
            r.remove(tickets[i]) && r.completed();
         }
      }
   }
   ...

否则,没有订单。因此,需要检查是否有未结仓位:为此,我们使用另一个 trades 筛选对象,但是结果被添加到同一个接收数组 tickets 中。如果没有仓位,我们可下达一对新订单。

   else // n == 0
   {
      // if there are no open positions, place 2 orders
      if(!trades.select(tickets))
      {
         MqlTradeRequestSyncOCO r;
         SymbolMonitor sm(_Symbol);
         
         const double point = sm.get(SYMBOL_POINT);
         const double lot = Volume == 0 ? sm.get(SYMBOL_VOLUME_MIN) : Volume;
         const double buy = sm.get(SYMBOL_BID) + point * Distance2SLTP;
         const double sell = sm.get(SYMBOL_BID) - point * Distance2SLTP;
         
         r.buyStop(lotbuybuy - Distance2SLTP * point,
            buy + Distance2SLTP * point) && r.completed();
         r.sellStop(lotsellsell + Distance2SLTP * point,
            sell - Distance2SLTP * point) && r.completed();
      }
   }
}

我们使用默认设置在 EURUSD 对上运行测试程序中的 EA 交易。下图显示了测试过程。

测试程序中具有一对基于 OCO 策略的止损挂单的 EA 交易

测试程序中具有一对基于 OCO 策略的止损挂单的 EA 交易

在下达一对订单的阶段,我们将在日志中看到以下条目。

buy stop 0.01 EURUSD at 1.11151 sl: 1.10651 tp: 1.11651 (1.10646 / 1.10683)

sell stop 0.01 EURUSD at 1.10151 sl: 1.10651 tp: 1.09651 (1.10646 / 1.10683)

OnTradeTransaction(1)

TRADE_TRANSACTION_ORDER_ADD, #=2(ORDER_TYPE_BUY_STOP/ORDER_STATE_PLACED), ORDER_TIME_GTC, EURUSD, »

» @ 1.11151, SL=1.10651, TP=1.11651, V=0.01

OnTrade(1)

OnTradeTransaction(2)

TRADE_TRANSACTION_REQUEST

OnTradeTransaction(3)

TRADE_TRANSACTION_ORDER_ADD, #=3(ORDER_TYPE_SELL_STOP/ORDER_STATE_PLACED), ORDER_TIME_GTC, EURUSD, »

» @ 1.10151, SL=1.10651, TP=1.09651, V=0.01

OnTrade(2)

OnTradeTransaction(4)

TRADE_TRANSACTION_REQUEST

一旦其中一个订单被触发,就会发生以下情况:

order [#3 sell stop 0.01 EURUSD at 1.10151] triggered

deal #2 sell 0.01 EURUSD at 1.10150 done (based on order #3)

deal performed [#2 sell 0.01 EURUSD at 1.10150]

order performed sell 0.01 at 1.10150 [#3 sell stop 0.01 EURUSD at 1.10151]

OnTradeTransaction(5)

TRADE_TRANSACTION_DEAL_ADD, D=2(DEAL_TYPE_SELL), #=3(ORDER_TYPE_BUY/ORDER_STATE_STARTED), »

» EURUSD, @ 1.10150, SL=1.10651, TP=1.09651, V=0.01, P=3

OnTrade(3)

OnTradeTransaction(6)

TRADE_TRANSACTION_ORDER_DELETE, #=3(ORDER_TYPE_SELL_STOP/ORDER_STATE_FILLED), ORDER_TIME_GTC, »

» EURUSD, @ 1.10151, SL=1.10651, TP=1.09651, V=0.01, P=3

OnTrade(4)

OnTradeTransaction(7)

TRADE_TRANSACTION_HISTORY_ADD, #=3(ORDER_TYPE_SELL_STOP/ORDER_STATE_FILLED), ORDER_TIME_GTC, »

» EURUSD, @ 1.10151, SL=1.10651, TP=1.09651, P=3

order canceled [#2 buy stop 0.01 EURUSD at 1.11151]

OnTrade(5)

OnTradeTransaction(8)

TRADE_TRANSACTION_ORDER_DELETE, #=2(ORDER_TYPE_BUY_STOP/ORDER_STATE_CANCELED), ORDER_TIME_GTC, »

» EURUSD, @ 1.11151, SL=1.10651, TP=1.11651, V=0.01

OnTrade(6)

OnTradeTransaction(9)

TRADE_TRANSACTION_HISTORY_ADD, #=2(ORDER_TYPE_BUY_STOP/ORDER_STATE_CANCELED), ORDER_TIME_GTC, »

» EURUSD, @ 1.11151, SL=1.10651, TP=1.11651, V=0.01

OnTrade(7)

OnTradeTransaction(10)

TRADE_TRANSACTION_REQUEST

订单 #3 被自己删除,订单 #2 被我们的 EA 交易删除(取消)。

如果我们仅通过在设置中更改的 OnTrade 事件运行 EA 交易,我们应得到完全相似的财务结果(其他条件相同,例如,如果不包括分时报价生成中的随机延迟)。唯一不同的是 RunStrategy 函数调用次数。例如,对于 2022 年 4 个月 EURUSD,H1 的 88 笔交易,我们将获得以下 ExecutionCount 的近似指标(重要的是比率,而不是与你的经纪商的分时报价相关的绝对值):

  • OnTradeTransaction – 132
  • OnTrade – 438

这是一个实际证明,与 OnTrade 相比,基于 OnTradeTransaction 可以构建更具选择性的算法。

这个 OCO2.mq5 EA 交易版本对订单和仓位的反应相当简单。特别是之前的仓位被止损或者止盈平仓后,就会下达两个新订单。如果你手动删除其中一个订单,EA 交易将立即删除第二个订单,然后用当前价格的偏移量重新创建一对新订单。你可以通过嵌入一个类似于网格 EA 交易中所做的调度来改进这种行为,并且不会对历史中被取消的订单作出反应(但是,请注意,MQL5 没有提供查明订单是手动取消还是编程取消的方法)。在探讨 财经日历 API 时,我们将提供一个不同的方向来改进这个 EA 交易。

此外,在当前版本中已经提供了一个有趣的模式,与在输入变量 Expiration 中设置挂单到期日期相关。如果一对订单没有触发,然后,在它们到期之后,会立即相对于改变后的新当前价格下达一对新订单。作为一次独立的练习,你可以尝试通过更改 ExpirationDistance2SLTP 来优化测试程序中的 EA 交易。测试程序的编程工作,包括优化模式,将在 下一章中介绍。

以下是从 2021 年初开始的 16 个月内发现的 EURUSD 对的设置选项之一 (Distance2SLTP=250, Expiration=5000)。

OCO2 EA 交易的测试运行结果

OCO2 EA 交易的测试运行结果