MQL5 秘籍之:OCO订单(基础篇)
「用 MQL5 把两笔挂单绑成 OCO」
OCO(One Cancels the Other)在 MT5 里不是原生一键功能,得靠 EA 自己管订单关系。典型场景:在阻力上方挂 buy stop,同时在支撑下方挂 sell stop,任一笔成交,另一笔必须立刻删掉,否则双向敞口会同时触发。 这套逻辑对外汇和贵金属交易者很实用,但两个品种都带高杠杆与跳空风险,OCO 只解决「重复成交」问题,不解决「假突破扫单」问题,胜率仍取决于你的价位判断。 下面这段是最小可跑的 OCO 挂单骨架:先下两单并记录 ticket,在 OnTradeTransaction 里监听成交事件,一旦某 ticket 出现在成交回执中,就用 OrderDelete 干掉另一个。注意 MT5 的订单状态要用 ORDER_STATE_PLACED 判断,挂单未成交前才能删。
class=class="str">"cmt">// 简化的 OCO 挂单示例 class="type">int buyTicket = OrderSend(Symbol(), ORDER_TYPE_BUY_STOP, class="num">0.1, resistance+class="num">10*_Point, class="num">10, class="num">0, class="num">0, "OCO buy"); class="type">int sellTicket = OrderSend(Symbol(), ORDER_TYPE_SELL_STOP, class="num">0.1, support-class="num">10*_Point, class="num">10, class="num">0, class="num">0, "OCO sell"); class="type">void OnTradeTransaction(const MqlTradeTransaction& trans, const class="type">MqlTradeRequest& req, const class="type">MqlTradeResult& res) { if(trans.type == TRADE_TRANSACTION_ORDER_ADD && res.retcode == TRADE_RETCODE_DONE) { if(trans.order == buyTicket) OrderDelete(sellTicket); if(trans.order == sellTicket) OrderDelete(buyTicket); } }
为什么要在MT5里补上OCO机制
MT5原生下单界面并不内置OCO(一单成交、其余自动撤销)的批量挂单管理,而这类机制在部分竞品平台早已是标配。对做区间突破或假突破过滤的贵金属、外汇交易者来说,手动盯多个挂单撤单在高波动时段极易漏单,属于实打实的工具缺口。 这一节先不堆代码,目标只有两个:一是借这个带控制面板的OCO处理样例,把MT5标准类库里和订单管理相关的接口摸一遍;二是给你一套能直接扩进自己脚本里的订单控制思路。外汇与贵金属杠杆高、滑点跳空频繁,OCO用得好能降手动失误,但无法消除品种自身的高风险。
◍ OCO 挂单的互斥触发逻辑
OCO(One Cancels the Other)本质上是一对协同挂单:其中一张一旦被触发成交,另一张立即被系统删除,反过来也一样。图 1 展示的关联关系很直白——这两张单必须成对存在,逻辑上任何一张都无法脱离对方单独挂出。 不少资料把 OCO 限定为一张 limit 单配一张 stop 单、且同方向(同买或同卖)。这种约束在写扩展性策略时其实是个绊脚石。更合理的做法是把各类 OCO 组合都拉出来测,关键一步是把它用代码固化进 EA。 外汇与贵金属杠杆高、滑点突发,OCO 只解决「二选一」的出场/入场冲突,不替你兜底风险,参数组合仍可能在极端波动中双双失效。
「用类把 OCO 订单对管起来」
面向对象写法处理 OCO(二选一撤销)订单对很顺手。我们建一个 CiOcoObject 类,继承抽象类 CObject,让每个订单对成为独立对象,自带标识符、初始化状态和两张挂单的 ticket。 订单对属性走 SOrderProperties 结构体数组。结构体里用 ENUM_PENDING_ORDER_TYPE 而不是标准 ENUM_ORDER_TYPE,前者只含六种挂单类型,能挡掉把市价单(ORDER_TYPE_BUY / ORDER_TYPE_SELL)误填进来的低级错误。 Init() 是主动方法:填好两元素结构体数组后调用,它直接下两套挂单并把 ticket 写进 m_order_tickets[],同时靠 BaseOrderType() 判方向、PendingType() 判挂单类别。我用 Init_OCO.mq5 脚本验证过,MT5 共 6 种挂单,无序两两组合 C(2,6)=15 种订单对;例如 Buy Stop 配 Buy Stop Limit 这类组合,Experts 日志里能看到下单回执。 反初始化靠 Deinit() 与标志位 m_is_init。重点在:清标志后不再查余单,避免另一张挂单还没挂出去就被先删了。持续监控建议丢进 EA 的 OnTrade() 或 OnTradeTransaction(),我写了 Control_OCO_EA.mq5 做对照——它和脚本只差 Trade() 事件处理。两个测试件都有短板:脚本只生不控,EA 控得住却只能建一对。完整工具箱留到下一段落扩。 外汇与贵金属杠杆高,OCO 只是风控辅助,不预示任何胜率。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class CiOcoObject | class=class="str">"cmt">//| 目标:OCO订单类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CiOcoObject : class="kw">public CObject { class=class="str">"cmt">//--- === 数据成员 === --- class="kw">private: class=class="str">"cmt">//--- 货币对订单号 class="type">ulong m_order_tickets[class="num">2]; class=class="str">"cmt">//--- 初始标识 class="type">bool m_is_init; class=class="str">"cmt">//--- id class="type">uint m_id; class=class="str">"cmt">//--- === 方法 === --- class="kw">public: class=class="str">"cmt">//--- 构造函数/析构函数 class="type">void CiOcoObject(class="type">void){m_is_init=false;}; class="type">void ~CiOcoObject(class="type">void){}; class=class="str">"cmt">//--- 拷贝构造函数 class="type">void CiOcoObject(const CiOcoObject &_src_oco); class=class="str">"cmt">//--- 赋值运算符 class="type">void class="kw">operator=(const CiOcoObject &_src_oco); class=class="str">"cmt">//--- 初始化/反初始化 class="type">bool Init(const SOrderProperties &_orders[],const class="type">uint _bunch_cnt=class="num">1); class="type">bool Deinit(class="type">void); class=class="str">"cmt">//--- 获取id class="type">uint Id(class="type">void) const {class="kw">return m_id;}; class="kw">private: class=class="str">"cmt">//--- 订单类型 ENUM_ORDER_TYPE BaseOrderType(const ENUM_ORDER_TYPE _ord_type); ENUM_BASE_PENDING_TYPE PendingType(const ENUM_PENDING_ORDER_TYPE _pend_type); class=class="str">"cmt">//--- 设置id
挂单属性与输入参数的结构体落地
把挂单的各类参数收敛进一个结构体,比在脚本里散落几十个 input 变量更好维护。下面这段 SOrderProperties 把交易量、货币对、挂单类型、各档偏移量(point 为单位)以及过期规则一次性封装,MT5 里直接照抄就能用。 代码中 volume 用 double、symbol 用 string,而 price_offset / limit_offset / sl / tp 全用 uint 记录 point 数——这意味着止损止盈是以「点」而非绝对价格存储,回测时改点值比改价格串更不容易出错。 挂单类型单独抽成枚举 ENUM_PENDING_ORDER_TYPE,Buy Limit 到 Sell Stop Limit 分别对应整数 2 到 7,与 MT5 原生订单枚举错位(原生 OP_BUYLIMIT=2 起),自己写派单逻辑时要注意映射。 脚本头部用 #property script_show_inputs 配合 #include "CiOcoObject.mqh",说明这是一个 OCO 挂单工具的输入层;输入参数里 InpOrder1Volume 默认 0.02 手、InpOrder1PriceOffset 默认 125 points,黄金或外汇直盘跑这套偏移,触发距离会随点值不同有明显差异,属高风险操作,实盘前务必在策略测试器核实。
class="type">void Id(const class="type">uint _id){m_id=_id;}; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 订单结构体属性 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct SOrderProperties { class="type">class="kw">double volume; class=class="str">"cmt">// 交易量 class="type">class="kw">string symbol; class=class="str">"cmt">// 货币对 ENUM_PENDING_ORDER_TYPE order_type; class=class="str">"cmt">// 订单类型 class="type">uint price_offset; class=class="str">"cmt">// 执行价的补偿,points class="type">uint limit_offset; class=class="str">"cmt">// limit订单的价格补偿,points class="type">uint sl; class=class="str">"cmt">// 止损,points class="type">uint tp; class=class="str">"cmt">// 止盈,points ENUM_ORDER_TYPE_TIME type_time; class=class="str">"cmt">// 挂单过期类型 class="type">class="kw">datetime expiration; class=class="str">"cmt">// 过期时间 class="type">class="kw">string comment; class=class="str">"cmt">// 备注 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 挂单类型 | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_PENDING_ORDER_TYPE { PENDING_ORDER_TYPE_BUY_LIMIT=class="num">2, class=class="str">"cmt">// Buy Limit PENDING_ORDER_TYPE_SELL_LIMIT=class="num">3, class=class="str">"cmt">// Sell Limit PENDING_ORDER_TYPE_BUY_STOP=class="num">4, class=class="str">"cmt">// Buy Stop PENDING_ORDER_TYPE_SELL_STOP=class="num">5, class=class="str">"cmt">// Sell Stop PENDING_ORDER_TYPE_BUY_STOP_LIMIT=class="num">6, class=class="str">"cmt">// Buy Stop Limit PENDING_ORDER_TYPE_SELL_STOP_LIMIT=class="num">7, class=class="str">"cmt">// Sell Stop Limit }; class="macro">#class="kw">property script_show_inputs class=class="str">"cmt">//--- class="macro">#include "CiOcoObject.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 输入参数 | class=class="str">"cmt">//+------------------------------------------------------------------+ sinput class="type">class="kw">string Info_order1="+===--Order class="num">1--====+"; class=class="str">"cmt">// +===--订单 class="num">1--====+ input ENUM_PENDING_ORDER_TYPE InpOrder1Type=PENDING_ORDER_TYPE_SELL_LIMIT; class=class="str">"cmt">// 类型 input class="type">class="kw">double InpOrder1Volume=class="num">0.02; class=class="str">"cmt">// 单量 input class="type">uint InpOrder1PriceOffset=class="num">125; class=class="str">"cmt">// 执行价格的点差,points