MQL5 Cookbook: ОСО Orders

Denis Kirichenko | 12 February, 2015

Introduction

This article focuses on dealing with such type of order pair as OCO. This mechanism is implemented in some trading terminals competing with MetaTrader 5. I pursue two aims through the example of creation of an EA with a panel for OCO orders processing. On the one hand, I wish to describe features of the Standard Library, on the other hand I would like to extend a trader's tool set.


1. Essence of OCO Orders

OCO orders (one-cancels-the-other order) represent a pair of two pending orders.

They are connected by mutual cancellation function: if the first one triggers, the second one should be removed, and vice versa.

Fig. 1 Pair of OCO orders

Fig. 1 Pair of OCO orders

Fig.1 shows a simple order interdependence scheme. It reflects an essential definition: a pair exists for so long as both orders exist. In terms of logic any [one] order of the pair is an essential but not sufficient condition for the pair existence.

Some sources say that the pair must have one limit order and one stop order, moreover orders must have one direction (either buy or sell). To my mind such restriction cannot aid in creation of flexible trading strategies. I suggest that various OCO orders shall be analyzed in the pair, and most importantly we shall try to program this pair.


2. Programming Order Pair

In my thinking, ООP toolset is suited for programming tasks connected with control over OCO orders in the best possible way.

Following sections are devoted to new data types which will serve our purpose. CiOcoObject class comes first.


2.1. CiOcoObject Class

So, we need to come up with some software object responsible for control over two interconnected orders.

Traditionally, let's create a new object on the basis of CObject abstract class.

This new class can look as follows:

//+------------------------------------------------------------------+
//| Class CiOcoObject                                                |
//| Purpose: a class for OCO orders                                  |            
//+------------------------------------------------------------------+
class CiOcoObject : public CObject
  {
   //--- === Data members === --- 
private:
   //--- tickets of pair
   ulong             m_order_tickets[2];
   //--- initialization flag
   bool              m_is_init;
   //--- id
   uint              m_id;

   //--- === Methods === --- 
public:
   //--- constructor/destructor
   void              CiOcoObject(void){m_is_init=false;};
   void             ~CiOcoObject(void){};
   //--- copy constructor
   void              CiOcoObject(const CiOcoObject &_src_oco);
   //--- assignment operator
   void              operator=(const CiOcoObject &_src_oco);

   //--- initialization/deinitialization
   bool              Init(const SOrderProperties &_orders[],const uint _bunch_cnt=1);
   bool              Deinit(void);
   //--- get id
   uint              Id(void) const {return m_id;};

private:
   //--- types of orders
   ENUM_ORDER_TYPE   BaseOrderType(const ENUM_ORDER_TYPE _ord_type);
   ENUM_BASE_PENDING_TYPE PendingType(const ENUM_PENDING_ORDER_TYPE _pend_type);
   //--- set id
   void              Id(const uint _id){m_id=_id;};
  };

Each pair of OCO orders will have its own identifier. Its value is set by means of the generator of random numbers (object of CRandom class).

Methods of pair initialization and deinitialization are of concern in the context of interface. The first one creates (initializes) the pair, and the second one removes (deinitializes) it.

CiOcoObject::Init() method accepts array of structures of SOrderProperties type as argument. This type of structure represents properties of the order in the pair, i.e. OCO order.


2.2 SOrderProperties Structure

Let us consider fields of the aforementioned structure.

//+------------------------------------------------------------------+
//| Order properties structure                                       |
//+------------------------------------------------------------------+
struct SOrderProperties
  {
   double                  volume;           // order volume   
   string                  symbol;           // symbol
   ENUM_PENDING_ORDER_TYPE order_type;       // order type   
   uint                    price_offset;     // offset for execution price, points
   uint                    limit_offset;     // offset for limit price, points
   uint                    sl;               // stop loss, points
   uint                    tp;               // take profit, points
   ENUM_ORDER_TYPE_TIME    type_time;        // expiration type
   datetime                expiration;       // expiration
   string                  comment;          // comment
  }

So, to make the initialization method work we should previously fill the structures array consisting of two elements. In simple words, we need to explain the program which orders it will be placing.

