轻松快捷开发 MetaTrader 程序的函数库(第 三十三部分):延后交易请求 - 在特定条件下平仓·进阶篇
⚙️

轻松快捷开发 MetaTrader 程序的函数库(第 三十三部分):延后交易请求 - 在特定条件下平仓·进阶篇

(2/3)· 从全平到逆向仓位平仓,三种平仓逻辑如何塞进延后请求库而不踩坑

实战向 第 2/3 篇
很多人在写延后平仓时,忘了把交易请求结构字段清零,结果本该简单平仓的操作变成了逆向仓位平仓。还有部分平仓遇到剩余手数刚好等于上次已平手数时,旧的事件比对逻辑会误判请求已完成。这些细节不处理,策略跑实盘时可能悄悄漏单。

「挂单请求的相关性判定逻辑」

在 MT5 的自定义交易控制类里,CheckPReqRelevance 方法负责判断一条挂起的交易请求是否还值得继续发送。它的入参包含请求对象指针、MqlTradeRequest 引用以及列表索引,返回布尔值决定外部循环是否跳过该请求。 当动作是开仓(TRADE_ACTION_DEAL 且持仓票号为 0)或挂单(TRADE_ACTION_PENDING)时,先通过 magic 算出挂单请求 ID,再用 GetList(ORDER_PROP_PEND_REQ_ID, id, EQUAL) 拉出含此 ID 的订单/持仓列表。若列表指针无效直接返回 false;若列表数量大于 0,说明请求已被成交处理,打印日志后从请求列表删除该项并返回 false,外部循环自然推进到下一条。 非开仓类动作(平仓、删单、改单、改止盈止损、反向平仓 CLOSE_BY)走 else 分支。以平仓为例,用 ORDER_PROP_TICKET 和持仓票号取持仓列表,指针无效同样返回 false。这套判定能避免对已完成动作重复发单,外汇与贵金属市场高杠杆、滑点频繁,重复发单可能放大穿仓风险,建议在策略回测里用 Print 日志核对每笔 ID 的存活时长。

MQL5 / C++
class="type">bool CTradingControl::CheckPReqRelevance(CPendRequest *req_obj,class="kw">const class="type">MqlTradeRequest &request,class="kw">const class="type">int index)
  {
class=class="str">"cmt">//--- If this is a position opening or placing a pending order
   if((req_obj.Action()==TRADE_ACTION_DEAL && req_obj.Position()==class="num">0) || req_obj.Action()==TRADE_ACTION_PENDING)
     {
      class=class="str">"cmt">//--- Get the pending request ID
       class="type">uchar id=this.GetPendReqID((class="type">uint)request.magic);
      class=class="str">"cmt">//--- Get the list of orders/positions containing the order/position with the pending request ID
       CArrayObj *list=this.m_market.GetList(ORDER_PROP_PEND_REQ_ID,id,EQUAL);
       if(::CheckPointer(list)==POINTER_INVALID)
         class="kw">return class="kw">false;
      class=class="str">"cmt">//--- If the order/position is present, the request is handled: remove it and proceed to the next(leave the method for the external loop)
       if(list.Total()>class="num">0)
         {
          if(this.m_log_level>LOG_LEVEL_NO_MSG)
             ::Print(req_obj.Header(),": ",CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_EXECUTED));
          this.m_list_request.Delete(index);
          class="kw">return class="kw">false;
         }
     }
