修改挂单

MetaTrader 5 允许你修改挂单的某些特性,包括激活价格、保护水平和到期日期。订单类型或交易量等主要特性不能更改。若需更改,你应 删除 该订单并将其替换为另一个订单。服务器本身可以更改订单类型的唯一情况是激活止损限价订单,该订单会变成相应的限价订单。

要以编程方式修改订单,需要通过 TRADE_ACTION_MODIFY 操作执行:需要将该常量写入 action 字段(属于 MqlTradeRequest 结构体),而且需要在通过 OrderSend or OrderSendAsync 函数将其发送到服务器之前执行。修改订单的订单号在 order 字段中指示。考虑到 action and order,此操作的完整必填字段列表包括:

  • action
  • order
  • price
  • type_time(默认值 0 对应于 ORDER_TIME_GTC)
  • expiration(默认值 0 对于 ORDER_TIME_GTC)
  • type_filling(默认值 0 对应于 ORDER_FILLING_FOK)
  • stoplimit(仅适用于 ORDER_TYPE_BUY_STOP_LIMIT 和 ORDER_TYPE_SELL_STOP_LIMIT 类型的订单)

选填字段:

  • sl
  • tp

如果已经为订单设置了保护水平,则应注明保护水平,以便保存。零值表示删除了 Stop Loss 和/或 Take Profit

MqlTradeRequestSync 结构体 (MqlTradeSync.mqh) 中,订单修改的实现包含在 modify 方法中。

struct MqlTradeRequestSyncpublic MqlTradeRequest
{
   ...
   bool modify(const ulong ticket,
      const double pconst double stop = 0const double take = 0,
      ENUM_ORDER_TYPE_TIME duration = ORDER_TIME_GTCdatetime until = 0,
      const double origin = 0)
   {
      if(!OrderSelect(ticket)) return false;
      
      action = TRADE_ACTION_MODIFY;
      order = ticket;
      
      // the following fields are needed for checks inside subfunctions
      type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
      symbol = OrderGetString(ORDER_SYMBOL);
      volume = OrderGetDouble(ORDER_VOLUME_CURRENT);
      
      if(!setVolumePrices(volumepstoptakeorigin)) return false;
      if(!setExpiration(durationuntil)) return false;
      ZeroMemory(result);
      return OrderSend(thisresult);
   }

if 运算符的专用分支中,请求的实际执行再次在 completed 方法中完成。

   bool completed()
   {
      ...
      else if(action == TRADE_ACTION_MODIFY)
      {
         result.order = order;
         result.bid = sl;
         result.ask = tp;
         result.price = price;
         result.volume = stoplimit;
         return result.modified(timeout);
      }
      ...
   }

为了让 MqlTradeResultSync 结构体知道已编辑订单特性的新值,并能够将其与结果进行比较,我们将这些新值写在自由字段中(在这种类型的请求中,服务器不会填充这些字段)。此外,在 modified 方法中,结果结构体正在等待应用修改。

struct MqlTradeResultSyncpublic MqlTradeResult
{
   ...
   bool modified(const ulong msc = 1000)
   {
      if(retcode != TRADE_RETCODE_DONE && retcode != TRADE_RETCODE_PLACED)
      {
         return false;
      }
   
      if(!wait(orderModifiedmsc))
      {
         Print("Order not found in environment: #" + (string)order);
         return false;
      }
      return true;
   }
   
