轻松快捷开发 MetaTrader 程序的函数库(第 三十四部分):延后交易请求 - 在特定条件下删除和修改订单与持仓(基础篇)
📘

轻松快捷开发 MetaTrader 程序的函数库(第 三十四部分):延后交易请求 - 在特定条件下删除和修改订单与持仓(基础篇)

第 1/3 篇

「延后交易请求:条件触发下的订单与持仓改写」

在 MT5 里做自动化交易,经常会遇到「现在不满足、但价格走到某处就该改单/删单」的场景。把这类逻辑硬塞进主循环,不但代码乱,还容易在高频 tick 里重复触发。 本系列第三十四部分给出的思路是:把交易请求先存进函数库,等特定条件成立再统一处理——比如挂单触及某价位后自动改止损,或持仓浮亏扩大到阈值时延迟平仓。 原文示例发布于 2020-05-11,截至统计时获 3098 次查看、30 条讨论,说明这类「延后请求」库在实际搬砖中需求不低。外汇与贵金属杠杆高,这类自动改写逻辑若条件设错,可能瞬间放大回撤,验证前务必用策略测试器跑历史数据。

◍ 延后请求交易的功能拼图

这一节把延后请求交易的功能往完整方向补:删除挂单、改止损止盈、调挂单参数都会做进去。做完之后,整套机制就能支撑简单自定义策略,或按用户设定的条件触发 EA 行为逻辑。 图形外壳就绪后,还能在 EA 运行期直接从自身拉出可视构造函数,现场拼行为逻辑;这部分等函数库图形层完成后再补一个简例。 现在已能扩展挂单的附加类型,比如给 MQL4 补 StopLimit,后续函数库能力够了我再做。由此可派生 BuyTime、SellTime、BuyTimeStop、SellTimeStop 这类全新挂单类别。 目前还差若干图形结构,暂不能拼出完整自定义订单;等函数库对应能力到位再回头补。这一节里我会先修抽象延后请求子类里的几个瑕疵,并按计划把缺的交易功能写出来。

给挂单名称剥掉多余的“挂单”前缀

MQL5 标准库里没有直出挂单名称的函数,只有 OrderTypeDescription() 能返回“说明+名称”的组合串。想拿到纯名称,就得从返回结果里把描述文本剔掉,只留订单类型本身。 我们在 DELib.mqh 里改了 OrderTypeDescription():新增 descr 形参(默认 true),当置为 false 时不再拼接 MSG_ORD_PENDING 这类“挂单”初级文本。这样同一函数既能带前缀显示完整描述,也能在日志、面板里只吐“Buy Limit / Sell Stop”等干净名字。 顺带把延后请求对象的六个子类(开仓、平仓、改止损盈、下挂单、删挂单、改挂单属性)的描述生成逻辑修了——在摘要里补上订单/持仓类型文本,再挂单据与逗号分隔的延后请求 ID。当前结构先描述后 ID,虽不完美但能跑。 CTradingControl 与 CEngine 两层都加了删挂单、改止损盈、改挂单参数的延后请求创建方法,后者仅转发前者的调用并返回结果。外汇与贵金属杠杆交易高风险,延后请求失败概率随滑点与流动性突变上升,改完请在 MT5 策略测试器用极端点差样本验证转发链路。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the order name                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string OrderTypeDescription(class="kw">const ENUM_ORDER_TYPE type,class="type">bool as_order=true,class="type">bool prefix_for_market_order=true,class="type">bool descr=true)
  {
   class="type">class="kw">string pref=
     (
      !prefix_for_market_order ? "" :
      class="macro">#ifdef __MQL5__ CMessage::Text(MSG_ORD_MARKET)
      class="macro">#elseclass=class="str">"cmt">/*__MQL4__*/(as_order ? CMessage::Text(MSG_ORD_MARKET) : CMessage::Text(MSG_ORD_POSITION)) class="macro">#endif
     );
   class="kw">return
     (
      type==ORDER_TYPE_BUY_LIMIT       ?  (descr ? CMessage::Text(MSG_ORD_PENDING) : "")+" Buy Limit"       :
      type==ORDER_TYPE_BUY_STOP         ?  (descr ? CMessage::Text(MSG_ORD_PENDING) : "")+" Buy Stop"         :
      type==ORDER_TYPE_SELL_LIMIT       ?  (descr ? CMessage::Text(MSG_ORD_PENDING) : "")+" Sell Limit"       :
      type==ORDER_TYPE_SELL_STOP        ?  (descr ? CMessage::Text(MSG_ORD_PENDING) : "")+" Sell Stop"        :
      class="macro">#ifdef __MQL5__
      type==ORDER_TYPE_BUY_STOP_LIMIT  ?  (descr ? CMessage::Text(MSG_ORD_PENDING) : "")+" Buy Stop Limit"  :
      type==ORDER_TYPE_SELL_STOP_LIMIT ?  (descr ? CMessage::Text(MSG_ORD_PENDING) : "")+" Sell Stop Limit" :
      type==ORDER_TYPE_CLOSE_BY         ?  CMessage::Text(MSG_ORD_CLOSE_BY)                                      :
      class="macro">#else
      type==ORDER_TYPE_BALANCE          ?  CMessage::Text(MSG_LIB_PROP_BALANCE)                                  :
      class="macro">#endif
      ""
     );
  }