Enumeration of ENUM_PENDING_ORDER_TYPE type is used in the structure:

//+------------------------------------------------------------------+
//| 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
  };

Generally speaking, it looks the same as the ENUM _ORDER_TYPE standard enumeration, but it allows selecting only pending orders, or, more truly, types of such orders.

It protects from errors when selecting the corresponding order type in the Input parameters (Fig.2).

Fig. 2. The "Type" field with a drop-down list of available order types

Fig. 2. The "Type" field with a drop-down list of available order types

If however we use ENUM _ORDER_TYPE standard enumeration, then we could set a type of a market order (ORDER_TYPE_BUY or ORDER_TYPE_SELL), which is not required as we are dealing with pending orders only.


2.3. Initialization of Pair

As noted above, CiOcoObject::Init() method is engaged in order pair initialization.

In fact it places the order pair itself and records success or failure of new pair emergence. I should say that this is an active method, as it performs trading operations by itself. We can also create passive method as well. It will simply connect into a pair already active pending orders which have been placed independently.

I will not provide a code of the entire method. But I would like to note that it is important to calculate all prices (opening, stop, profit, limit), so the CTrade::OrderOpen() trade class method can perform a trade order. For that end we should consider two things: order direction (buy or sell) and position of an order execution price relative to a current price (above or under).

This method calls a couple of private methods: BaseOrderType() and PendingType(). The first one defines order direction, the second one determines pending order type.

If the order is placed, its ticket is recorded in the m_order_tickets[] array.

I used a simple Init_OCO.mq5 script to test this method.

#property script_show_inputs
//---
#include "CiOcoObject.mqh"
//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
sinput string Info_order1="+===--Order 1--====+";   // +===--Order 1--====+
input ENUM_PENDING_ORDER_TYPE InpOrder1Type=PENDING_ORDER_TYPE_SELL_LIMIT; // Type
input double InpOrder1Volume=0.02;                  // Volume
input uint InpOrder1PriceOffset=125;                // Offset for execution price, points
input uint InpOrder1LimitOffset=50;                 // Offset for limit price, points
input uint InpOrder1SL=250;                         // Stop loss, points
input uint InpOrder1TP=455;                         // Profit, points
input string InpOrder1Comment="OCO Order 1";        // Comment
//---
sinput string Info_order2="+===--Order 2--====+";   // +===--Order 2--====+
input ENUM_PENDING_ORDER_TYPE InpOrder2Type=PENDING_ORDER_TYPE_SELL_STOP; // Type
input double InpOrder2Volume=0.04;                  // Volume    
input uint InpOrder2PriceOffset=125;                // Offset for execution price, points
input uint InpOrder2LimitOffset=50;                 // Offset for limit price, points
input uint InpOrder2SL=275;                         // Stop loss, points
input uint InpOrder2TP=300;                         // Profit, points
input string InpOrder2Comment="OCO Order 2";        // Comment

//--- globals
CiOcoObject myOco;
SOrderProperties gOrdersProps[2];
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- property of the 1st order
   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;

//--- property of the 2nd order
   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;

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

Here you can set various properties of future orders of the pair. MetaTrader 5 has six different types of pending orders.

With this context, there may be 15 variants (combinations) of pairs (provided that there are different orders in the pair).

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

All variants have been tested with the aid of the script. I'll give an example for Buy Stop - Buy Stop Limit pair.

Types of orders should be specified in script parameters (Fig.3).


Fig. 3. Pair of "Buy Stop" order with "Buy Stop Limit" order

Fig. 3. Pair of "Buy Stop" order with "Buy Stop Limit" order

The following information will appear in the register "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

But we cannot work with OCO orders to the utmost with the aid of the script without resorting to looping.


2.4. Deinitialization of Pair

This method is responsible for control over the order pair. The pair will "die" when any order leaves the list of active orders.

I suppose that this method should be placed in OnTrade() or OnTradeTransaction() handlers of the EA's code. In such a manner, the EA will be able to process activation of any pair order without delay.

