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

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

(2/3)· 当 EA 需要按用户逻辑动态撤单改价,硬编码交易指令只会拖慢策略迭代

实战向进阶 第 2/3 篇
很多交易者把撤单改价逻辑写死在 EA 主循环里,行情一变就得重新编译。用延后请求把操作抽象成对象,条件触发时再执行,策略调整只需改参数。

◍ 删单请求前的几道闸门

在 MT5 自建交易库里,删除挂单不是直接调 OrderDelete 就完事。CTradingControl::CreatePreqDelete 这个函数先卡全局禁交易标志:一旦 IsTradingDisable() 为真,立刻返回 WRONG_VALUE(值通常为 -1),连请求结构都不填。 接着它要依次拿到三样东西——订单对象、品种对象、交易对象。任何一步 Get 失败,错误标志会被置为 TRADE_REQUEST_ERR_FLAG_INTERNAL_ERR 并退出。实测中若 ticket 已不在订单池,GetOrderObjByTicket 返回 NULL 的概率极高,这时别指望后续删除能跑通。 行情刷新是另一道硬卡。symbol_obj.RefreshRates() 若失败,代码手动把结果码写成 10021(无报价处理请求),并写进错误列表。这个 10021 在 MT5 标准错误枚举里对应报价缺失,亚盘流动性稀薄的贵金属品种更容易触发。 全过完才会分配一个空闲 ID(GetFreeID 必须 ≥1),然后把请求结构填上 TRADE_ACTION_REMOVE、品种名和 ticket。也就是说,真正进交易队列前,至少过了 5 道校验。

MQL5 / C++
class="type">int CTradingControl::CreatePreqDelete(class="kw">const class="type">class="kw">ulong ticket)
  {
class=class="str">"cmt">//--- If the global trading ban flag is set, exit and class="kw">return WRONG_VALUE
   if(this.IsTradingDisable())
     {
       if(this.m_log_level>LOG_LEVEL_NO_MSG)
         ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_TRADING_DISABLE));
       class="kw">return WRONG_VALUE;
     }
class=class="str">"cmt">//--- Set the error flag as "no errors"
   this.m_error_reason_flags=TRADE_REQUEST_ERR_FLAG_NO_ERROR;
   ENUM_ACTION_TYPE action=ACTION_TYPE_CLOSE;
class=class="str">"cmt">//--- Get an order object by ticket
   COrder *order=this.GetOrderObjByTicket(ticket);
   if(order==NULL)
     {
       this.m_error_reason_flags=TRADE_REQUEST_ERR_FLAG_INTERNAL_ERR;
       if(this.m_log_level>LOG_LEVEL_NO_MSG)
         ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_ERROR_FAILED_GET_ORD_OBJ));
       class="kw">return WRONG_VALUE;
     }
   ENUM_ORDER_TYPE order_type=(ENUM_ORDER_TYPE)order.TypeOrder();
class=class="str">"cmt">//--- Get a symbol object by an order ticket
   CSymbol *symbol_obj=this.GetSymbolObjByOrder(ticket,DFUN);
   if(symbol_obj==NULL)
     {
       this.m_error_reason_flags=TRADE_REQUEST_ERR_FLAG_INTERNAL_ERR;
       if(this.m_log_level>LOG_LEVEL_NO_MSG)
         ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_ERROR_FAILED_GET_SYM_OBJ));
       class="kw">return WRONG_VALUE;
     }
class=class="str">"cmt">//--- get a trading object from a symbol object
   CTradeObj *trade_obj=symbol_obj.GetTradeObj();
   if(trade_obj==NULL)
     {
       this.m_error_reason_flags=TRADE_REQUEST_ERR_FLAG_INTERNAL_ERR;
       if(this.m_log_level>LOG_LEVEL_NO_MSG)
         ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_ERROR_FAILED_GET_TRADE_OBJ));
       class="kw">return WRONG_VALUE;
     }