class=class="str">"cmt">//--- Otherwise: full and partial position closure, removing an order, modifying order parameters and position stop orders
   else
     {
      CArrayObj *list=NULL;
      class=class="str">"cmt">//--- if this is a position closure, including a closure by an opposite one
       if((req_obj.Action()==TRADE_ACTION_DEAL && req_obj.Position()>class="num">0) || req_obj.Action()==TRADE_ACTION_CLOSE_BY)
         {
          class=class="str">"cmt">//--- Get a position with the necessary ticket from the list of open positions
           list=this.m_market.GetList(ORDER_PROP_TICKET,req_obj.Position(),EQUAL);
           if(::CheckPointer(list)==POINTER_INVALID)
             class="kw">return class="kw">false;

挂单请求里的部分平仓判定

在挂单管理逻辑里,当市场已无对应持仓时,这段处理会直接删掉请求并返回 false,把控制权交还外部循环。 如果持仓还在,就说明当前触发的是部分平仓场景,需要到账户交易事件里翻记录确认。 事件列表从尾部往前扫(j 从 events_total-1 递减到 WRONG_VALUE),只认 TRADE_EVENT_POSITION_CLOSED_PARTIAL 和 TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS 两类事件。 一旦事件里的持仓票号与挂单请求里的 Position() 对上,就调 m_market.GetList(ORDER_PROP_TICKET, req_obj.Position(), EQUAL) 去市场持仓里抓对应单子;返回空或总数为 0 就说明查无此仓,后续分支不会再走。 外汇和贵金属部分平仓受点差与滑点影响,事件回放和实际持仓可能差几毫秒,验证时建议在 MT5 策略测试器开逐笔成交日志对照。

MQL5 / C++
if(list.Total()==class="num">0)
  {
   if(this.m_log_level>LOG_LEVEL_NO_MSG)
      ::Print(req_obj.Header(),": ",CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_EXECUTED));
   this.m_list_request.Delete(index);
   class="kw">return class="kw">false;
  }
else
  {
   if(this.m_events.IsEvent())
     {
      list=this.m_events.GetList();
      if(list==NULL)
         class="kw">return class="kw">false;
      class="type">int events_total=list.Total();
      for(class="type">int j=events_total-class="num">1; j>WRONG_VALUE; j--)
        {
         CEvent *event=list.At(j);
         if(event==NULL)
            class="kw">continue;
         if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL || event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS)
           {
            if(event.TicketFirstOrderPosition()==req_obj.Position())
              {
               CArrayObj *list_orders=this.m_market.GetList(ORDER_PROP_TICKET,req_obj.Position(),EQUAL);
               if(list_orders==NULL || list_orders.Total()==class="num">0)

◍ 挂单成交后的请求清理逻辑

这段代码片段处理的是挂单类请求在账户交易事件回放中的收尾:当某笔挂单的「已成交体量 + 未成交剩余体量」恰好等于最初请求体量时,该 pending request 对象会被从列表删除并跳出循环。 代码先取订单列表末尾的 COrder 指针,空指针直接 break 防止越界;随后把实际持仓属性写回请求对象,再用 req_obj.GetProperty(PEND_REQ_PROP_MQL_REQ_VOLUME)event.VolumeOrderExecuted()+event.VolumeOrderCurrent() 做等值判断。 若日志级别高于 NO_MSG,会打印「挂单请求已执行」的提示,然后 m_list_request.Delete(index) 并 break。循环外再用 CheckPointer(req_obj)==POINTER_INVALID 判断对象是否已被删,若失效则 return false 交还外层循环。 对于 SLTP 修改动作,代码另起一支:取全部账户交易事件列表,从尾部 events_total-1 向前遍历到 WRONG_VALUE,便于按时间逆序匹配修改事件。外汇与贵金属市场事件密集,这种逆序匹配可降低漏判概率,但高频行情下仍可能因事件丢失导致请求对象残留,实盘须自行加超时兜底。

MQL5 / C++
              class="kw">break;
              COrder *order=list_orders.At(list_orders.Total()-class="num">1);
              if(order==NULL)
                 class="kw">break;
              class=class="str">"cmt">//--- Set actual position data to the pending request object
              this.SetOrderActualProperties(req_obj,order);
              class=class="str">"cmt">//--- If(executed request volume + unexecuted request volume) is equal to the requested volume in a pending request -
              class=class="str">"cmt">//--- the request is handled: remove it and class="kw">break the loop by the list of account trading events
              if(req_obj.GetProperty(PEND_REQ_PROP_MQL_REQ_VOLUME)==event.VolumeOrderExecuted()+event.VolumeOrderCurrent())
                {
                 if(this.m_log_level>LOG_LEVEL_NO_MSG)
                    ::Print(req_obj.Header(),": ",CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_EXECUTED));
                 this.m_list_request.Delete(index);
                 class="kw">break;
                }
             }
           }
         }
         class=class="str">"cmt">//--- If a handled pending request object was removed by the trading event list in the loop, move on to the next one(leave the method for the external loop)
         if(::CheckPointer(req_obj)==POINTER_INVALID)
            class="kw">return class="kw">false;
        }
      }
   }