//+------------------------------------------------------------------+
//| Deinitialization of pair                                         |
//+------------------------------------------------------------------+
bool CiOcoObject::Deinit(void)
  {
//--- if pair is initialized
   if(this.m_is_init)
     {
      //--- check your orders 
      for(int ord_idx=0;ord_idx<ArraySize(this.m_order_tickets);ord_idx++)
        {
         //--- current pair order
         ulong curr_ord_ticket=this.m_order_tickets[ord_idx];
         //--- another pair order
         int other_ord_idx=!ord_idx;
         ulong other_ord_ticket=this.m_order_tickets[other_ord_idx];

         //---
         COrderInfo order_obj;

         //--- if there is no current order
         if(!order_obj.Select(curr_ord_ticket))
           {
            PrintFormat("Order #%d is not found in active orders list.",curr_ord_ticket);
            //--- attempt to delete another order                 
            if(order_obj.Select(other_ord_ticket))
              {
               CTrade trade_obj;
               //---
               if(trade_obj.OrderDelete(other_ord_ticket))
                  return true;
              }
           }
        }
     }
//---
   return false;
  }

I'd like to mention one detail. Pair initialization flag is checked in the body of the class method. Attempt to check orders will not be made if flag is cleared. This approach prevents deleting one active order when another one has not been placed yet.

Let's add functionality to the script where a couple of orders have been placed. For this purpose, we will create Control_OCO_EA.mq5 test EA.

Generally speaking the EA will differ from the script only by the Trade() event handling block in its code:

//+------------------------------------------------------------------+
//| Trade function                                                   |
//+------------------------------------------------------------------+
void OnTrade()
  {
//--- OCO pair deinitialization
   if(myOco.Deinit())
     {
      Print("No more order pair!");
      //--- clear  pair
      CiOcoObject new_oco;
      myOco=new_oco;
     }
  }

The video shows work of both programs in MetaTrader 5 terminal.



However both test programs have weaknesses.

The first program (script) can only actively create the pair but then it looses control over it.

The second program (Expert Advisor) though controls the pair, but it can't repeatedly create other pairs after creation of the first one. To make OCO order program (script) full-featured, we need to expand its toolset with the opportunity to place orders. We will do that in the next section.


3. Controlling EA

Let us create OCO Order Management Panel on the chart for placing and setting parameters of pair orders.

It will be a part of the controlling EA (Fig.4). The source code is located in Panel_OCO_EA.mq5.

Fig. 4. Panel for creation OCO orders: initial state

Fig. 4. Panel for creation OCO orders: initial state


We should select a type of a future order and fill out the fields to place the pair of OCO orders.

Then the label on the only button on the panel will be changed (text property, Fig.5).

Fig. 5. Panel for creation OCO orders: new pair

Fig. 5. Panel for creation OCO orders: new pair


The following classes of the Standard Library were used to construct our Panel:

Of course, parent class methods were called automatically.

Now we get down to the code. It has to be said that the part of the Standard Library which has been dedicated for creation of indication panels and dialogs is quite large.

For instance, if you want to catch a drop-down list closing event, you will have to delve deep into the stack of calls (Fig. 6).

Fig. 6. Stack of Calls

Fig. 6. Stack of Calls


A developer sets macros and a notation in %MQL5\Include\Controls\Defines.mqh file for specific events.

I have created ON_OCO custom event to create the OCO pair.

#define ON_OCO (101) // OCO pair creation event 

Parameters of future orders are filled and the pair is generated in OnChartEvent() handler body. 

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- handling all chart events by main dialog
   myDialog.ChartEvent(id,lparam,dparam,sparam);

//--- drop-down list handling
   if(id==CHARTEVENT_CUSTOM+ON_CHANGE)
     {
      //--- if it is Panel list
      if(!StringCompare(StringSubstr(sparam,0,7),"myCombo"))
        {
         static ENUM_PENDING_ORDER_TYPE prev_vals[2];
         //--- list index
         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);
         //--- remember order type change
         if(prev_vals[combo_idx]!=curr_val)
           {
            prev_vals[combo_idx]=curr_val;
            gOrdersProps[combo_idx].order_type=curr_val;
           }
        }
     }

