MQL5 秘籍之:OCO订单

Denis Kirichenko | 13 八月, 2015

简介

本文聚焦于处理OCO类型的订单。这个机制在一些MetaTrader 5的竞争对手产品中已有实现。通过这个带有控制面板的处理OCO订单的例子,我想达到两个目的。其一,我想介绍标准类库的特性,另一方面我想扩展交易者的交易工具。


1. OCO订单的本质

OCO订单(一个订单取消另一个订单)代表一对挂单。

他们通过相互撤销的机制协同运作:如果一个订单激活了,那么第二个订单将被删除,反之亦然。

图 1 一对OCO订单

图 1 一对OCO订单

图 1 表示一个简单的订单关联关系。它代表了:两张订单必须同时存在。根据逻辑关系,这对订单中的任何一张单子都无法独自存在。

有些资料上说这对订单必须一单为limit单,另一单为stop单,并且订单必须是同一个方向的(买或者卖)。据我所知这样的限制对于创造富有扩展性的交易策略是不利的。我建议各种OCO订单对类型都应该被分析一下,更为重要的是我们要将其程序化。


2. 程序化订单对

在我看来,OOP工具箱对于编写同OCO订单相关的任务是非常合适的。

下面部分将创建用于实现我们目标的新数据类型。首选是CiOcoObject类。


2.1. CiOcoObject 类

因此,我们需要一些类来操控两个相关订单。

我们基于抽象类CObject创建一个新的对象。

新的类如下:

//+------------------------------------------------------------------+
//| Class CiOcoObject                                                |
//| 目标:OCO订单类                                                    |            
//+------------------------------------------------------------------+
class CiOcoObject : public CObject
  {
   //--- === 数据成员 === --- 
private:
   //--- 货币对订单号
   ulong             m_order_tickets[2];
   //--- 初始标识
   bool              m_is_init;
   //--- id
   uint              m_id;

   //--- === 方法 === --- 
public:
   //--- 构造函数/析构函数
   void              CiOcoObject(void){m_is_init=false;};
   void             ~CiOcoObject(void){};
   //--- 拷贝构造函数
   void              CiOcoObject(const CiOcoObject &_src_oco);
   //--- 赋值运算符
   void              operator=(const CiOcoObject &_src_oco);

   //--- 初始化/反初始化
   bool              Init(const SOrderProperties &_orders[],const uint _bunch_cnt=1);
   bool              Deinit(void);
   //--- 获取id
   uint              Id(void) const {return m_id;};

private:
   //--- 订单类型
   ENUM_ORDER_TYPE   BaseOrderType(const ENUM_ORDER_TYPE _ord_type);
   ENUM_BASE_PENDING_TYPE PendingType(const ENUM_PENDING_ORDER_TYPE _pend_type);
   //--- 设置id
   void              Id(const uint _id){m_id=_id;};
  };

每个OCO订单都会有它自己的标识符。它的值由随机数发生器(CRandom类的对象)设置。

订单对的初始化和反初始化方法在接口中。第一个方法创建(初始化)订单对,第二个方法移除(销毁)订单对。

CiOcoObject::Init() 方法接收SOrderProperties 结构体数组类型作为参数。这个结构体类型中存储订单对的相关属性,例如:OCO订单。


2.2 SOrderProperties 结构体

我们来看看前面提到的结构体的内部。

//+------------------------------------------------------------------+
//| 订单结构体属性                                                     |
//+------------------------------------------------------------------+
struct SOrderProperties
  {
   double                  volume;           // 交易量   
   string                  symbol;           // 货币对
   ENUM_PENDING_ORDER_TYPE order_type;       // 订单类型   
   uint                    price_offset;     // 执行价的补偿,points
   uint                    limit_offset;     // limit订单的价格补偿,points
   uint                    sl;               // 止损,points
   uint                    tp;               // 止盈,points
   ENUM_ORDER_TYPE_TIME    type_time;        // 挂单过期类型
   datetime                expiration;       // 过期时间
   string                  comment;          // 备注
  }