class=class="str">"cmt">//--- If this is a modification of position stop orders
if(req_obj.Action()==TRADE_ACTION_SLTP)
   {
    class=class="str">"cmt">//--- Get the list of all account trading events
    list=this.m_events.GetList();
    if(list==NULL)
       class="kw">return class="kw">false;
    class=class="str">"cmt">//--- In the loop from the end of the account trading event list
    class="type">int events_total=list.Total();
    for(class="type">int j=events_total-class="num">1; j>WRONG_VALUE; j--)
      {

「从账户事件回写挂单修改状态」

这段逻辑处理的是:当账户交易事件流里出现挂单止损/止盈被改写的事件时,如何把实际成交属性回填到待处理请求对象。核心判断在 event.TypeEvent() 大于 TRADE_EVENT_MODIFY_ORDER_TP 之后,再比对 event.TicketFirstOrderPosition() 与 req_obj.Position() 的票号是否一致。 一致才会去市场持仓列表里按 ORDER_PROP_TICKET 取对应订单:list_orders 为空或数量为 0 就直接 break,说明持仓侧已查无此单,继续跑下去没有意义。取到最后一笔 order 后调用 SetOrderActualProperties 把真实止损止盈写回请求对象。 回填之后用 req_obj.IsCompleted() 确认修改是否全部生效。若生效且日志级别高于 LOG_LEVEL_NO_MSG,会打印“挂单请求已执行”的提示,随后从 m_list_request 里按 index 删除该请求并 break 事件循环。若循环里发现 req_obj 指针已失效(POINTER_INVALID),方法直接返回 false,把控制权交回外层循环。 在 MT5 里验证时,可故意发一笔改 TP 的挂单请求,再盯日志里 MSG_LIB_TEXT_PEND_REQUEST_EXECUTED 是否出现——没出现就说明 IsCompleted 逻辑或票号比对在你的订单结构里对不上,得查 Position() 的赋值来源。外汇与贵金属杠杆高,这类自动回填若出错可能让止损失效,跑前务必用模拟盘核对。

MQL5 / C++
      class=class="str">"cmt">//--- get the next trading event
      CEvent *event=list.At(j);
      if(event==NULL)
         class="kw">continue;
      class=class="str">"cmt">//--- If this is a change of the position&class="macro">#x27;s stop orders
      if(event.TypeEvent()>TRADE_EVENT_MODIFY_ORDER_TP)
         {
         class=class="str">"cmt">//--- If a position ticket in a trading event coincides with the ticket in a pending trading request
         if(event.TicketFirstOrderPosition()==req_obj.Position())
            {
            class=class="str">"cmt">//--- Get a position object from the list of market positions
            CArrayObj *list_orders=this.m_market.GetList(ORDER_PROP_TICKET,req_obj.Position(),EQUAL);
            if(list_orders==NULL || list_orders.Total()==class="num">0)
               class="kw">break;
            COrder *order=list_orders.At(list_orders.Total()-class="num">1);
            if(order==NULL)
               class="kw">break;
            class=class="str">"cmt">//--- Set actual position data to the pending request object
            this.SetOrderActualProperties(req_obj,order);
            class=class="str">"cmt">//--- If all modifications have worked out -
            class=class="str">"cmt">//--- the request is handled: remove it and class="kw">break the loop by the list of account trading events
            if(req_obj.IsCompleted())
               {
               if(this.m_log_level>LOG_LEVEL_NO_MSG)
                  ::Print(req_obj.Header(),": ",CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_EXECUTED));
               this.m_list_request.Delete(index);
               class="kw">break;
               }
            }
         }
      }
      class=class="str">"cmt">//--- If a handled pending request object was removed by the trading event list in the loop, move on to the next one(leave the method for the external loop)
      if(::CheckPointer(req_obj)==POINTER_INVALID)
         class="kw">return class="kw">false;
      }
   class=class="str">"cmt">//--- If this is a pending order removal
   if(req_obj.Action()==TRADE_ACTION_REMOVE)
      {

从账户事件流反查挂单改单是否落地

处理挂单类请求时,不能只靠发送返回值判断成败。上面这段逻辑先按订单状态拉出已删除挂单历史,再用 ticket 精确过滤;若列表长度大于 0,说明该挂单已被撤消,对应请求可直接从队列摘除并返回 false,把后续交给外部循环。 对 TRADE_ACTION_MODIFY 类型的挂单修改,则走另一条路:拉取全部账户交易事件,从列表尾部倒序遍历。事件类型落在 TRADE_EVENT_TRIGGERED_STOP_LIMIT_ORDER 与 TRADE_EVENT_MODIFY_POSITION_SL_TP 之间,且事件内的订单 ticket 与请求对象 ticket 一致时,才认为是本次改单的回执。 此时再从市场订单列表按 ticket 取对象,若取不到或总数为 0 就 break,避免空指针继续跑。外汇与贵金属挂单在高波动时段可能部分成交或滑点改价,这类事件反查能帮你把“发出去了”和“真的改了”区分开,建议直接把这段代码塞进你的 EA 请求管理器里跑一晚模拟盘验证。

MQL5 / C++
   class=class="str">"cmt">//--- Get the list of removed pending orders from the historical list
   list=this.m_history.GetList(ORDER_PROP_STATUS,ORDER_STATUS_HISTORY_PENDING,EQUAL);
   if(::CheckPointer(list)==POINTER_INVALID)
      class="kw">return class="kw">false;
   class=class="str">"cmt">//--- Leave a single order with the necessary ticket in the list
   list=CSelect::ByOrderProperty(list,ORDER_PROP_TICKET,req_obj.Order(),EQUAL);
   class=class="str">"cmt">//--- If the order is present, the request is handled: remove it and proceed to the next(leave the method for the external loop)
   if(list.Total()>class="num">0)
     {
      if(this.m_log_level>LOG_LEVEL_NO_MSG)
         ::Print(req_obj.Header(),": ",CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_EXECUTED));
      this.m_list_request.Delete(index);
      class="kw">return class="kw">false;
     }
   }
   class=class="str">"cmt">//--- If this is a pending order modification
   if(req_obj.Action()==TRADE_ACTION_MODIFY)
     {
      class=class="str">"cmt">//--- Get the list of all account trading events
      list=this.m_events.GetList();
      if(list==NULL)
         class="kw">return class="kw">false;
      class=class="str">"cmt">//--- In the loop from the end of the account trading event list
      class="type">int events_total=list.Total();
      for(class="type">int j=events_total-class="num">1; j>WRONG_VALUE; j--)
        {
         class=class="str">"cmt">//--- get the next trading event
         CEvent *event=list.At(j);
         if(event==NULL)
            class="kw">continue;
         class=class="str">"cmt">//--- If this event involves any change of modified pending order parameters
         if(event.TypeEvent()>TRADE_EVENT_TRIGGERED_STOP_LIMIT_ORDER && event.TypeEvent()<TRADE_EVENT_MODIFY_POSITION_SL_TP)
           {
            class=class="str">"cmt">//--- If an order ticket in a trading event coincides with the ticket in a pending trading request
            if(event.TicketOrderEvent()==req_obj.Order())
              {
               class=class="str">"cmt">//--- Get an order object from the list
               CArrayObj *list_orders=this.m_market.GetList(ORDER_PROP_TICKET,req_obj.Order(),EQUAL);
               if(list_orders==NULL || list_orders.Total()==class="num">0)
                  class="kw">break;
               COrder *order=list_orders.At(class="num">0);
               if(order==NULL)

◍ 挂单请求完成后的清理与平仓入口

当挂单修改逻辑跑完一轮后,代码会检查 req_obj.IsCompleted() 的返回状态。若返回真,说明该笔挂单请求已在账户事件循环中被实际处理,此时从 m_list_request 里按 index 删除对象并 break,避免重复扫同一事件。 退出时 return(::CheckPointer(req_obj)==POINTER_INVALID ? false : true) 这一行很关键:若对象已删除指针失效就返 false,让外层循环知道不必再盯这个请求;否则返 true 继续留待后续处理。 紧接着的 CreatePReqClose 方法负责建一个平掉指定 ticket 仓位的挂单请求。开头先判 IsTradingDisable(),全局禁交易标志一旦置位直接返 WRONG_VALUE 并打日志,外汇与贵金属品种在高波动时段常触发此类风控锁,实盘前务必确认开关状态。 方法内通过 GetOrderObjByTicket(ticket) 拿订单对象,拿不到就把错误标志设为 TRADE_REQUEST_ERR_FLAG_INTERNAL_ERR 并返 false;随后取 order.TypeOrder() 与对应 CSymbol 对象,为下一步填平仓参数做准备。开 MT5 把这段塞进自己的交易控制类,断点跟一遍 m_list_request.Delete(index) 的触发时机,能少踩不少重复发单的坑。

MQL5 / C++
            class="kw">break;
            class=class="str">"cmt">//--- Set actual order data to the pending request object
            this.SetOrderActualProperties(req_obj,order);
            class=class="str">"cmt">//--- If all modifications have worked out -
            class=class="str">"cmt">//--- the request is handled: remove it and class="kw">break the loop by the list of account trading events
            if(req_obj.IsCompleted())
              {
               if(this.m_log_level>LOG_LEVEL_NO_MSG)
                  ::Print(req_obj.Header(),": ",CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_EXECUTED));
               this.m_list_request.Delete(index);
               class="kw">break;
              }
            }
         }
      }
   }
   }