   static bool orderModified(MqlTradeResultSync &ref)
   {
      if(!(OrderSelect(ref.order) || HistoryOrderSelect(ref.order)))
      {
         Print("OrderSelect failed: #=" + (string)ref.order);
         return false;
      }
      return TU::Equal(ref.bidOrderGetDouble(ORDER_SL))
         && TU::Equal(ref.askOrderGetDouble(ORDER_TP))
         && TU::Equal(ref.priceOrderGetDouble(ORDER_PRICE_OPEN))
         && TU::Equal(ref.volumeOrderGetDouble(ORDER_PRICE_STOPLIMIT));
   }

此处我们看到如何使用 OrderGetDouble 函数读取订单特性,并与指定的值进行比较。所有这一切都按照我们已经熟悉的过程发生,在等待函数内的一个循环中,在 msc 的某个超时时间内(默认为 1000 毫秒)。

例如,我们使用 EA 交易 PendingOrderModify.mq5,同时从 PendingOrderSend.mq5 继承一些代码片段。具体来说,是一组输入参数和用于创建新订单的 PlaceOrder 函数。如果给定的交易品种和 Magic 号组合没有订单,则在第一次启动时使用,从而确保 EA 交易有修改内容。

需要一个新函数来查找合适的订单:GetMyOrder。该函数非常类似于 GetMyPosition 函数,后者用于在 仓位跟踪 示例 (TrailingStop.mq5) 中查找合适的仓位。GetMyOrder 中使用了内置 MQL5 API 函数,从其名称中就能大致推断其用途,技术说明将在 单独的章节中介绍。

ulong GetMyOrder(const string nameconst ulong magic)
{
   for(int i = 0i < OrdersTotal(); ++i)
   {
      ulong t = OrderGetTicket(i);
      if(OrderGetInteger(ORDER_MAGIC) == magic
         && OrderGetString(ORDER_SYMBOL) == name)
      {
         return t;
      }
   }
   
   return 0;
}

现在缺少输入参数 Distance2SLTP。取而代之的是,新的 EA 交易将自动计算价格的日线范围,并在该范围的一半处设置保护水平。每天开始时,将重新计算 sltp 字段中的范围和新水平。将根据新值生成订单修改请求。

那些触发并转为仓位的挂单将在达到 Stop LossTake Profit 时平仓。终端可以通知 MQL 程序关于挂单激活和平仓的信息,前提是你在终端中说明了 交易事件 处理程序。例如,如果存在未结仓位,便可以避免创建新订单。但也可以使用当前策略。所以,我们以后再处理事件。

EA 交易的主要逻辑在 OnTick 处理程序中实现。

void OnTick()
{
   static datetime lastDay = 0;
   static const uint DAYLONG = 60 * 60 * 24// number of seconds in a day
   //discard the "fractional" part, i.e. time
   if(TimeTradeServer() / DAYLONG * DAYLONG == lastDayreturn;
   ...

该函数开头的两行确保算法在每天开始时运行一次。为此,我们计算不带时间的当前日期,并将其与包含上次成功日期的 lastDay 变量值进行比较。当然,在该函数结束时,成功或错误状态会变得很清楚,所以我们稍后再来讨论它。

接下来,计算前一天的价格范围。

   const string symbol = StringLen(Symbol) == 0 ? _Symbol : Symbol;
   const double range = iHigh(symbolPERIOD_D11) - iLow(symbolPERIOD_D11);
   Print("Autodetected daily range: ", (float)range);
   ...

根据 GetMyOrder 函数中是否有订单,我们将通过 PlaceOrder 创建一个新订单,或者使用 ModifyOrder 编辑一个现有订单。

   uint retcode = 0;
   ulong ticket = GetMyOrder(symbolMagic);
   if(!ticket)
   {
      retcode = PlaceOrder((ENUM_ORDER_TYPE)TypesymbolVolume,
         rangeExpirationUntilMagic);
   }
   else
   {
      retcode = ModifyOrder(ticketrangeExpirationUntil);
   }
   ...

PlaceOrderModifyOrder 这两个函数都基于 EA 交易的输入参数和找到的价格范围运行。这两个含税可返回请求状态,需要以某种方式对其进行分析,以决定采取何种操作:

  • 如果请求成功,则更新 lastDay 变量(订单已更新,EA 交易休眠至第二天开始)
  • 如果出现临时问题(例如,交易时段还没有开始),则将前一日期留在 lastDay 中,在下一个分时报价处再试一次
  • 如果检测到严重问题,则停止 EA 交易(例如,所选订单类型或交易方向不允许出现在某个交易品种上)

   ...
   if(/* some kind of retcode analysis */)
   {
      lastDay = TimeTradeServer() / DAYLONG * DAYLONG;
   }
}

平仓:全部和部分一节中,我们使用了一个带有 IS_TANGIBLE 宏的简化分析,该宏给出了“是”和“否”类别的答案,以指示是否存在错误。显然,这种方法需要改进,我们将很快回到这个问题上来。现在,我们将重点介绍 EA 交易的主要功能。

PlaceOrder 函数的源代码与前一个示例相比几乎没有变化。 ModifyOrder 如下所示。

注意,我们是根据日线范围来确定订单位置的,并对日线范围应用了系数表。但是,原则并没有改变,因为我们现在有两个处理订单的函数,PlaceOrderModifyOrderCoefficients 表被放置在全局背景中。我们在此不再赘述,将直接讨论 ModifyOrder 函数。

uint ModifyOrder(const ulong ticketconst double range,
   ENUM_ORDER_TYPE_TIME expirationdatetime until)
{
   // default values
   const string symbol = OrderGetString(ORDER_SYMBOL);
   const double point = SymbolInfoDouble(symbolSYMBOL_POINT);
   ...

价格水平的计算取决于订单类型和传递的范围。

   const ENUM_ORDER_TYPE type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
   const double price = TU::GetCurrentPrice(typesymbol) + range * Coefficients[type];
   
   // origin is filled only for orders *_STOP_LIMIT
   const bool stopLimit =
      type == ORDER_TYPE_BUY_STOP_LIMIT ||
      type == ORDER_TYPE_SELL_STOP_LIMIT;
   const double origin = stopLimit ? TU::GetCurrentPrice(typesymbol) : 0
   
   TU::TradeDirection dir(type);
   const int sltp = (int)(range / 2 / point);
   const double stop = sltp == 0 ? 0 :
      dir.negative(stopLimit ? origin : pricesltp * point);
   const double take = sltp == 0 ? 0 :
      dir.positive(stopLimit ? origin : pricesltp * point);
   ...

计算完所有值后,我们将创建一个 MqlTradeRequestSync 结构体的对象并执行请求。

   MqlTradeRequestSync request(symbol);
   
   ResetLastError();
   // pass the data for the fields, send the order and wait for the result
   if(request.modify(ticketpricestoptakeexpirationuntilorigin)
      && request.completed())
   {
      Print("OK order modified: #="ticket);
   }
   
   Print(TU::StringOf(request));
   Print(TU::StringOf(request.result));
   return request.result.retcode;
}

为了分析我们必须在 OnTick 内部的调用块中执行的 retcode,开发了一种新的机制来补充文件 TradeRetcode.mqh。所有服务器返回码都被分成几个“严重性”组,由 TRADE_RETCODE_SEVERITY 枚举的元素进行描述。

enum TRADE_RETCODE_SEVERITY
{
   SEVERITY_UNDEFINED,   // something non-standard - just output to the log
   SEVERITY_NORMAL,      // normal operation
   SEVERITY_RETRY,       // try updating environment/prices again (probably several times) 
   SEVERITY_TRY_LATER,   // we should wait and try again
   SEVERITY_REJECT,      // request denied, probably(!) you can try again
                         // 
   SEVERITY_INVALID,     // need to fix the request
   SEVERITY_LIMITS,      // need to check the limits and fix the request
   SEVERITY_PERMISSIONS// it is required to notify the user and change the program/terminal settings
   SEVERITY_ERROR,       // stop, output information to the log and to the user
};

简言之,前半部分对应于可恢复的错误:通常只需要等待一段时间,然后重试请求。后半部分要求你更改请求的内容,检查账户或交易品种设置,程序的权限,最坏的情况下,需要停止交易。如果愿意的话,可以在 SEVERITY_REJECT 之前而不是之后画一条条件分隔线,因为它现在是高亮显示的。

通过 TradeCodeSeverity 函数(提供了缩写版本)将所有代码分组。

TRADE_RETCODE_SEVERITY TradeCodeSeverity(const uint retcode)
{
   static const TRADE_RETCODE_SEVERITY severities[] =
   {
      ...
      SEVERITY_RETRY,       // REQUOTE (10004)
      SEVERITY_UNDEFINED,     
      SEVERITY_REJECT,      // REJECT (10006)
      SEVERITY_NORMAL,      // CANCEL (10007)
      SEVERITY_NORMAL,      // PLACED (10008)
      SEVERITY_NORMAL,      // DONE (10009)
      SEVERITY_NORMAL,      // DONE_PARTIAL (10010)
      SEVERITY_ERROR,       // ERROR (10011)
      SEVERITY_RETRY,       // TIMEOUT (10012)
      SEVERITY_INVALID,     // INVALID (10013)
      SEVERITY_INVALID,     // INVALID_VOLUME (10014)
      SEVERITY_INVALID,     // INVALID_PRICE (10015)
      SEVERITY_INVALID,     // INVALID_STOPS (10016)
      SEVERITY_PERMISSIONS// TRADE_DISABLED (10017)
      SEVERITY_TRY_LATER,   // MARKET_CLOSED (10018)
      SEVERITY_LIMITS,      // NO_MONEY (10019)
      ...
   };
   
   if(retcode == 0return SEVERITY_NORMAL;
   if(retcode < 10000 || retcode > HEDGE_PROHIBITEDreturn SEVERITY_UNDEFINED;
   return severities[retcode - 10000];
}

借助这个功能,OnTick 处理程序可以补充“智能”错误处理。静态变量 RetryFrequency 存储了程序在出现非关键错误时尝试重复请求的频率。上次时间(比如上次进行重试的时间)存储在 RetryRecordTime 变量中。

void OnTick()
{
   ...
   const static int DEFAULT_RETRY_TIMEOUT = 1// seconds
   static int RetryFrequency = DEFAULT_RETRY_TIMEOUT;
   static datetime RetryRecordTime = 0;
   if(TimeTradeServer() - RetryRecordTime < RetryFrequencyreturn;
   ...

一旦 PlaceOrderModifyOrder 函数返回 retcode 值,我们就知道其严重程度,并且根据严重程度,我们从三个选项中选择一个:停止 EA 交易、等待超时或常规操作(lastDay 的当天标记订单成功修改)。

   const TRADE_RETCODE_SEVERITY severity = TradeCodeSeverity(retcode);
   if(severity >= SEVERITY_INVALID)
   {
      Alert("Can't place/modify pending order, EA is stopped");
      RetryFrequency = INT_MAX;
   }
   else if(severity >= SEVERITY_RETRY)
   {
      RetryFrequency += (int)sqrt(RetryFrequency + 1);
      RetryRecordTime = TimeTradeServer();
      PrintFormat("Problems detected, waiting for better conditions "
         "(timeout enlarged to %d seconds)",
         RetryFrequency);
   }
   else
   {
      if(RetryFrequency > DEFAULT_RETRY_TIMEOUT)
      {
         RetryFrequency = DEFAULT_RETRY_TIMEOUT;
         PrintFormat("Timeout restored to %d second"RetryFrequency);
      }
      lastDay = TimeTradeServer() / DAYLONG * DAYLONG;
   }

在被分类为可解决重复问题的情况下,RetryFrequency 超时会随着每个后续错误逐渐增加,但是当请求被成功处理时重置为 1 秒。

应当注意,所应用的结构体 MqlTradeRequestSync 的方法进而检查大量参数组合的正确性,如果发现问题,会在 SendRequest 调用之前中断该过程。默认情况下启用此行为,但是可以通过在 MqlTradeSync.mqh#include 指令之前定义一个空的 RETURN(X) 宏来禁用它。

#define RETURN(X)
#include <MQL5Book/MqlTradeSync.mqh>

使用这个宏定义,检查仅在日志中打印警告,但会继续执行方法,直到 SendRequest 调用。

在任何情况下,在调用 MqlTradeResultSync 结构体的一个或另一个方法后,错误代码将被添加到 retcode 中。这将由服务器或 MqlTradeRequestSync 结构体的检查算法来完成(此处我们利用了 MqlTradeResultSync 实例包含在 MqlTradeRequestSync 中这一事实)。为了简洁起见,此处不提供对返回错误代码以及 MqlTradeRequestSync 方法中使用 RETURN 宏的说明。感兴趣的读者可以在 MqlTradeSync.mqh 文件中看到完整的源代码。

在启用可视化模式的情况下,我们在测试程序中运行 EA 交易 PendingOrderModify.mq5,并使用 XAUUSD, H1 的数据(所有分时报价或真实分时报价模式)。使用默认设置,EA 交易将以最小手数下达 ORDER_TYPE_BUY_STOP 类型的订单。我们从日志和交易历史可以确认,该程序在每天开始时下达挂单并进行修改。

2022.01.03 01:05:00 Autodetected daily range: 14.37

2022.01.03 01:05:00 buy stop 0.01 XAUUSD at 1845.73 sl: 1838.55 tp: 1852.91 (1830.63 / 1831.36)

2022.01.03 01:05:00 OK order placed: #=2

2022.01.03 01:05:00 TRADE_ACTION_PENDING, XAUUSD, ORDER_TYPE_BUY_STOP, V=0.01, ORDER_FILLING_FOK, »

» @ 1845.73, SL=1838.55, TP=1852.91, ORDER_TIME_GTC, M=1234567890

2022.01.03 01:05:00 DONE, #=2, V=0.01, Bid=1830.63, Ask=1831.36, Request executed

2022.01.04 01:05:00 Autodetected daily range: 33.5

2022.01.04 01:05:00 order modified [#2 buy stop 0.01 XAUUSD at 1836.56]

2022.01.04 01:05:00 OK order modified: #=2

2022.01.04 01:05:00 TRADE_ACTION_MODIFY, XAUUSD, ORDER_TYPE_BUY_STOP, V=0.01, ORDER_FILLING_FOK, »

» @ 1836.56, SL=1819.81, TP=1853.31, ORDER_TIME_GTC, #=2

2022.01.04 01:05:00 DONE, #=2, @ 1836.56, Bid=1819.81, Ask=1853.31, Request executed, Req=1

2022.01.05 01:05:00 Autodetected daily range: 18.23

2022.01.05 01:05:00 order modified [#2 buy stop 0.01 XAUUSD at 1832.56]

2022.01.05 01:05:00 OK order modified: #=2

2022.01.05 01:05:00 TRADE_ACTION_MODIFY, XAUUSD, ORDER_TYPE_BUY_STOP, V=0.01, ORDER_FILLING_FOK, »

» @ 1832.56, SL=1823.45, TP=1841.67, ORDER_TIME_GTC, #=2

2022.01.05 01:05:00 DONE, #=2, @ 1832.56, Bid=1823.45, Ask=1841.67, Request executed, Req=2

...

2022.01.11 01:05:00 Autodetected daily range: 11.96

2022.01.11 01:05:00 order modified [#2 buy stop 0.01 XAUUSD at 1812.91]

2022.01.11 01:05:00 OK order modified: #=2

2022.01.11 01:05:00 TRADE_ACTION_MODIFY, XAUUSD, ORDER_TYPE_BUY_STOP, V=0.01, ORDER_FILLING_FOK, »

» @ 1812.91, SL=1806.93, TP=1818.89, ORDER_TIME_GTC, #=2

2022.01.11 01:05:00 DONE, #=2, @ 1812.91, Bid=1806.93, Ask=1818.89, Request executed, Req=6

2022.01.11 18:10:58 order [#2 buy stop 0.01 XAUUSD at 1812.91] triggered

2022.01.11 18:10:58 deal #2 buy 0.01 XAUUSD at 1812.91 done (based on order #2)

2022.01.11 18:10:58 deal performed [#2 buy 0.01 XAUUSD at 1812.91]

2022.01.11 18:10:58 order performed buy 0.01 at 1812.91 [#2 buy stop 0.01 XAUUSD at 1812.91]

2022.01.11 20:28:59 take profit triggered #2 buy 0.01 XAUUSD 1812.91 sl: 1806.93 tp: 1818.89 »

» [#3 sell 0.01 XAUUSD at 1818.89]

2022.01.11 20:28:59 deal #3 sell 0.01 XAUUSD at 1818.91 done (based on order #3)

2022.01.11 20:28:59 deal performed [#3 sell 0.01 XAUUSD at 1818.91]

2022.01.11 20:28:59 order performed sell 0.01 at 1818.91 [#3 sell 0.01 XAUUSD at 1818.89]

2022.01.12 01:05:00 Autodetected daily range: 23.28

2022.01.12 01:05:00 buy stop 0.01 XAUUSD at 1843.77 sl: 1832.14 tp: 1855.40 (1820.14 / 1820.49)

2022.01.12 01:05:00 OK order placed: #=4

2022.01.12 01:05:00 TRADE_ACTION_PENDING, XAUUSD, ORDER_TYPE_BUY_STOP, V=0.01, ORDER_FILLING_FOK, »

» @ 1843.77, SL=1832.14, TP=1855.40, ORDER_TIME_GTC, M=1234567890

2022.01.12 01:05:00 DONE, #=4, V=0.01, Bid=1820.14, Ask=1820.49, Request executed, Req=7

订单可以在任何时候触发,一段时间后通过止损或止盈进行平仓(如上面的代码所示)。

在某些情况下,可能会出现第二天开始时该仓位仍然存在的情况,然后除此之外还会创建一个新订单,如下图所示。

EA 交易根据测试程序中的挂单制定的交易策略

EA 交易根据测试程序中的挂单制定的交易策略

请注意,由于我们需要请求 PERIOD_D1 时间范围的报价以计算日线范围,因此除了当前的工作图表之外,可视化测试程序还会打开相应的图表。这种服务不仅适用于工作时间范围以外的时间范围,还适用于其他交易品种。这在开发 多货币 EA 交易时特别有用。

要检查错误处理是如何工作的,请尝试禁用 EA 交易类型的交易。日志将包含以下内容:

Autodetected daily range: 34.48
TRADE_ACTION_PENDING, XAUUSD, ORDER_TYPE_BUY_STOP, V=0.01, ORDER_FILLING_FOK, »
  » @ 1975.73, SL=1958.49, TP=1992.97, ORDER_TIME_GTC, M=1234567890
CLIENT_DISABLES_AT, AutoTrading disabled by client
Alert: Can't place/modify pending order, EA is stopped

此错误非常严重,EA 交易将停止工作。

为了演示一个更简单的错误,我们可以使用 OnTimer 处理程序来代替 OnTick。然后,若在交易时段仅占一天的一部分的交易品种上启动相同的 EA 交易,则将周期性地产生一系列关于关闭市场(“市场关闭”)的非关键错误。在这种情况下,EA 交易会不断尝试开始交易,不断增加等待时间。

特别是,很容易在测试程序中进行检查,它允许你为任何交易品种设置任意的交易时段。在 Settings 选项卡上 Delays 下拉菜单的右边,有一个按钮可打开 Trade setup 对话框。此处,应包括 Use your settings 选项,并在 Trade 选项卡上向 Non-trading periods 表中添加至少一条记录。

在测试程序中设置非交易时段

在测试程序中设置非交易时段

请注意,此处设置的是非交易时段,而非交易时段,即与交易品种规格相比,此设置的作用正好相反。

许多与交易限制相关的潜在错误基本上可以通过使用某个类进行环境分析来消除,比如 Permissions 类,之前在 账户交易的限制和权限一节中介绍过。