轻松快捷开发 MetaTrader 程序的函数库(第 三十四部分):延后交易请求 - 在特定条件下删除和修改订单与持仓·进阶篇
(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 道校验。
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 跑一遍确认对象链完整。
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。
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 就能看到具体中断点。
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 请求从构造到发送之间价格可能已跳空,挂单未必按预期触发。
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 以外的数,能快速定位是参数越界还是经纪商限制。
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,)