class=class="str">"cmt">//--- Update symbol quotes
   if(!symbol_obj.RefreshRates())
     {
       trade_obj.SetResultRetcode(class="num">10021);
       trade_obj.SetResultComment(CMessage::Text(trade_obj.GetResultRetcode()));
       this.AddErrorCodeToList(class="num">10021);  class=class="str">"cmt">// No quotes to handle the request
       if(this.m_log_level>LOG_LEVEL_NO_MSG)
         ::Print(DFUN,CMessage::Text(class="num">10021));
       class="kw">return WRONG_VALUE;
     }

class=class="str">"cmt">//--- Look for the least of the possible IDs. If failed to find, class="kw">return WRONG_VALUE
   class="type">int id=this.GetFreeID();
   if(id<class="num">1)
     {
       class=class="str">"cmt">//--- No free IDs to create a pending request
       if(this.m_log_level>LOG_LEVEL_NO_MSG)
         ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_NO_FREE_IDS));
       class="kw">return WRONG_VALUE;
     }
   class=class="str">"cmt">//--- Set the trading operation type, as well as deleted order&class="macro">#x27;s symbol and ticket in the request structure
   this.m_request.action=TRADE_ACTION_REMOVE;
   this.m_request.symbol=symbol_obj.Name();
   this.m_request.order=ticket;

挂单改仓时 SL/TP 的重定价逻辑

在 CTradingControl 里,CreatePReqModifyPosition 负责给持仓挂一笔“修改止损止盈”的待处理请求。它先查全局交易禁令标志,若 IsTradingDisable() 为真就直接返回 WRONG_VALUE,不会往下走任何下单逻辑。 函数通过 ticket 拿到 COrder 与 CSymbol 对象,任一步拿到 NULL 都会把 m_error_reason_flags 置为 INTERNAL_ERR 并退出。这一步决定了你的 EA 在品种对象未初始化时,改单请求会静默失败,只在日志级别高于 NO_MSG 时打印一句系统错误。 价格重设是核心:SetPrices 的调用里,sl 和 tp 参数用了三元表达式——传入 WRONG_VALUE 就沿用 order.StopLoss() / order.TakeProfit(),否则用你给的新值。也就是说,只传一个参数就能单独改 SL 或 TP,另一个保持原样。 回测中若日志出现 MSG_LIB_SYS_ERROR_FAILED_GET_TRADE_OBJ,基本可锁定 symbol_obj.GetTradeObj() 返回了空,而不是你的止损价算错。外汇与贵金属杠杆高,挂单改 SL/TP 失败可能让浮亏超出预期,建议先在策略测试器里用日志级别 LOG_LEVEL_ALL 跑一遍确认对象链完整。

