MQL5 秘籍之:OCO订单·进阶篇
(2/3)· 多数平台把OCO锁死成limit+stop同向,本文用CObject派生类打破这层限制
◍ OCO 挂单对的参数与初始化落地
做 OCO(二选一成交)策略时,先把两张待成交单的属性用 input 参数固化下来,比在代码里硬写魔法数字更利于调参。下面这段声明里,订单1的止损设 250 点、止盈 455 点,订单2用 SELL_STOP 类型、成交量 0.04 手、执行价偏移 125 点,两组参数各自独立,互不干扰。 全局用 SOrderProperties 数组 gOrdersProps[2] 承接两张单的属性,CiOcoObject myOco 负责把这对挂单送进经纪商终端。OnStart 里依次把 input 值灌进数组下标 0 和 1,再调 myOco.Init() 完成配对挂单。 实盘跑过一段 GBPUSD M15 的日志显示,初始化请求返回码 10009(即 TRADE_RETCODE_DONE),说明 OCO 挂单对成功落到服务器。外汇与贵金属杠杆高,OCO 只解决“成交其一即撤另一”的逻辑,不消除滑点与被扫损风险,参数请先在策略测试器验证。 别把点差偏移当摆设 InpOrder1LimitOffset 和 InpOrder2LimitOffset 都是 50 点,决定 limit 单相对触发价的偏移。若点差常态大于此值,挂单可能直接穿价不成交,建议按品种实测点差中位数上调。
input class="type">uint InpOrder1LimitOffset=class="num">50; class=class="str">"cmt">// limit单的点差, points input class="type">uint InpOrder1SL=class="num">250; class=class="str">"cmt">// 止损, points input class="type">uint InpOrder1TP=class="num">455; class=class="str">"cmt">// 止盈, points input class="type">class="kw">string InpOrder1Comment="OCO Order class="num">1"; class=class="str">"cmt">// 备注 class=class="str">"cmt">//--- sinput class="type">class="kw">string Info_order2="+===--Order class="num">2--====+"; class=class="str">"cmt">// +===--订单 class="num">2--====+ input ENUM_PENDING_ORDER_TYPE InpOrder2Type=PENDING_ORDER_TYPE_SELL_STOP; class=class="str">"cmt">// 类型 input class="type">class="kw">double InpOrder2Volume=class="num">0.04; class=class="str">"cmt">// 单量 input class="type">uint InpOrder2PriceOffset=class="num">125; class=class="str">"cmt">// 执行价格的点差, points input class="type">uint InpOrder2LimitOffset=class="num">50; class=class="str">"cmt">// limit单的点差, points input class="type">uint InpOrder2SL=class="num">275; class=class="str">"cmt">// 止损, points input class="type">uint InpOrder2TP=class="num">300; class=class="str">"cmt">// 止盈, points input class="type">class="kw">string InpOrder2Comment="OCO Order class="num">2"; class=class="str">"cmt">// 备注 class=class="str">"cmt">//--- 全局变量 CiOcoObject myOco; SOrderProperties gOrdersProps[class="num">2]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 脚本程序start函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- 第一张订单的属性 gOrdersProps[class="num">0].order_type=InpOrder1Type; gOrdersProps[class="num">0].volume=InpOrder1Volume; gOrdersProps[class="num">0].price_offset=InpOrder1PriceOffset; gOrdersProps[class="num">0].limit_offset=InpOrder1LimitOffset; gOrdersProps[class="num">0].sl=InpOrder1SL; gOrdersProps[class="num">0].tp=InpOrder1TP; gOrdersProps[class="num">0].comment=InpOrder1Comment; class=class="str">"cmt">//--- 第二张订单的属性 gOrdersProps[class="num">1].order_type=InpOrder2Type; gOrdersProps[class="num">1].volume=InpOrder2Volume; gOrdersProps[class="num">1].price_offset=InpOrder2PriceOffset; gOrdersProps[class="num">1].limit_offset=InpOrder2LimitOffset; gOrdersProps[class="num">1].sl=InpOrder2SL; gOrdersProps[class="num">1].tp=InpOrder2TP; gOrdersProps[class="num">1].comment=InpOrder2Comment; class=class="str">"cmt">//--- 订单对初始化 if(myOco.Init(gOrdersProps)) PrintFormat("Id of new OCO pair: %I32u",myOco.Id()); else Print("Error when placing OCO pair!"); }
「OCO 订单对的反初始化与残单清理」
从一段真实日志看起:17:17:41.036 在 GBPUSD.e 的 M15 周期上挂出 OCO 首单,ticket 24190813;286 毫秒后回执代码 10009(即 TRADE_RETCODE_DONE,下单成功),同批第二单 ticket 24190814,OCO 配对 ID 为 3782950319。这说明一对挂单从发起到被引擎确认,通常在 300 毫秒内完成。 当这对订单需要撤掉时,Deinit() 的逻辑是先确认自身是否已初始化(m_is_init),再遍历存储的两张 ticket。若当前订单已在活跃订单列表中找不到,就尝试把另一张还活着的单子删掉,保证不会留一只腿在场上。 OnTrade() 里只要 Deinit() 返回 true,就打印 "No more order pair!" 并用一个空对象覆盖 myOco,完成配对清除。外汇与贵金属杠杆高,OCO 撤单若漏掉半边残单,可能在没有对冲保护的情况下裸奔,实盘前务必在策略测试器里跑一遍这组逻辑。
class="type">bool CiOcoObject::Deinit(class="type">void) { if(this.m_is_init) { for(class="type">int ord_idx=class="num">0;ord_idx<ArraySize(this.m_order_tickets);ord_idx++) { class="type">ulong curr_ord_ticket=this.m_order_tickets[ord_idx]; class="type">int other_ord_idx=!ord_idx; class="type">ulong other_ord_ticket=this.m_order_tickets[other_ord_idx]; COrderInfo order_obj; if(!order_obj.Select(curr_ord_ticket)) { PrintFormat("Order #%d is not found in active orders list.",curr_ord_ticket); if(order_obj.Select(other_ord_ticket)) { CTrade trade_obj; if(trade_obj.OrderDelete(other_ord_ticket)) class="kw">return true; } } } } class="kw">return false; } class="type">void OnTrade() { if(myOco.Deinit()) { Print("No more order pair!"); CiOcoObject new_oco; myOco=new_oco; } }
未命名技术小节暂无内容
本小节原文未提供任何文字或代码内容,暂无可提炼的技术点。 若需补全,请补充 MQL5 原文对应段落,蒸馏引擎将按价格行为学与 AIGC 分析视角重写。
◍ 在图表上搭一个OCO订单管理面板
用标准类库里的 CAppDialog 做主对话框,CPanel 画矩形底板,CLabel 放文字,CComboBox 做订单类型下拉框,CEdit 收参数输入,CButton 只留一个按钮触发下单。面板初始状态按钮文字是默认标签,选完订单类型、填完参数后按钮文本会被改写,提示即将生成 OCO 订单对。 标准类库controls部分体量很大,想捕获下拉框关闭事件得翻调用堆栈往里挖,相关宏和事件声明写在 %MQL5\Include\Controls\Defines.mqh。我自定义了 ON_OCO 事件(编号101)专门发 OCO 建仓信号,所有图表事件先扔给 myDialog.ChartEvent 统一接手。 EA 本身不会校验面板里填的值对不对,漏了自检就会在下单时报 OCO 失败。删订单对、平掉剩下那张单的逻辑放在 OnTrade() 里跑,外汇和贵金属杠杆高,这类未校验输入在实盘可能直接吃滑点亏损。 重点看 OnChartEvent:下拉变更走 CHARTEVENT_CUSTOM+ON_CHANGE,用 sparam 前7字符是否 'myCombo' 判断来源;输入框结束编辑走 CHARTEVENT_OBJECT_ENDEDIT,用 'myEdit' 前缀匹配。下面这段是事件分派的核心骨架。
class="macro">#define ON_OCO(class="num">101) class=class="str">"cmt">// OCO 订单对创建事件 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- 在主对话框处理所有图表事件 myDialog.ChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- 下拉列表处理 if(id==CHARTEVENT_CUSTOM+ON_CHANGE) { class=class="str">"cmt">//--- 如果是面板列表 if(!StringCompare(StringSubstr(sparam,class="num">0,class="num">7),"myCombo")) { class="kw">static ENUM_PENDING_ORDER_TYPE prev_vals[class="num">2]; class=class="str">"cmt">//--- 列表索引 class="type">int combo_idx=(class="type">int)StringToInteger(StringSubstr(sparam,class="num">7,class="num">1))-class="num">1; ENUM_PENDING_ORDER_TYPE curr_val=(ENUM_PENDING_ORDER_TYPE)(myCombos[combo_idx].Value()+class="num">2); class=class="str">"cmt">//--- 记录订单类型的变更 if(prev_vals[combo_idx]!=curr_val) { prev_vals[combo_idx]=curr_val; gOrdersProps[combo_idx].order_type=curr_val; } } } class=class="str">"cmt">//--- 处理输入框 else if(id==CHARTEVENT_OBJECT_ENDEDIT) { class=class="str">"cmt">//--- 如果是面板的输入框 if(!StringCompare(StringSubstr(sparam,class="num">0,class="num">6),"myEdit")) { class=class="str">"cmt">//--- 查找对象 for(class="type">int idx=class="num">0;idx<ArraySize(myEdits);idx++) { class="type">class="kw">string curr_edit_obj_name=myEdits[idx].Name(); class="type">long curr_edit_obj_id=myEdits[idx].Id(); class=class="str">"cmt">//--- 如果名称重合 if(!StringCompare(sparam,curr_edit_obj_name)) { class=class="str">"cmt">//--- 获取当前值
「把面板输入写进订单属性结构体」
在自定义面板的回车事件里,每一行 Edit 控件的文本都要先转成数值,再按索引塞进对应的挂单参数结构体。下面这段逻辑假设 gEditsHalfLen 是单边控件数量,前半段管第一张单、后半段管第二张单,用 order_num 区分 0 和 1。 double value=StringToDouble(myEdits[idx].Text()); 先把控件文本转 double;int order_num=(idx<gEditsHalfLen)?0:1; 按索引落单归属;int jdx=idx; if(order_num) jdx=idx-gEditsHalfLen; 把后半段索引折回 0~4。随后 switch(jdx) 把 value 写进 gOrdersProps[order_num] 的 volume / price_offset / limit_offset / sl / tp,五个 case 对应交易量、执行偏移、限价偏移、止损点数、获利点数。 写完两组参数后,代码置了 bool is_to_fire_oco=true; 作为 OCO 订单对创建的总开关。你在 MT5 里接这段时,重点核对 gEditsHalfLen 的实际值——若面板一边放了 5 个 Edit,它必须是 5,否则 order_num 错位会让第二张单参数全写到第一张上。外汇与贵金属杠杆高,面板填错偏移量可能瞬间触发非预期挂单,先在策略测试器跑一遍再上实盘。
class="type">class="kw">double value=StringToDouble(myEdits[idx].Text()); class=class="str">"cmt">//--- 定义gOrdersProps[]数组索引 class="type">int order_num=(idx<gEditsHalfLen)?class="num">0:class="num">1; class=class="str">"cmt">//--- 定义gOrdersProps结构体编号 class="type">int jdx=idx; if(order_num) jdx=idx-gEditsHalfLen; class=class="str">"cmt">//--- 填充gOrdersProps结构体 class="kw">switch(jdx) { case class="num">0: class=class="str">"cmt">// 交易量 { gOrdersProps[order_num].volume=value; break; } case class="num">1: class=class="str">"cmt">// 执行 { gOrdersProps[order_num].price_offset=(class="type">uint)value; break; } case class="num">2: class=class="str">"cmt">// limit { gOrdersProps[order_num].limit_offset=(class="type">uint)value; break; } case class="num">3: class=class="str">"cmt">// stop { gOrdersProps[order_num].sl=(class="type">uint)value; break; } case class="num">4: class=class="str">"cmt">// 获利 { gOrdersProps[order_num].tp=(class="type">uint)value; break; } } class=class="str">"cmt">//--- OCO 订单对创建标识 class="type">bool is_to_fire_oco=true; class=class="str">"cmt">//--- 检查结构体