为了执行初始化方法,我们必须先填充含有两个元素的结构体数组。简单来说,我们得告诉程序要放置哪个订单。

枚举值ENUM_PENDING_ORDER_TYPE枚举值类型用于结构体中:

//+------------------------------------------------------------------+
//| 挂单类型                                                          |
//+------------------------------------------------------------------+
enum ENUM_PENDING_ORDER_TYPE
  {
   PENDING_ORDER_TYPE_BUY_LIMIT=2,       // Buy Limit
   PENDING_ORDER_TYPE_SELL_LIMIT=3,      // Sell Limit
   PENDING_ORDER_TYPE_BUY_STOP=4,        // Buy Stop
   PENDING_ORDER_TYPE_SELL_STOP=5,       // Sell Stop
   PENDING_ORDER_TYPE_BUY_STOP_LIMIT=6,  // Buy Stop Limit
   PENDING_ORDER_TYPE_SELL_STOP_LIMIT=7, // Sell Stop Limit
  };

总的来说,它看上去和标准的枚举ENUM _ORDER_TYPE变量一样,但是仅能够选择挂单。

在输入参数(图2)中选择相应订单类型时,它可以避免出现错误。

图 2. "Type"下拉列表中提供了订单类型。

图 2. "Type"下拉列表中提供了订单类型。

如果我们使用ENUM _ORDER_TYPE标准枚举类型,那么我们能够设置为市场订单类型(ORDER_TYPE_BUY 或 ORDER_TYPE_SELL),但对于只需要处理挂单的我们来说并没不需要。


2.3. 初始化订单对

如上所述,CiOcoObject::Init()方法用于订单对的初始化。

它用于下单及记录新的订单对是否成功出现。这是一个主动的方法,其自身执行交易操作。我们也可以创建被动方法。它将两个已经存在的分别独立放置的挂单关联起来。

我不会提供此方法完成的代码。但我要提醒读者的是,计算所有价格(开盘价,止损价,止盈价,限价)是非常重要的,交易类中的方法CTrade::OrderOpen()能够下单。最后我们还要考虑两件事:订单方向(买或卖)以及相对于现价订单的执行价格(高于或低于)。

该方法调用一些私有方法:BaseOrderType() 以及 PendingType()。第一个确定订单方向,第二个确定挂单类型。

下单后,订单号保存在m_order_tickets[]数组中。

我使用一个简单的Init_OCO.mq5脚本来测试此方法。

#property script_show_inputs
//---
#include "CiOcoObject.mqh"
//+------------------------------------------------------------------+
//| 输入参数                                                          |
//+------------------------------------------------------------------+
sinput string Info_order1="+===--Order 1--====+";   // +===--订单 1--====+
input ENUM_PENDING_ORDER_TYPE InpOrder1Type=PENDING_ORDER_TYPE_SELL_LIMIT; // 类型
input double InpOrder1Volume=0.02;                  // 单量
input uint InpOrder1PriceOffset=125;                // 执行价格的点差,points
input uint InpOrder1LimitOffset=50;                 // limit单的点差, points
input uint InpOrder1SL=250;                         // 止损, points
input uint InpOrder1TP=455;                         // 止盈, points
input string InpOrder1Comment="OCO Order 1";        // 备注
//---
sinput string Info_order2="+===--Order 2--====+";   // +===--订单 2--====+
input ENUM_PENDING_ORDER_TYPE InpOrder2Type=PENDING_ORDER_TYPE_SELL_STOP; // 类型
input double InpOrder2Volume=0.04;                  // 单量    
input uint InpOrder2PriceOffset=125;                // 执行价格的点差, points
input uint InpOrder2LimitOffset=50;                 // limit单的点差, points
input uint InpOrder2SL=275;                         // 止损, points
input uint InpOrder2TP=300;                         // 止盈, points
input string InpOrder2Comment="OCO Order 2";        // 备注