「挂单请求类的头部文本拼接逻辑」

在 MT5 的 pending request 封装类里,Header() 方法负责把一笔挂单请求缩写成一行可读状态文本,方便在日志或面板里快速分辨动作类型。 以 CPendReqOpen::Header() 为例,它先通过 PositionTypeDescription() 把存在 PEND_REQ_PROP_MQL_REQ_TYPE 里的枚举值转成文字(Buy / Sell 等),再拼上 MSG_LIB_TEXT_PEND_REQUEST_STATUS_OPEN 与请求 ID。 CPendReqClose 和 CPendReqSLTP 的结构几乎一致,只是状态前缀宏不同,且 Close 类额外读取 PEND_REQ_PROP_MQL_REQ_POSITION 把持仓编号也带出来,格式为「动作 类型 #持仓号, ID #请求号」。 你在自己写的 EA 里若也用这套封装,直接调 Header() 就能拿到统一格式的追踪串;改 MSG_LIB_TEXT_PEND_REQUEST_STATUS_* 对应的多语言文本,不会影响 ID 与持仓号的数值输出。

MQL5 / C++
class="type">class="kw">string CPendReqOpen::Header(class="type">void)
  {
  class="type">class="kw">string type=PositionTypeDescription((class="type">ENUM_POSITION_TYPE)this.GetProperty(PEND_REQ_PROP_MQL_REQ_TYPE));
  class="kw">return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_STATUS_OPEN)+" "+type+", ID #"+(class="type">class="kw">string)this.GetProperty(PEND_REQ_PROP_ID);
  }
class="type">class="kw">string CPendReqClose::Header(class="type">void)
  {
  class="type">class="kw">string type=PositionTypeDescription((class="type">ENUM_POSITION_TYPE)this.GetProperty(PEND_REQ_PROP_MQL_REQ_TYPE));
  class="kw">return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_STATUS_CLOSE)+" "+type+" #"+(class="type">class="kw">string)this.GetProperty(PEND_REQ_PROP_MQL_REQ_POSITION)+", ID #"+(class="type">class="kw">string)this.GetProperty(PEND_REQ_PROP_ID);
  }
class="type">class="kw">string CPendReqSLTP::Header(class="type">void)
  {
  class="type">class="kw">string type=PositionTypeDescription((class="type">ENUM_POSITION_TYPE)this.GetProperty(PEND_REQ_PROP_MQL_REQ_TYPE));
  class="kw">return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_STATUS_SLTP)+" "+type+" #"+(class="type">class="kw">string)this.GetProperty(PEND_REQ_PROP_MQL_REQ_POSITION)+", ID #"+(class="type">class="kw">string)this.GetProperty(PEND_REQ_PROP_ID);
  }

◍ 挂单请求类的头部拼接与工厂方法

在 MQL5 的挂单请求封装里,CPendReqPlace、CPendReqRemove、CPendReqModify 三个派生类都重写了 Header() 方法,用来生成人类可读的短状态串。它们统一先调用 OrderTypeDescription() 把枚举转成文字,再拼上请求 ID 或订单号,方便在日志里一眼区分“下单/删单/改单”三类动作。 具体看代码,Place 类只带自身 ID,Remove 和 Modify 类额外带 PEND_REQ_PROP_MQL_REQ_ORDER 字段(即被操作的原订单号),这样在 EA 跑多个挂单时不会搞混是哪一笔。 工厂函数层面,CreatePReqClose 支持传 volume=WRONG_VALUE 做整仓或部平,CreatePReqCloseBy 用两张 ticket 做反手平仓,CreatePreqDelete 只吃一个 ticket 就生成删单请求。改仓止损止盈则用模板化的 CreatePReqModifyPosition,SL/TP 类型由调用方推导,不写死 double。 实盘接这类封装时,建议把 Header() 输出接到 Print() 或文件日志,外汇与贵金属杠杆高、滑点突变频繁,能靠 ID 回溯每笔挂单状态才可能快速定位异常。

MQL5 / C++
class="type">class="kw">string CPendReqPlace::Header(class="type">void)
  {
   class="type">class="kw">string type=OrderTypeDescription((ENUM_ORDER_TYPE)this.GetProperty(PEND_REQ_PROP_MQL_REQ_TYPE),true,class="kw">false,class="kw">false);
   class="kw">return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_STATUS_PLACE)+type+", ID #"+(class="type">class="kw">string)this.GetProperty(PEND_REQ_PROP_ID);
  }