class=class="str">"cmt">//--- Exit if the pending request object has been removed after checking its operation(leave the method for the external loop)
   class="kw">return(::CheckPointer(req_obj)==POINTER_INVALID ? class="kw">false : true);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create a pending request for closing a position                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CTradingControl::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=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 class="kw">false;
     }
   ENUM_ORDER_TYPE order_type=(ENUM_ORDER_TYPE)order.TypeOrder();
class=class="str">"cmt">//--- Get a symbol object by a position ticket
   CSymbol *symbol_obj=this.GetSymbolObjByPosition(ticket,DFUN);
   class=class="str">"cmt">//--- If failed to get the symbol object, display the message and class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
把请求状态核对交给小布盯盘
这些延后请求激活与否的诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到 pending 请求的实际触发状态,你只管调逻辑。

常见问题

当剩余平仓手数等于之前已平手数且事件比对只查关联信息时,旧逻辑会认为数据一致即激活。应在新交易事件确立发生的时刻才检查最后事件,避免混淆。
可以,小布盯盘对应品种页内置了延后请求状态追踪,能直接显示待激活、已触发或异常终止的平仓请求,省去自己写日志核对。
若 position_by 等字段残留非零值,创建平仓对象时会生成逆向仓位平仓请求而非普通平仓,导致非预期锁仓或反手。
可在请求控制类里按对象属性对列表搜索排序,便于 GUI 展示和管理,随时更改或删除特定条件的平仓请求。
在对冲组合中用反向持仓抵消原有仓位时较方便,但外汇贵金属波动大、高风险,使用前需在策略回测里验证净敞口变化。