发送交易请求:OrderSend 和 OrderSendAsync

为了执行交易操作,MQL5 API 提供了两个函数:OrderSend 和 OrderSendAsync。OrderSendOrderSendAsync。就像 OrderCheck 一样,它们对以 MqlTradeRequest 结构体形式传递的请求参数执行形式检查,如果成功,就将请求发送到服务器。

这两个函数的区别如下。OrderSend 期望订单在服务器上排队等待处理,从中接收有意义的数据,然后将数据填入 MqlTradeResult 结构体的字段中,该结构体作为第二个函数参数传递。无论服务器如何响应,OrderSendAsync 都会立即将控制权返回给调用代码。同时,除 retcode 之外,MqlTradeResult 结构体的所有字段中的重要信息都只填充到 request_id 中。使用此请求标识符,MQL 程序可以在 OnTradeTransaction 事件中接收有关处理此请求进度的更多信息。另一种方法是定期分析订单、交易和仓位列表。这种方法也可以在一个循环中完成,在出现通信问题时设置一些超时。

值得注意的是,尽管第二个函数的名称中有 Async(异步)后缀,但没有这个后缀的第一个函数也不是完全同步的。事实是,服务器处理订单的结果,特别是一笔交易的执行(也可能是基于一笔订单的几笔交易)和开仓,通常是在外部交易系统中异步发生。因此,OrderSend 函数还需要延迟收集和分析请求执行的结果,如果有必要,必须由 MQL 程序自己实现。我们将查看一个真正同步发送请求并在稍后接收其所有结果的示例(参见 MqlTradeSync.mqh)。

bool OrderSend(const MqlTradeRequest &request, MqlTradeResult &result)

如果在终端中对 request 结构体进行了成功的基本检查,并且在服务器上进行了一些附加检查,则该函数返回 true。但是,这仅表示服务器接受订单,并不保证交易操作的成功执行。

如果在服务器格式化对 OrderSend 调用的响应时,该数据已知,则交易服务器可以在返回的 result 结构体中填充 dealorder 字段值。但是,在一般情况下,执行交易或发出与订单相对应的限价订单事件可以在响应被发送到终端中的 MQL 程序之后进行。因此,对于任何类型的交易请求,当接收到执行 OrderSend 结果时,必须检查返回的 result 结构体中提供的交易服务器返回代码 retcode 和外部交易系统响应代码 retcode_external(如有必要)。你应根据这两个代码决定是等待服务器上的挂起操作,还是采用你自己的操作。