//--- 全局变量
CiOcoObject myOco;
SOrderProperties gOrdersProps[2];
//+------------------------------------------------------------------+
//| 脚本程序start函数                                                  |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- 第一张订单的属性
   gOrdersProps[0].order_type=InpOrder1Type;
   gOrdersProps[0].volume=InpOrder1Volume;
   gOrdersProps[0].price_offset=InpOrder1PriceOffset;
   gOrdersProps[0].limit_offset=InpOrder1LimitOffset;
   gOrdersProps[0].sl=InpOrder1SL;
   gOrdersProps[0].tp=InpOrder1TP;
   gOrdersProps[0].comment=InpOrder1Comment;

//--- 第二张订单的属性
   gOrdersProps[1].order_type=InpOrder2Type;
   gOrdersProps[1].volume=InpOrder2Volume;
   gOrdersProps[1].price_offset=InpOrder2PriceOffset;
   gOrdersProps[1].limit_offset=InpOrder2LimitOffset;
   gOrdersProps[1].sl=InpOrder2SL;
   gOrdersProps[1].tp=InpOrder2TP;
   gOrdersProps[1].comment=InpOrder2Comment;

//--- 订单对初始化
   if(myOco.Init(gOrdersProps))
      PrintFormat("Id of new OCO pair: %I32u",myOco.Id());
   else
      Print("Error when placing OCO pair!");
  }

此处你可以设置订单对的各种属性。MetaTrader 5 有6中不同类型的挂单。

这样的话,将会有15种订单对(组合)(如果订单对中的订单都为不同类型的话)。

C(k,N) = C(2,6) = 15

所有变量都在脚本中测试过了。我将举一个Buy Stop - Buy Stop Limit订单对的例子。

订单类型在脚本参数(图3)中指定。


图 3. "Buy Stop"订单和"Buy Stop Limit"订单组成的订单对

图 3. "Buy Stop"订单和"Buy Stop Limit"订单组成的订单对

以下信息将在“Experts”中体现:

QO      0       17:17:41.020    Init_OCO (GBPUSD.e,M15) Code of request result: 10009
JD      0       17:17:41.036    Init_OCO (GBPUSD.e,M15) New order ticket: 24190813
QL      0       17:17:41.286    Init_OCO (GBPUSD.e,M15) Code of request result: 10009
JH      0       17:17:41.286    Init_OCO (GBPUSD.e,M15) New order ticket: 24190814
MM      0       17:17:41.379    Init_OCO (GBPUSD.e,M15) Id of new OCO pair: 3782950319

但是如果不进行循环操作,我们无法通过脚本来处理好OCO订单。


2.4. 订单对的反初始化

此方法用于控制订单对。当任何一个订单从活跃订单列表中消失时,订单对将“消亡”。

我认为此方法应该在EA的OnTrade()或者OnTradeTransaction()函数中实现。这样的话,EA将能够无延时的处理任何定单对的激活。

//+------------------------------------------------------------------+
//| 订单对反初始化                                                     |
//+------------------------------------------------------------------+
bool CiOcoObject::Deinit(void)
  {
//--- 如果订单对已初始化
   if(this.m_is_init)
     {
      //---检查订单 
      for(int ord_idx=0;ord_idx<ArraySize(this.m_order_tickets);ord_idx++)
        {
         //--- 订单对的当前订单
         ulong curr_ord_ticket=this.m_order_tickets[ord_idx];
         //--- 另一个订单
         int other_ord_idx=!ord_idx;
         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))
                  return true;
              }
           }
        }
     }
//---
   return false;
  }

我要提醒一个细节。在类的方法中检查订单对的初始化标志。如果此标识被清除则不会检查订单。这样做是为了避免在另一张挂单好没有放置好之前删除已经存在的订单。

让我们为脚本添加一些功能,用于处理一些订单被放置后的情况。我们创建Control_OCO_EA.mq5测试EA。

一般来说,EA同脚本的区别仅仅在于 Trade()事件处理模块:

