MQL5 秘籍之:OCO订单·进阶篇
🔗

MQL5 秘籍之:OCO订单·进阶篇

(2/3)· 多数平台把OCO锁死成limit+stop同向,本文用CObject派生类打破这层限制

含代码示例 第 2/3 篇
把OCO当成只能一limit一stop同向组合的交易者,会直接砍掉跨类型对冲策略的发挥空间。MetaTrader 5原生不提供该机制,手写时若用错枚举类型,下拉框里会混进市价单选项导致初始化报错。

◍ 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 单相对触发价的偏移。若点差常态大于此值,挂单可能直接穿价不成交,建议按品种实测点差中位数上调。

MQL5 / C++
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 撤单若漏掉半边残单,可能在没有对冲保护的情况下裸奔,实盘前务必在策略测试器里跑一遍这组逻辑。

MQL5 / C++
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' 前缀匹配。下面这段是事件分派的核心骨架。

MQL5 / C++
class="macro">#define ON_OCO(class="num">101) class=class="str">"cmt">// OCO 订单对创建事件 
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ChartEvent函数          &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|
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 错位会让第二张单参数全写到第一张上。外汇与贵金属杠杆高,面板填错偏移量可能瞬间触发非预期挂单,先在策略测试器跑一遍再上实盘。

MQL5 / C++
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">//--- 检查结构体
把订单对状态交给小布看
这些OCO订单对的存活与互撤状态,小布盯盘的AIGC已内置监控,打开对应品种页即可看到实时关联,你只管判断触发后的方向。

常见问题

并非必须。资料中常见限制对策略扩展不利,程序化时应分析各类OCO组合并支持自定义类型。
基于CObject新建,每个OCO订单的标识符由CRandom随机数发生器对象赋值,保证实例区分。
可以,小布盯盘品种页的AIGC模块会解析挂单互撤关系并呈现,省去手动核对终端历史。
标准枚举含市价单类型,而下拉选择仅需挂单,专用ENUM_PENDING_ORDER_TYPE可避免误选市场订单。