MQL5 秘籍之:OCO订单·综合运用
🔗

MQL5 秘籍之:OCO订单·综合运用

(3/3)· 多数平台的OCO只支持一limit一stop同方向,本文用MQL5类库打破限制写出通用订单对

含代码示例 第 3/3 篇

很多交易者把OCO理解成平台自带的一对限价止损单,遇到跨类型或反向组合就无从下手。MQL5标准类库其实提供了足够的底层构件,只是多数人没把对象关系想清楚就直接调交易函数。本文把前兩篇的概念落进一套可复用的代码框架。

「OCO挂单对的触发前置校验」

在 MT5 面板里点「创建 OCO 对」之前,代码会先轮询 gOrdersProps 数组,逐项确认这笔订单的结构是否齐活。遍历用 for(int idx=0;idx<ArraySize(gOrdersProps);idx++),只要某一个元素的 order_type、volume、price_offset、limit_offset、sl、tp 任一还等于 WRONG_VALUE,就说明用户没填完,直接 is_to_fire_oco=false 并 break 跳出,不再往下走。 这段嵌套 if 的写法很硬:六个条件全满足才 continue,否则立刻清掉创建标识。实盘里这意味着你漏设一个止损点数,OCO 对就不会被下发,避免半残订单进场。外汇与贵金属杠杆高,这种前置拦截能降低误操作概率。 只有 is_to_fire_oco 仍为 true,才会进填充备注、改按钮文字为 "New pair"、染深蓝底浅蓝字并 Enable 的逻辑。按钮文案和颜色就是给交易者的可视反馈:能点=参数齐,灰着=还差字段。 下面这段是点击事件的入口判断,sparam 前 6 字符比对 "myFire" 才认作 OCO 创建键,再叠一层 myButton.IsEnabled() 防重复触发。

MQL5 / C++
for(class="type">int idx=class="num">0;idx<ArraySize(gOrdersProps);idx++)
  {
  class=class="str">"cmt">//---  如果订单类型已设置 
  if(gOrdersProps[idx].order_type!=WRONG_VALUE)
  class=class="str">"cmt">//---  如果交易量已设置  
  if(gOrdersProps[idx].volume!=WRONG_VALUE)
  class=class="str">"cmt">//---  如果市价单的入场点差已设置
  if(gOrdersProps[idx].price_offset!=(class="type">uint)WRONG_VALUE)
  class=class="str">"cmt">//---  如果limit单的入场点差已设置
  if(gOrdersProps[idx].limit_offset!=(class="type">uint)WRONG_VALUE)
  class=class="str">"cmt">//---  如果止损已设置
  if(gOrdersProps[idx].sl!=(class="type">uint)WRONG_VALUE)
  class=class="str">"cmt">//---  如果止赢已设置
  if(gOrdersProps[idx].tp!=(class="type">uint)WRONG_VALUE)
  class="kw">continue;
  class=class="str">"cmt">//--- 清除OCO订单对的创建标识 
  is_to_fire_oco=false;
  break;
  }
class=class="str">"cmt">//--- 创建OCO订单对?
if(is_to_fire_oco)
  {
  class=class="str">"cmt">//--- 填充备注字段
  for(class="type">int ord_idx=class="num">0;ord_idx<ArraySize(gOrdersProps);ord_idx++)
    gOrdersProps[ord_idx].comment=StringFormat("OCO Order %d",ord_idx+class="num">1);
  class=class="str">"cmt">//--- 改变按钮属性
  myButton.Text("New pair");
  myButton.Color(clrDarkBlue);
  myButton.ColorBackground(clrLightBlue);
  class=class="str">"cmt">//--- 响应用户操作 
  myButton.Enable();
  }
}
}
class=class="str">"cmt">//--- 点击按钮
else if(id==CHARTEVENT_OBJECT_CLICK)
  {
  class=class="str">"cmt">//--- 如果是OCO订单对创建按钮
  if(!StringCompare(StringSubstr(sparam,class="num">0,class="num">6),"myFire"))
  class=class="str">"cmt">//--- 响应用户操作
  if(myButton.IsEnabled())
    {
    class=class="str">"cmt">//--- 触发OCO订单对创建事件

OCO 订单对的图表事件接收与列表挂载

在 EA 的 OnChartEvent 里,自定义事件 CHARTEVENT_CUSTOM+ON_OCO 承担的是「新建 OCO 订单对」的触发信号。收到该事件后,先调用 gOco.Init(),传入订单属性集合与当前列表总数加 1 作为新 ID;若初始化返回 true,说明挂单参数已就绪,可在日志里用 PrintFormat 打出形如 "Id of new OCO pair: %I32u" 的配对编号。 随后用 new CiOcoObject(gOco) 在堆上复制一份订单对对象,并用 CheckPointer 确认是 POINTER_DYNAMIC 合法动态指针。只有指针有效,才交给 gOcoList.Add() 挂入全局列表;Add 返回大于 -1 的索引即成功,此时 gOcoList.Total() 就是最新总对数,例如从 0 变 1、从 3 变 4。 任何一步失败(Init 返回 false、指针无效或 Add 返回 -1)都会走 else 分支打印 "OCO-orders placing error!" 或带 ID 的添加失败提示。无论成败,末尾必须调用 Reset() 清空 gOrdersProps 等暂存属性,否则下一轮事件会继承脏数据。外汇与贵金属 OCO 组合涉及双向挂单,滑点或流动性断裂可能导致只成交一侧,属高风险操作,实盘前务必在 MT5 策略测试器跑通这套事件链路。

MQL5 / C++
      EventChartCustom(class="num">0,ON_OCO,class="num">0,class="num">0.0,"OCO_fire");
      Print("Command to create new bunch has been received.");
     }
   }