每个接受的订单都存储在交易服务器上等待处理,直到发生下列任何影响其生命周期的事件:

  • 当计数器请求出现时执行
  • 当执行价格到达时触发
  • 到期日期
  • 用户或 MQL 程序取消
  • 经纪商删除(例如,在清算或资金短缺的情况下 Stop Out

OrderSendAsync 原型完全重复 OrderSend 的原型。

bool OrderSendAsync(const MqlTradeRequest &request, MqlTradeResult &result)

该函数用于高频交易,根据算法的条件,浪费时间等待服务器的响应是不可接受的。使用 OrderSendAsync 不会加快服务器处理请求或向外部交易系统发送请求的速度。

注意!在测试程序中,OrderSendAsync 函数的工作方式类似于 OrderSend。这使得调试异步请求的挂起处理变得困难。

当请求成功发送到 MetaTrader 5 服务器时,该函数返回 true。但是,这并不意味着请求到达了服务器并被接受进行处理。同时,收到的 result 结构体中的响应代码包含 TRADE_RETCODE_PLACED (10008) 值,即“订单已下达”。

在处理接收到的请求时,服务器将向终端发送一条关于仓位、订单和交易当前状态变化的响应消息,这将导致 MQL 程序中生成 OnTrade 事件。此时,程序可以分析新的交易环境和账户历史。我们将在下面看到相关示例。

此外,可以使用 OnTradeTransaction 处理程序跟踪服务器上交易者请求执行的细节。同时要注意,执行一个交易请求后,会多次调用 OnTradeTransaction 处理程序。例如,当发送市场买入请求时,该请求会被服务器接受进行处理,为账户创建相应的“买入”订单,订单被执行并且交易被执行,结果该订单被从未结订单列表中移除并且被添加到订单历史中。然后,交易被添加到历史中,并创建一个新的仓位。对于所有这些事件,将调用 OnTradeTransaction 函数。

让我们从一个简单的 EA 交易示例 CustomOrderSend.mq5 开始。该示例允许你在输入参数中设置请求的所有字段,类似于 CustomOrderCheck.mq5,但进一步的区别在于其会向服务器发送请求,而不是在终端中进行简单的检查。在你的模拟账户上运行 EA 交易。完成实验后,不要忘记从图表中删除 EA 交易或关闭图表,这样就不会在每次终端启动时都发送测试请求。

这个新示例还有其他一些改进。首先,添加了 Async 输入参数。

input bool Async = false;

此选项允许选择用于将请求发送到服务器的函数。默认情况下,该参数等于 false,并且使用 OrderSend 函数。如果将其设置为 true,则会调用 OrderSendAsync

此外,通过这个示例,我们将开始说明并完成 TradeUtils.mqh 头文件中的一组特殊函数,这些函数用于简化机器人的编码。所有函数都放在名称空间 TU 中(是“Trade Utilities”的缩写),首先,我们引入一些函数,以便于向结构体日志输出 MqlTradeRequestMqlTradeResult

namespace TU
{
   string StringOf(const MqlTradeRequest &r)
   {
      SymbolMetrics p(r.symbol);
      
      // main block: action, type, symbol      
      string text = EnumToString(r.action);
      if(r.symbol != NULLtext += ", " + r.symbol;
      text += ", " + EnumToString(r.type);
      // volume block
      if(r.volume != 0text += ", V=" + p.StringOf(r.volumep.lotDigits);
      text += ", " + EnumToString(r.type_filling);
      // block of all prices 
      if(r.price != 0text += ", @ " + p.StringOf(r.price);
      if(r.stoplimit != 0text += ", X=" + p.StringOf(r.stoplimit);
      if(r.sl != 0text += ", SL=" + p.StringOf(r.sl);
      if(r.tp != 0text += ", TP=" + p.StringOf(r.tp);
      if(r.deviation != 0text += ", D=" + (string)r.deviation;
      // pending orders expiration block
      if(IsPendingType(r.type)) text += ", " + EnumToString(r.type_time);
      if(r.expiration != 0text += ", " + TimeToString(r.expiration);
      // modification block
      if(r.order != 0text += ", #=" + (string)r.order;
      if(r.position != 0text += ", #P=" + (string)r.position;
      if(r.position_by != 0text += ", #b=" + (string)r.position_by;
      // auxiliary data
      if(r.magic != 0text += ", M=" + (string)r.magic;
      if(StringLen(r.comment)) text += ", " + r.comment;
      
      return text;
   }
   
   string StringOf(const MqlTradeResult &r)
   {
      string text = TRCSTR(r.retcode);
      if(r.deal != 0text += ", D=" + (string)r.deal;
      if(r.order != 0text += ", #=" + (string)r.order;
      if(r.volume != 0text += ", V=" + (string)r.volume;
      if(r.price != 0text += ", @ " + (string)r.price
      if(r.bid != 0text += ", Bid=" + (string)r.bid
      if(r.ask != 0text += ", Ask=" + (string)r.ask
      if(StringLen(r.comment)) text += ", " + r.comment;
      if(r.request_id != 0text += ", Req=" + (string)r.request_id;
      if(r.retcode_external != 0text += ", Ext=" + (string)r.retcode_external;
      
      return text;
   }
   ...
};

这些函数的目的是以简洁且方便的形式提供所有重要的(非空)字段:它们显示在一行中,每个字段都有一个唯一的名称。

可以看到,该函数为 MqlTradeRequest 使用了 SymbolMetrics 类。这样有助于将同一金融工具的多个价格或交易量进行标准化。不要忘记,价格和交易量的标准化是准备正确交易请求的先决条件。

   class SymbolMetrics
   {
   public:
      const string symbol;
      const int digits;
      const int lotDigits;
      
      SymbolMetrics(const string s): symbol(s),
         digits((int)SymbolInfoInteger(sSYMBOL_DIGITS)),
         lotDigits((int)MathLog10(1.0 / SymbolInfoDouble(sSYMBOL_VOLUME_STEP)))
      { }
         
      double price(const double p)
      {
         return TU::NormalizePrice(psymbol);
      }
      
      double volume(const double v)
      {
         return TU::NormalizeLot(vsymbol);
      }
   
      string StringOf(const double vconst int d = INT_MAX)
      {
         return DoubleToString(vd == INT_MAX ? digits : d);
      }
   };

这些值的直接标准化委托给 NormalizePriceNormalizeLot 辅助函数(后者的方案与我们在文件 LotMarginExposure.mqh 中看到的完全相同)。

   double NormalizePrice(const double priceconst string symbol = NULL)
   {
      const double tick = SymbolInfoDouble(symbolSYMBOL_TRADE_TICK_SIZE);
      return MathRound(price / tick) * tick;
   }

如果我们连接 TradeUtils.mqh,示例 CustomOrderSend.mq5 具有以下形式(省略的代码片段 '...' 与 CustomOrderCheck.mq5 相同)。

void OnTimer()
{
   ...
   MqlTradeRequest request = {};
   MqlTradeCheckResult result = {};
   
   TU::SymbolMetrics sm(symbol);
   
   // fill in the request structure
   request.action = Action;
   request.magic = Magic;
   request.order = Order;
   request.symbol = symbol;
   request.volume = sm.volume(volume);
   request.price = sm.price(price);
   request.stoplimit = sm.price(StopLimit);
   request.sl = sm.price(SL);
   request.tp = sm.price(TP);
   request.deviation = Deviation;
   request.type = Type;
   request.type_filling = Filling;
   request.type_time = ExpirationType;
   request.expiration = ExpirationTime;
   request.comment = Comment;
   request.position = Position;
   request.position_by = PositionBy;
   
   // send the request and display the result
   ResetLastError();
   if(Async)
   {
      PRTF(OrderSendAsync(requestresult));
   }
   else
   {
      PRTF(OrderSend(requestresult));
   }
   Print(TU::StringOf(request));
   Print(TU::StringOf(result));
}

由于价格和交易量现已标准化,你可以尝试在相应的输入参数中输入不规则的值。它们通常是在计算过程中在程序中获得的,我们的代码可根据交易品种规范对其进行转换。

在默认设置下,EA 交易会创建一个按市场买入当前金融工具的最小手数请求,并使用 OrderSend 函数进行请求。

OrderSend(request,result)=true / ok

TRADE_ACTION_DEAL, EURUSD, ORDER_TYPE_BUY, V=0.01, ORDER_FILLING_FOK, @ 1.12462

DONE, D=1250236209, #=1267684253, V=0.01, @ 1.12462, Bid=1.12456, Ask=1.12462, Request executed, Req=1

一般而言,在允许交易的情况下,该操作应成功完成(状态为“DONE”,注释为“Request executed”)。在 result 结构体中,我们会立即收到交易编号 D

如果我们打开 EA 交易设置并将 Async 参数的值替换为 true,我们将发送一个类似的请求,但会使用 OrderSendAsync 函数。

OrderSendAsync(request,result)=true / ok

TRADE_ACTION_DEAL, EURUSD, ORDER_TYPE_BUY, V=0.01, ORDER_FILLING_FOK, @ 1.12449

PLACED, Order placed, Req=2

在这种情况下,状态为“PLACED”,函数返回时的交易编号未知。我们只知道唯一的请求 ID Req=2。要获得交易和仓位编号,你需要在 OnTradeTransaction 处理程序中截取具有相同请求 ID 的TRADE_TRANSACTION_REQUEST 消息,其中填充的结构体将作为 MqlTradeResult 参数接收。

从用户的角度来看,这两个请求应一样快。

这两个函数的性能可以在 MQL 程序的代码中使用 EA 交易的另一个示例直接进行比较(参见 同步和异步请求一节),我们将在研究交易事件模型后探讨这一点。

应注意的是,交易事件被发送到 OnTradeTransaction 处理程序(如果代码中有),不管哪个函数用于发送请求:OrderSendOrderSendAsync。情况如下:若应用 OrderSend,有关订单执行的部分或全部信息在接收 MqlTradeResult 结构体中立即可用。但一般情况下,其结果是随时间和交易量分布的,例如,当一个订单被“填充”成几个交易时。那么可以从交易事件中或者通过分析交易和订单的历史来获得完整的信息。

如果你尝试故意发送不正确的请求,例如,将订单类型更改为挂单 ORDER_TYPE_BUY_STOP,你将会收到一条错误消息,因为对于此类订单,你应使用 TRADE_ACTION_PENDING 操作。此外,它们应位于与当前价格有一段距离的位置(我们默认使用市场价格)。在这个测试之前,重要的是不要忘记将查询模式改回同步 (Async=false),这样在结束 OrderSend 调用之后便能立即看到 MqlTradeResult 结构体中的错误。否则,OrderSendAsync 虽然返回 true,但顺序仍然不会被设置,并且程序只能在 OnTradeTransaction 中接收到相关信息,而我们还没有这个信息。

OrderSend(request,result)=false / TRADE_SEND_FAILED(4756)

TRADE_ACTION_DEAL, EURUSD, ORDER_TYPE_BUY_STOP, V=0.01, ORDER_FILLING_FOK, @ 1.12452, ORDER_TIME_GTC

REQUOTE, Bid=1.12449, Ask=1.12452, Requote, Req=5

在这种情况下,错误会报告无效的 Requote 价格。

使用函数执行特定交易操作的示例将在后面几个章节中介绍。