//+------------------------------------------------------------------+
//| 交易函数                                                          |
//+------------------------------------------------------------------+
void OnTrade()
  {
//--- OCO 订单反初始化
   if(myOco.Deinit())
     {
      Print("No more order pair!");
      //--- 清除订单对
      CiOcoObject new_oco;
      myOco=new_oco;
     }
  }


这个视频展示了两者在MetaTrader 5终端中的运作情况。



然而这两个测试程序都有缺陷。

第一个程序(脚本)只能创建订单对但是随后便失去了对它们的控制。

第二个程序(EA)虽然能够控制订单对,但是在创建完第一个后,不能够重复创建其他订单对。为了构建功能完整的 OCO 订单程序(脚本),我们要将其扩展为可以做单的工具箱。在下一章节中我们来实现之。


3. 控制 EA

让我们在图表上创建OCO订单管理面板,来下单并设置订单对的参数。

这是控制EA的(图4)的一部分。源码在Panel_OCO_EA.mq5中。

图 4. 创建OCO订单的面板:初始状态

图 4. 创建OCO订单的面板:初始状态


我们要选择一个订单类型,并且设置相关参数来下OCO订单对。

然后面板上的唯一按钮的标签将被改变(文本属性,图5)。

Fig. 5. 创建OCO订单的面板:新的订单对

Fig. 5. 创建OCO订单的面板:新的订单对


标准类库中的类被用于构建我们的面板:

当然,面板类的方法自动被调用。

现在我们来看看代码。不得不提的是用于创建指示性面板和对话框的标准类库部分非常的大。

例如,如果你想要捕获下拉列表关闭事件,就不得不深入到堆栈内部来调用。

Fig. 6. 调用堆栈

Fig. 6. 调用堆栈


开发者在%MQL5\Include\Controls\Defines.mqh文件中为特定的事件设置宏和声明。

我创建了ON_OCO自定义事件来创建OCO订单对。

#define ON_OCO (101) // OCO 订单对创建事件 


订单的参数的设置及订单对的创建将在OnChartEvent()函数体内完成。 

//+------------------------------------------------------------------+
//| ChartEvent函数                                                    |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- 在主对话框处理所有图表事件
   myDialog.ChartEvent(id,lparam,dparam,sparam);

//--- 下拉列表处理
   if(id==CHARTEVENT_CUSTOM+ON_CHANGE)
     {
      //--- 如果是面板列表
      if(!StringCompare(StringSubstr(sparam,0,7),"myCombo"))
        {
         static ENUM_PENDING_ORDER_TYPE prev_vals[2];
         //--- 列表索引
         int combo_idx=(int)StringToInteger(StringSubstr(sparam,7,1))-1;

         ENUM_PENDING_ORDER_TYPE curr_val=(ENUM_PENDING_ORDER_TYPE)(myCombos[combo_idx].Value()+2);
         //--- 记录订单类型的变更
         if(prev_vals[combo_idx]!=curr_val)
           {
            prev_vals[combo_idx]=curr_val;
            gOrdersProps[combo_idx].order_type=curr_val;
           }
        }
     }

