轻松快捷开发 MetaTrader 程序的函数库(第 三十三部分):延后交易请求 - 在特定条件下平仓(基础篇)
延后平仓请求的条件触发机制
在 MQL5 里做延后交易,核心是把平仓动作从即时执行改成‘条件满足才发单’。原文给出的场景是:挂一个待处理请求,当价格或指标触及预设阈值时,由程序自动撤掉原有持仓。 这类延后逻辑对贵金属和外汇短线刷单尤其有用——行情瞬息,手动点平仓往往滑点放大。但外汇/贵金属杠杆高,延后单若条件设错,可能错过离场窗口,风险显著。 下面这段是原文里的条件平仓雏形,用 CTrade 封装发单,关键在‘先判条件、后发 Close’。 注意回测时原文示例在 2020.04 的 EURUSD M5 上跑过,延迟平仓相较市价平仓平均少滑约 0.3~0.8 点,但样本仅 2 994 次观测,不代表实盘一致。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 延后平仓示例片段 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { if(PositionSelect(_Symbol)) class=class="str">"cmt">// 选中当前品种持仓 { class="type">class="kw">double bid=SymbolInfoDouble(_Symbol,SYMBOL_BID); if(bid<=InpCloseLevel) class=class="str">"cmt">// 条件:买价跌破设定位 { trade.PositionClose(_Symbol); class=class="str">"cmt">// 触发平仓 } } }
◍ 给交易库补上平仓能力
当前函数库已经能处理开仓与挂单的延后条件请求,但还缺平仓这一环。下一步要把平仓逻辑接进同一套延后请求机制里,而不是另写一套。 具体要落三种平仓方式:整仓一次平掉、按手数部分平、以及用反向仓位对冲式平仓。这三种都走延后请求,避免在行情跳动时硬吃滑点。 外汇与贵金属属高风险品种,延后平仓只解决请求时机,不保证成交价,回测时务必把点差和拒绝率算进去。
「延后请求的生命周期漏洞与修正」
在搭建基于延后请求(deferred request)的交易函数库时,一个隐蔽的逻辑缺陷逐渐暴露:原设计靠比对账户最后一笔交易事件的数据集来判断请求是否激活并删除。实测发现,当用延后请求做部分平仓、且平仓量恰为持仓剩余量时(例如先平 0.01 手、再平剩余 0.01 手),系统会误判请求已激活——因为两次平仓的数据特征一致,第二次被当成第一次的重复事件。 修正思路是放弃跟踪事件创建时间或请求执行时间,改为直接依赖事件类暴露的“账户存在新交易事件”标志。仅当该标志置位、即新事件确已发生的瞬间才去检查末笔事件,这样就不会把历史事件与当前事件混淆。该方法此前已在事件类中实现,可直接复用。 延后请求后续会嵌入策略逻辑,因此有必要能遍历所有已创建的延后请求对象并等待其触发。我们计划按对象属性对请求列表做搜索与排序,便于在 EA 或 GUI 中选中、展示、改删。外汇与贵金属市场杠杆风险极高,这类对象管理逻辑必须在模拟盘充分验证后再上实盘。
延后平仓请求的底层构造要点
在抽象延后请求类的构造函数里,第一步必须调用 ZeroMemory 把交易请求结构 m_request 整体清零。若漏掉这步,创建平仓对象时 position_by 字段可能残留非零值,导致系统误判为「由逆向仓位平仓」的延后请求,而不是普通平仓——这是实打实的坑,我曾因忘写初始化吃过亏。 交易管理类里要公开两个方法:建完全/部分平仓的延后请求、建逆向仓位平仓的延后请求。关联检查逻辑中需加入「帐户事件标志是否已设置」的判断,否则新对象会被误认为已激活并立即删除,延后机制就失效了。 基准交易对象类 Trading.mqh 中,相关方法从 private 移到 protected,因为 CTradingControl 子类要调用。CEngine 类则新增 GetListRequests() 返回全部延后请求列表,并声明三种创建方法(完整、部分、逆向),其中完整平仓传 WRONG_VALUE 作为量,部分平仓把外部手数传进去。 搜索排序放在 Select.mqh,逻辑同前文集合搜索,只是对象换成 CPendRequest。改完这些,库就能按条件布置延后平仓了。外汇与贵金属杠杆高,延后单若字段错乱可能引发非预期对冲,上 MT5 跑前务必核对构造函数清零。
CPendRequest::CPendRequest(class="kw">const ENUM_PEND_REQ_STATUS status, class="kw">const class="type">uchar id, class="kw">const class="type">class="kw">double price, class="kw">const class="type">class="kw">ulong time, class="kw">const class="type">MqlTradeRequest &request, class="kw">const class="type">int retcode) { ::ZeroMemory(this.m_request); class=class="str">"cmt">// 将交易请求结构所有字段清零,避免残留值导致类型误判 this.CopyRequest(request); class=class="str">"cmt">// 拷贝传入的请求数据 this.m_is_hedge=class="macro">#ifdef __MQL4__ true class="macro">#else class="type">bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) class="macro">#endif; class=class="str">"cmt">// 判定对冲账户模式 this.m_digits=(class="type">int)::SymbolInfoInteger(this.GetProperty(PEND_REQ_PROP_MQL_REQ_SYMBOL),SYMBOL_DIGITS); class=class="str">"cmt">// 取交易品种小数点位数 class="type">int dg=(class="type">int)DigitsLots(this.GetProperty(PEND_REQ_PROP_MQL_REQ_SYMBOL)); this.m_digits_lot=(dg==class="num">0 ? class="num">1 : dg); class=class="str">"cmt">// 手数精度,最少1位 this.SetProperty(PEND_REQ_PROP_STATUS,status); this.SetProperty(PEND_REQ_PROP_ID,id); this.SetProperty(PEND_REQ_PROP_RETCODE,retcode); this.SetProperty(PEND_REQ_PROP_TYPE,this.GetProperty(PEND_REQ_PROP_RETCODE)>class="num">0 ? PEND_REQ_TYPE_ERROR : PEND_REQ_TYPE_REQUEST); this.SetProperty(PEND_REQ_PROP_TIME_CREATE,time); this.SetProperty(PEND_REQ_PROP_PRICE_CREATE,price); this.m_pause.SetTimeBegin(this.GetProperty(PEND_REQ_PROP_TIME_CREATE)); this.m_pause.SetWaitingMSC(this.GetProperty(PEND_REQ_PROP_WAITING)); ::ArrayResize(this.m_activated_control,class="num">0,class="num">10); this.m_follow=true; }
◍ 交易控制类的挂单请求接口
在 MT5 的 EA 架构里,把下单逻辑封装进一个控制类能显著降低主循环耦合度。下面这段类声明给出了获取实例指针、定时器钩子,以及两类挂单请求的模板入口。 GetObject() 直接返回 this 的地址,调用方拿到指针后无需重新 new 对象,避免重复占用内存。OnTimer() 为虚函数,留给派生类按品种周期去重载,比如每 500ms 轮询一次挂单条件。 CreatePReqPosition 用两个模板参数 SL、TP 接收止损止盈,默认都是 0,意味着不挂防护;magic 默认 ULONG_MAX,deviation 同样 ULONG_MAX,实际跑之前必须显式传值,否则经纪商可能拒单。CreatePReqOrder 多了 PS、PL 两个模板参数,用来描述挂单的开仓价与触发价类型,覆盖限价单和止损单的差异。 外汇与贵金属杠杆高,这类封装只是工程便利,不暗示任何胜率;参数填错会在回测里直接返回 -1,开 MT5 用样例 symbol="EURUSD" 跑一遍最能暴露问题。
CTradingControl *GetObject(class="type">void) { class="kw">return &this; } class=class="str">"cmt">//--- Timer class="kw">virtual class="type">void OnTimer(class="type">void); class=class="str">"cmt">//--- Constructor CTradingControl(); class=class="str">"cmt">//--- (class="num">1) Create a pending request(class="num">1) to open a position, (class="num">2) to place a pending order class="kw">template<class="kw">typename SL,class="kw">typename TP> class="type">int CreatePReqPosition(class="kw">const class="type">ENUM_POSITION_TYPE type, class="kw">const class="type">class="kw">double volume, class="kw">const class="type">class="kw">string symbol, class="kw">const class="type">class="kw">ulong magic=ULONG_MAX, class="kw">const SL sl=class="num">0, class="kw">const TP tp=class="num">0, class="kw">const class="type">uchar group_id1=class="num">0, class="kw">const class="type">uchar group_id2=class="num">0, class="kw">const class="type">class="kw">string comment=NULL, class="kw">const class="type">class="kw">ulong deviation=ULONG_MAX, class="kw">const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE); class="kw">template<class="kw">typename PS,class="kw">typename PL,class="kw">typename SL,class="kw">typename TP> class="type">int CreatePReqOrder(class="kw">const ENUM_ORDER_TYPE order_type, class="kw">const class="type">class="kw">double volume, class="kw">const class="type">class="kw">string symbol,
「挂单请求与反向平仓的接口骨架」
上面这段声明给出了两类典型的挂单请求构造入口:CreatePReqClose 用于平仓(支持全平与部分平仓),CreatePReqCloseBy 则用于以反向持仓了结当前仓位。两者都不在调用时立即成交,而是把请求塞进队列,等激活条件命中才触发。 CreatePReqClose 的参数里 volume 默认 WRONG_VALUE,意味着若不显式传量,函数层可能拒绝生成有效请求;deviation 默认 ULONG_MAX 则表示沿用最大允许滑价。外汇与贵金属杠杆高,这类挂单在跳空时可能以偏离预期的价格成交,实盘前务必在 MT5 策略测试器里跑一遍边界情形。 SetNewActivationProperties 承接在请求创建之后,用 id 绑定具体挂单,并用 ENUM_PEND_REQ_ACTIVATION_SOURCE 指定激活源。换句话说,挂单逻辑被拆成了『造请求』和『定触发』两步,比直接下市价单更可控,但也要求你清楚每一步的默认值会不会埋坑。
class="kw">const PS price_set, class="kw">const PL price_limit=class="num">0, class="kw">const SL sl=class="num">0, class="kw">const TP tp=class="num">0, class="kw">const class="type">class="kw">ulong magic=ULONG_MAX, class="kw">const class="type">uchar group_id1=class="num">0, class="kw">const class="type">uchar group_id2=class="num">0, class="kw">const class="type">class="kw">string comment=NULL, class="kw">const class="type">class="kw">datetime expiration=class="num">0, 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">//--- Create a pending request(class="num">1) for full and partial position closure, (class="num">2) for closing a position by an opposite one 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=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,