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 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
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
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
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
The following classes of the Standard Library were used to construct our Panel:
- CAppDialog is the main application dialog;
- CPanel is a rectangle label;
- CLabel is a text label;
- CComboBox is a field with a drop-down list;
- CEdit is an input field;
- CButton is a button.
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
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:
- Changing drop-down lists for defining an order type;
- Editing input fields for filling up properties of orders;
- Click on button for ON_OCO event generation;
- ON_OCO event response: order pair creation.
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.