MQL5 / C++
class="kw">template<class="kw">typename SL,class="kw">typename TP>
class="type">int CTradingControl::CreatePReqModifyPosition(class="kw">const class="type">class="kw">ulong ticket,class="kw">const SL sl=WRONG_VALUE,class="kw">const TP tp=WRONG_VALUE)
  {
  if(this.IsTradingDisable())
     {
     if(this.m_log_level>LOG_LEVEL_NO_MSG)
        ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_TRADING_DISABLE));
     class="kw">return WRONG_VALUE;
     }
  this.m_error_reason_flags=TRADE_REQUEST_ERR_FLAG_NO_ERROR;
  ENUM_ACTION_TYPE action=ACTION_TYPE_MODIFY;
  COrder *order=this.GetOrderObjByTicket(ticket);
  if(order==NULL)
     {
     this.m_error_reason_flags=TRADE_REQUEST_ERR_FLAG_INTERNAL_ERR;
     if(this.m_log_level>LOG_LEVEL_NO_MSG)
        ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_ERROR_FAILED_GET_ORD_OBJ));
     class="kw">return WRONG_VALUE;
     }
  ENUM_ORDER_TYPE order_type=(ENUM_ORDER_TYPE)order.TypeOrder();
  CSymbol *symbol_obj=this.GetSymbolObjByPosition(ticket,DFUN);
  if(symbol_obj==NULL)
     {
     this.m_error_reason_flags=TRADE_REQUEST_ERR_FLAG_INTERNAL_ERR;
     if(this.m_log_level>LOG_LEVEL_NO_MSG)
        ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_ERROR_FAILED_GET_SYM_OBJ));
     class="kw">return WRONG_VALUE;
     }
  CTradeObj *trade_obj=symbol_obj.GetTradeObj();
  if(trade_obj==NULL)
     {
     this.m_error_reason_flags=TRADE_REQUEST_ERR_FLAG_INTERNAL_ERR;
     if(this.m_log_level>LOG_LEVEL_NO_MSG)
        ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_ERROR_FAILED_GET_TRADE_OBJ));
     class="kw">return WRONG_VALUE;
     }
  if(!this.SetPrices(order_type,class="num">0,(sl==WRONG_VALUE ? order.StopLoss() : sl),(tp==WRONG_VALUE ? order.TakeProfit() : tp),class="num">0,DFUN,symbol_obj))
     {
     this.m_error_reason_flags=TRADE_REQUEST_ERR_FLAG_INTERNAL_ERR;

「挂单改单请求的构造与拦截」

在交易控制类里,修改挂单前先查全局禁交易标志。若 IsTradingDisable() 返回真,直接返回 WRONG_VALUE,这条路径能避免禁交易时段误发请求。 找不到可用请求 ID 时(GetFreeID() 返回小于 1),同样走 WRONG_VALUE 退出,不往下写请求结构。可验证点:ID 下限是 1,不是 0,说明 ID 池从 1 开始分配。 拿到 ID 后把动作类型、品种名、持仓票号、订单类型、手数写进 m_request,再调 CreatePendingRequest 挂起。成功返回 id,失败返回 WRONG_VALUE。下面这段是修改挂单的入口函数骨架,参数全带 WRONG_VALUE 默认值,调用方可只传 ticket。

MQL5 / C++
class="kw">template<class="kw">typename PS,class="kw">typename PL,class="kw">typename SL,class="kw">typename TP>
class="type">int CTradingControl::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">//--- If the global trading ban flag is set, exit and class="kw">return WRONG_VALUE
  if(this.IsTradingDisable())
   {

◍ 改单前先把订单和品种对象抓稳

在 MT5 的 EA 封装里,改单函数一进来先按 ticket 取 COrder 对象,取不到就直接打内部错误标记并返回 false,这一步卡死了后续所有逻辑。 若订单对象拿到了,接着用 GetSymbolObjByOrder 取品种对象、再取 CTradeObj,任何一环为 NULL 都会把 m_error_reason_flags 置成 TRADE_REQUEST_ERR_FLAG_INTERNAL_ERR 并退出,说明改单依赖的链路必须全通。 价格赋值用 SetPrices 统一处理:传入的 price/sl/tp/limit 若大于 0 用新值,sl 或 tp 小于 0 则沿用原订单的 StopLoss() / TakeProfit(),等于 0 就清零;这套三元判断能让调用方只改想改的字段。 SetPrices 失败时会写死结果码 10021(无报价处理请求),并在日志打印对应文本后返回 false,实盘里遇到这码多半是品种停盘或报价断流,外汇贵金属高风险下需先确认行情连接。 最后 GetFreeID 找可用请求 ID,返回值小于 1 直接 return WRONG_VALUE,意味着同帧并发改单数已达上限,开 MT5 把日志级别调高于 LOG_LEVEL_NO_MSG 就能看到具体中断点。

MQL5 / C++
if(this.m_log_level>LOG_LEVEL_NO_MSG)
     ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_TRADING_DISABLE));
   class="kw">return WRONG_VALUE;
   }
class=class="str">"cmt">//--- Set the error flag as "no errors"
   this.m_error_reason_flags=TRADE_REQUEST_ERR_FLAG_NO_ERROR;
   ENUM_ACTION_TYPE action=ACTION_TYPE_MODIFY;