//--- handling input fields
   else if(id==CHARTEVENT_OBJECT_ENDEDIT)
     {
      //--- if it is Panel's input field
      if(!StringCompare(StringSubstr(sparam,0,6),"myEdit"))
        {
         //--- find object
         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 names coincide
            if(!StringCompare(sparam,curr_edit_obj_name))
              {
               //--- get current value of field
               double value=StringToDouble(myEdits[idx].Text());
               //--- define gOrdersProps[] array index
               int order_num=(idx<gEditsHalfLen)?0:1;
               //--- define gOrdersProps structure field number
               int jdx=idx;
               if(order_num)
                  jdx=idx-gEditsHalfLen;
               //--- fill up gOrdersProps structure field
               switch(jdx)
                 {
                  case 0: // volume
                    {
                     gOrdersProps[order_num].volume=value;
                     break;
                    }
                  case 1: // execution
                    {
                     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: // profit
                    {
                     gOrdersProps[order_num].tp=(uint)value;
                     break;
                    }
                 }
              }
           }
         //--- OCO pair creation flag
         bool is_to_fire_oco=true;
         //--- check structure filling 
         for(int idx=0;idx<ArraySize(gOrdersProps);idx++)
           {
            //---  if order type is set 
            if(gOrdersProps[idx].order_type!=WRONG_VALUE)
               //---  if volume is set  
               if(gOrdersProps[idx].volume!=WRONG_VALUE)
                  //---  if offset for execution price is set
                  if(gOrdersProps[idx].price_offset!=(uint)WRONG_VALUE)
                     //---  if offset for limit price is set
                     if(gOrdersProps[idx].limit_offset!=(uint)WRONG_VALUE)
                        //---  if stop loss is set
                        if(gOrdersProps[idx].sl!=(uint)WRONG_VALUE)
                           //---  if take profit is set
                           if(gOrdersProps[idx].tp!=(uint)WRONG_VALUE)
                              continue;

            //--- clear OCO pair creation flag 
            is_to_fire_oco=false;
            break;
           }
         //--- create OCO pair?
         if(is_to_fire_oco)
           {
            //--- complete comment fields
            for(int ord_idx=0;ord_idx<ArraySize(gOrdersProps);ord_idx++)
               gOrdersProps[ord_idx].comment=StringFormat("OCO Order %d",ord_idx+1);
            //--- change button properties
            myButton.Text("New pair");
            myButton.Color(clrDarkBlue);
            myButton.ColorBackground(clrLightBlue);
            //--- respond to user actions 
            myButton.Enable();
           }
        }
     }
//--- handling click on button
   else if(id==CHARTEVENT_OBJECT_CLICK)
     {
      //--- if it is OCO pair creation button
      if(!StringCompare(StringSubstr(sparam,0,6),"myFire"))
         //--- if to respond to user actions
         if(myButton.IsEnabled())
           {
            //--- generate OCO pair creation event
            EventChartCustom(0,ON_OCO,0,0.0,"OCO_fire");
            Print("Command to create new bunch has been received.");
           }
     }
//--- handling new pair initialization command 
   else if(id==CHARTEVENT_CUSTOM+ON_OCO)
     {
      //--- OCO pair initialization
      if(gOco.Init(gOrdersProps,gOcoList.Total()+1))
        {
         PrintFormat("Id of new OCO pair: %I32u",gOco.Id());
         //--- copy pair
         CiOcoObject *ptr_new_oco=new CiOcoObject(gOco);
         if(CheckPointer(ptr_new_oco)==POINTER_DYNAMIC)
           {
            //--- add to list
            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!");

      //--- clear properties
      Reset();
     }
  }

Handler code isn't small. I would like to lay emphasis on several blocks.

First handling of all chart events is given to the main dialog.

Next are blocks of various events handling:

The EA does not verify the correctness of filling the panel's fields. That is why we have to check values by ourselves, otherwise the EA will show OCO orders placing error.

Necessity to remove the pair and close the remaining order is checked in OnTrade() handler body.


Conclusion

I tried to demonstrate the riches of the Standard Library classes which can be used for fulfillment of some specific tasks.

Particularly, we were dealing with a problem of OCO orders handling. I hope that the code of the EA with Panel for OCO orders handling will be a starting point for creation of more complicated order pairs.