轻松快捷开发 MetaTrader 程序的函数库(第 三十三部分):延后交易请求 - 在特定条件下平仓·进阶篇
(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 的存活时长。
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 策略测试器开逐笔成交日志对照。
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,便于按时间逆序匹配修改事件。外汇与贵金属市场事件密集,这种逆序匹配可降低漏判概率,但高频行情下仍可能因事件丢失导致请求对象残留,实盘须自行加超时兜底。
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() 的赋值来源。外汇与贵金属杠杆高,这类自动回填若出错可能让止损失效,跑前务必用模拟盘核对。
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 请求管理器里跑一晚模拟盘验证。
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) 的触发时机,能少踩不少重复发单的坑。
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;