class=class="str">"cmt">//--- Get an order object by ticket
   COrder *order=this.GetOrderObjByTicket(ticket);
   if(order==NULL)
   {
      this.m_error_reason_flags=TRADE_REQUEST_ERR_FLAG_INTERNAL_ERR;
      if(this.m_log_level>LOG_LEVEL_NO_MSG)
         ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_ERROR_FAILED_GET_ORD_OBJ));
      class="kw">return class="kw">false;
   }
   ENUM_ORDER_TYPE order_type=(ENUM_ORDER_TYPE)order.TypeOrder();
class=class="str">"cmt">//--- Get a symbol object by an order ticket
   CSymbol *symbol_obj=this.GetSymbolObjByOrder(ticket,DFUN);
   if(symbol_obj==NULL)
   {
      this.m_error_reason_flags=TRADE_REQUEST_ERR_FLAG_INTERNAL_ERR;
      if(this.m_log_level>LOG_LEVEL_NO_MSG)
         ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_ERROR_FAILED_GET_SYM_OBJ));
      class="kw">return class="kw">false;
   }
class=class="str">"cmt">//--- get a trading object from a symbol object
   CTradeObj *trade_obj=symbol_obj.GetTradeObj();
   if(trade_obj==NULL)
   {
      this.m_error_reason_flags=TRADE_REQUEST_ERR_FLAG_INTERNAL_ERR;
      if(this.m_log_level>LOG_LEVEL_NO_MSG)
         ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_ERROR_FAILED_GET_TRADE_OBJ));
      class="kw">return class="kw">false;
   }
class=class="str">"cmt">//--- Set the prices
class=class="str">"cmt">//--- If failed to set - write the "internal error" flag, set the error code in the class="kw">return structure,
class=class="str">"cmt">//--- display the message in the journal and class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
   if(!this.SetPrices(order_type,
                     (price>class="num">0 ? price : order.PriceOpen()),
                     (sl>class="num">0 ? sl : sl<class="num">0 ? order.StopLoss() : class="num">0),
                     (tp>class="num">0 ? tp : tp<class="num">0 ? order.TakeProfit() : class="num">0),
                     (limit>class="num">0 ? limit : order.PriceStopLimit()),
                     DFUN,symbol_obj))
   {
      this.m_error_reason_flags=TRADE_REQUEST_ERR_FLAG_INTERNAL_ERR;
      trade_obj.SetResultRetcode(class="num">10021);
      trade_obj.SetResultComment(CMessage::Text(trade_obj.GetResultRetcode()));
      if(this.m_log_level>LOG_LEVEL_NO_MSG)
         ::Print(DFUN,CMessage::Text(class="num">10021));   class=class="str">"cmt">// No quotes to process the request
      class="kw">return class="kw">false;
   }
class=class="str">"cmt">//--- Look for the least of the possible IDs. If failed to find, class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
   class="type">int id=this.GetFreeID();
   if(id<class="num">1)
      class="kw">return WRONG_VALUE;

改单请求的结构填充与 pending 接口

挂单修改时,请求结构要先写死几个字段:magic、volume、filling 类型、过期时间与过期类型。若外部传入的 type_filling / expiration / type_time 大于 WRONG_VALUE 就用传入值,否则回退到原订单的对应属性,避免把已有订单的成交策略洗掉。 随后把 action 置为 TRADE_ACTION_MODIFY,并填入 symbol、order ticket 与目标 order_type。CreatePendingRequest 的第三个参数写死为 1,表示这是修改类请求;超时窗用 END_TIME - TimeCurrent() 计算,返回成功拿 id,失败回 WRONG_VALUE。 类里还暴露了四类 pending 构造入口:ClosePositionPending 全平、ClosePositionPartiallyPending partial 平、ClosePositionByPending 反单平、DeleteOrderPending 删挂单。修改侧则是模板函数 ModifyPositionPending(带 SL/TP 泛型)与 ModifyOrderPending(带 PR/SL/TP/PL 泛型),PR 即新挂单价。 在 MT5 里接这套封装时,优先确认 WRONG_VALUE 在你的工程里是 -1 还是其他哨兵值;若与 broker 返回的枚举冲突,修改单会静默失败。外汇与贵金属杠杆高,pending 请求从构造到发送之间价格可能已跳空,挂单未必按预期触发。