class="type">class="kw">string CPendReqRemove::Header(class="type">void)
  {
   class="type">class="kw">string type=OrderTypeDescription((ENUM_ORDER_TYPE)this.GetProperty(PEND_REQ_PROP_MQL_REQ_TYPE),true,class="kw">false,class="kw">false);
   class="kw">return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_STATUS_REMOVE)+type+" #"+(class="type">class="kw">string)this.GetProperty(PEND_REQ_PROP_MQL_REQ_ORDER)+", ID #"+(class="type">class="kw">string)this.GetProperty(PEND_REQ_PROP_ID);
  }

class="type">class="kw">string CPendReqModify::Header(class="type">void)
  {
   class="type">class="kw">string type=OrderTypeDescription((ENUM_ORDER_TYPE)this.GetProperty(PEND_REQ_PROP_MQL_REQ_TYPE),true,class="kw">false,class="kw">false);
   class="kw">return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_STATUS_MODIFY)+type+", #"+(class="type">class="kw">string)this.GetProperty(PEND_REQ_PROP_MQL_REQ_ORDER)+" ID #"+(class="type">class="kw">string)this.GetProperty(PEND_REQ_PROP_ID);
  }

  class="type">int               CreatePReqClose(class="kw">const class="type">class="kw">ulong ticket,class="kw">const class="type">class="kw">double volume=WRONG_VALUE,class="kw">const class="type">class="kw">string comment=NULL,class="kw">const class="type">class="kw">ulong deviation=ULONG_MAX);
  class="type">int               CreatePReqCloseBy(class="kw">const class="type">class="kw">ulong ticket,class="kw">const class="type">class="kw">ulong ticket_by);
  class="type">int               CreatePreqDelete(class="kw">const class="type">class="kw">ulong ticket);
  class="kw">template<class="kw">typename SL,class="kw">typename TP> 
  class="type">int               CreatePReqModifyPosition(class="kw">const class="type">class="kw">ulong ticket,class="kw">const SL sl=WRONG_VALUE,class="kw">const TP tp=WRONG_VALUE);

挂单改单请求的接口骨架

在 MT5 的 EA 封装里,把挂单的修改动作抽象成一个待激活请求对象,比直接调 OrderSend 更利于回测与条件触发。下面这段声明给出了创建修改请求的函数签名,所有未指定的字段默认用 WRONG_VALUE 占位,意味着「不改这一项」。 CreatePReqModifyOrder 接收 ticket 与一组可选参数:price、sl、tp、limit、expiration 以及两种枚举(挂单有效期类型、成交填充类型)。调用时只传你想动的字段,其余留 WRONG_VALUE,引擎便只更新对应属性。 SetNewActivationProperties 负责给这条请求挂激活条件:来源、属性 ID、控制值、比较符、实际值。比如当某指标实际值跨过控制值(comparer_type 决定大于还是小于等于),修改请求才被推入执行队列。外汇与贵金属杠杆高,这类自动改单逻辑在滑点扩大时可能失效,实盘前务必在策略测试器用历史 tick 跑通。 把这两段直接贴进你的 include 头文件,先拿一个 EURUSD 的挂单手动改 TP 验证通道是否打通,比读文档更快。

MQL5 / C++
class="type">int CreatePReqModifyOrder(class="kw">const class="type">class="kw">ulong ticket,
                             class="kw">const PS price=WRONG_VALUE,
                             class="kw">const SL sl=WRONG_VALUE,
                             class="kw">const TP tp=WRONG_VALUE,
                             class="kw">const PL limit=WRONG_VALUE,
                             class="type">class="kw">datetime expiration=WRONG_VALUE,
                             class="kw">const ENUM_ORDER_TYPE_TIME type_time=WRONG_VALUE,
                             class="kw">const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE);

class=class="str">"cmt">//--- Set pending request activation criteria
class="type">bool SetNewActivationProperties(class="kw">const class="type">uchar id,
                                class="kw">const ENUM_PEND_REQ_ACTIVATION_SOURCE source,
                                class="kw">const class="type">int class="kw">property,
                                class="kw">const class="type">class="kw">double control_value,
                                class="kw">const ENUM_COMPARER_TYPE comparer_type,
                                class="kw">const class="type">class="kw">double actual_value);

常见问题

把修改条件封装成延后请求,在行情每 tick 检查条件,命中后才调用改单接口,避免提前占用挂单。
在挂单请求类的名称拼接处剥掉多余前缀,只保留品种与方向标识,工厂方法统一输出干净名称。
可以,小布能按你设的触发价和条件持续监控,命中后直接发起改单或删单请求,你不用一直盯盘。
延后删除等特定条件成立才执行,适合避开波动尖峰;直接平仓立即成交,用于止损或主动离场。
拼错会让日志和订单注释对不上,排查延后请求时容易误判是哪类挂单,建议用工厂方法统一拼接。