//--- 处理输入框
   else if(id==CHARTEVENT_OBJECT_ENDEDIT)
     {
      //--- 如果是面板的输入框
      if(!StringCompare(StringSubstr(sparam,0,6),"myEdit"))
        {
         //--- 查找对象
         for(int idx=0;idx<ArraySize(myEdits);idx++)
           {
            string curr_edit_obj_name=myEdits[idx].Name();
            long curr_edit_obj_id=myEdits[idx].Id();
            //--- 如果名称重合
            if(!StringCompare(sparam,curr_edit_obj_name))
              {
               //--- 获取当前值
               double value=StringToDouble(myEdits[idx].Text());
               //--- 定义gOrdersProps[]数组索引
               int order_num=(idx<gEditsHalfLen)?0:1;
               //--- 定义gOrdersProps结构体编号
               int jdx=idx;
               if(order_num)
                  jdx=idx-gEditsHalfLen;
               //--- 填充gOrdersProps结构体
               switch(jdx)
                 {
                  case 0: // 交易量
                    {
                     gOrdersProps[order_num].volume=value;
                     break;
                    }
                  case 1: // 执行
                    {
                     gOrdersProps[order_num].price_offset=(uint)value;
                     break;
                    }
                  case 2: // limit
                    {
                     gOrdersProps[order_num].limit_offset=(uint)value;
                     break;
                    }
                  case 3: // stop
                    {
                     gOrdersProps[order_num].sl=(uint)value;
                     break;
                    }
                  case 4: // 获利
                    {
                     gOrdersProps[order_num].tp=(uint)value;
                     break;
                    }
                 }
              }
           }
         //--- OCO 订单对创建标识
         bool is_to_fire_oco=true;
         //--- 检查结构体 
         for(int idx=0;idx<ArraySize(gOrdersProps);idx++)
           {
            //---  如果订单类型已设置 
            if(gOrdersProps[idx].order_type!=WRONG_VALUE)
               //---  如果交易量已设置  
               if(gOrdersProps[idx].volume!=WRONG_VALUE)
                  //---  如果市价单的入场点差已设置
                  if(gOrdersProps[idx].price_offset!=(uint)WRONG_VALUE)
                     //---  如果limit单的入场点差已设置
                     if(gOrdersProps[idx].limit_offset!=(uint)WRONG_VALUE)
                        //---  如果止损已设置
                        if(gOrdersProps[idx].sl!=(uint)WRONG_VALUE)
                           //---  如果止赢已设置
                           if(gOrdersProps[idx].tp!=(uint)WRONG_VALUE)
                              continue;

            //--- 清除OCO订单对的创建标识 
            is_to_fire_oco=false;
            break;
           }
         //--- 创建OCO订单对?
         if(is_to_fire_oco)
           {
            //--- 填充备注字段
            for(int ord_idx=0;ord_idx<ArraySize(gOrdersProps);ord_idx++)
               gOrdersProps[ord_idx].comment=StringFormat("OCO Order %d",ord_idx+1);
            //--- 改变按钮属性
            myButton.Text("New pair");
            myButton.Color(clrDarkBlue);
            myButton.ColorBackground(clrLightBlue);
            //--- 响应用户操作 
            myButton.Enable();
           }
        }
     }
//--- 点击按钮
   else if(id==CHARTEVENT_OBJECT_CLICK)
     {
      //--- 如果是OCO订单对创建按钮
      if(!StringCompare(StringSubstr(sparam,0,6),"myFire"))
         //--- 响应用户操作
         if(myButton.IsEnabled())
           {
            //--- 触发OCO订单对创建事件
            EventChartCustom(0,ON_OCO,0,0.0,"OCO_fire");
            Print("Command to create new bunch has been received.");
           }
     }
//--- 处理新订单对初始化命令 
   else if(id==CHARTEVENT_CUSTOM+ON_OCO)
     {
      //--- OCO订单对初始化
      if(gOco.Init(gOrdersProps,gOcoList.Total()+1))
        {
         PrintFormat("Id of new OCO pair: %I32u",gOco.Id());
         //--- 复制
         CiOcoObject *ptr_new_oco=new CiOcoObject(gOco);
         if(CheckPointer(ptr_new_oco)==POINTER_DYNAMIC)
           {
            //--- 添加到列表
            int node_idx=gOcoList.Add(ptr_new_oco);
            if(node_idx>-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!");

      //--- 清除相关属性
      Reset();
     }
  }

处理器的代码并不少。我想要重点介绍几个模块。

首先在主对话框中处理所有图表事件。

其次是处理各种各样其他事件的模块:

EA没有效验面板中各项字段的正确性。这也是为什么我们要自己检查这些值,否则EA将出现OCO订单下单失败的报错。

删除订单对以及平仓剩余订单的操作在OnTrade()处理函数中实现。


总结

我尝试展现标准类库的丰富性,用它可以实现我们的一些特定需求。

尤其是我们用它解决了处理OCO订单的问题。我希望这个带控制面板的用于处理OCO订单的EA,将是你创建更多复杂订单对的起点。