MQL5 / C++
this.m_request.magic=order.GetMagicID((class="type">uint)order.Magic());
this.m_request.volume=order.Volume();
this.m_request.type_filling=(type_filling>WRONG_VALUE ? type_filling : order.TypeFilling());
this.m_request.expiration=(expiration>WRONG_VALUE ? expiration : order.TimeExpiration());
this.m_request.type_time=(type_time>WRONG_VALUE ? type_time : order.TypeTime());
this.m_request.action=TRADE_ACTION_MODIFY;
this.m_request.symbol=symbol_obj.Name();
this.m_request.order=ticket;
this.m_request.type=order_type;
if(this.CreatePendingRequest(PEND_REQ_STATUS_MODIFY,(class="type">uchar)id,class="num">1,class="type">class="kw">ulong(END_TIME-(class="type">class="kw">ulong)::TimeCurrent()),this.m_request,class="num">0,symbol_obj,order))
   class="kw">return id;
class="kw">return WRONG_VALUE;

「把删单改单封装成待执行请求」

在交易引擎里,把撤掉挂单和改仓位、改挂单的参数先收进“待执行请求”对象,能避免每次下单逻辑里反复拼装结构体。下面这段 CEngine 的成员函数就是干这个的:DeleteOrderPending 只接 ticket,内部转调 CreatePreqDelete,返回整型状态码。 ModifyPositionPending 用模板接收 SL/TP 类型,默认填 WRONG_VALUE,意味着不传就不动止损止盈;它最终交给 CreatePReqModifyPosition(ticket,sl,tp) 去生成请求。外汇和贵金属杠杆高,改仓位前务必确认 sl/tp 间距满足经纪商止损水平,否则请求会直接被拒。 ModifyOrderPending 更重一点,除了 ticket 还要接价格、sl、tp、stoplimit 四个模板参数,全带 WRONG_VALUE 缺省。这样你在 EA 里想改挂单触发价但不动止损,只传 price 即可,其余保持默认。 让小布替你跑这套 把这三类函数原样丢进 MT5 的包含文件,随便写个脚本传真实 ticket 进去,看返回码是不是 TRADE_RETCODE_OK 以外的数,能快速定位是参数越界还是经纪商限制。

MQL5 / C++
class="type">int CEngine::DeleteOrderPending(class="kw">const class="type">class="kw">ulong ticket)
  {
   class="kw">return this.m_trading.CreatePreqDelete(ticket);
  }

class="kw">template<class="kw">typename SL,class="kw">typename TP>
class="type">int CEngine::ModifyPositionPending(class="kw">const class="type">class="kw">ulong ticket,class="kw">const SL sl=WRONG_VALUE,class="kw">const TP tp=WRONG_VALUE,class="kw">const class="type">class="kw">string comment=NULL)
  {
   class="kw">return this.m_trading.CreatePReqModifyPosition(ticket,sl,tp);
  }

class="kw">template<class="kw">typename PR,class="kw">typename SL,class="kw">typename TP,class="kw">typename PL>
class="type">int CEngine::ModifyOrderPending(class="kw">const class="type">class="kw">ulong ticket,
                 class="kw">const PR price=WRONG_VALUE,
                 class="kw">const SL sl=WRONG_VALUE,)
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到订单状态与延后请求触发的实时反馈,你专注决策即可。

常见问题

延后请求在条件满足时才进入执行队列,正常轮询下不会漏;若终端断线需重连后补发,概率上存在短暂空窗。
文中分属不同后代类,持仓改价与挂单属性修改是独立方法,需分别声明请求对象,不能一次调用混用。
小布盯盘已内置常见订单诊断,若你自用该函数库编译 EA,把品种页打开即可对照请求 ID 看执行状态,不用自己写日志。
基准显示函数始终带初级文本,禁用标志后仅留订单名,便于延后请求摘要里紧凑拼接类型与 ID。
需等图形外壳就绪、函数库功能补全后才返回该任务,当前篇先补齐删改等基础延后交易功能。