class=class="str">"cmt">//--- 处理新订单对初始化命令 
  else if(id==CHARTEVENT_CUSTOM+ON_OCO)
  {
   class=class="str">"cmt">//--- OCO订单对初始化
   if(gOco.Init(gOrdersProps,gOcoList.Total()+class="num">1))
     {
      PrintFormat("Id of new OCO pair: %I32u",gOco.Id());
      class=class="str">"cmt">//--- 复制
      CiOcoObject *ptr_new_oco=new CiOcoObject(gOco);
      if(CheckPointer(ptr_new_oco)==POINTER_DYNAMIC)
        {
         class=class="str">"cmt">//--- 添加到列表
         class="type">int node_idx=gOcoList.Add(ptr_new_oco);
         if(node_idx>-class="num">1)
           PrintFormat("Total number of bunch: %d",gOcoList.Total());
         else
           PrintFormat("Error when adding OCO pair %I32u to list!",gOco.Id());
        }
     }
   else
      Print("OCO-orders placing error!");
   class=class="str">"cmt">//--- 清除相关属性
   Reset();
   }

◍ 把OCO面板跑起来的实操路径

前面两节把标准类库处理OCO订单的封装思路讲透了,这一节只补一个最容易被卡住的落点:代码下到本地后怎么在MT5里真正跑起来。有用户反馈按说明解压后导航器里看不到OCO文件夹,本质原因是.mq5和.mqh没有先逐个编译。 正确动作是建一个独立文件夹(比如命名OCO EA),把init_oco.mq5、control_oco_ea.mq5、panel_oco_ea.mq5、ciocoobject.mqh、crandom.mqh全部塞进MT5数据文件夹的MQL5/Experts下。按F4进IDE,把这五个文件挨个打开并编译,构建版本1981和当前新版均能做到零错误零警告。 编译完回MT5导航器,Experts里会多出OCO EA分组,把panel_oco_ea拖上图表就能调出控制面板。外汇与贵金属杠杆高,OCO只是锁风险的一层手段,仓位和止损仍要自己控。 那位2025年9月才实测跑通的用户留了句话:一步步看,别跳。代码能编译通过,差的就是路径和顺序。

把订单对状态交给小布盯盘
小布盯盘的AIGC已内置OCO挂单对的识别与冲突提示,打开对应品种页即可看到当前未触发订单的互撤关系,你只管定策略边界。

常见问题

并非必须。文内用CiOcoObject类演示了任意挂单类型组合的程序化互撤,限制多来自平台UI而非机制本身,外汇贵金属波动剧烈仍属高风险。
该标准枚举含市场订单类型,而OCO只需挂单。专用枚举ENUM_PENDING_ORDER_TYPE可避免初始化时误选成市价单。
每个OCO对象由CRandom生成独立ID,便于在订单池里定位与反初始化,避免多对挂单互相串号。
可以。把EA跑出的挂单对同步到小布盯盘品种页,系统会标注互撤链路并在其一成交时提示剩余单将撤,省去手动核对终端。
关于CTrade封装与基础挂单类的完整讨论见《MQL5秘籍之:OCO订单·基础篇》。