![Рецепты MQL5 - ОСО-ордера](https://c.mql5.com/i/articles/overlay.png)
Рецепты MQL5 - ОСО-ордера
Введение
В статье речь пойдет о работе с таким типом связки ордеров как OCO. Данный механизм реализован в некоторых конкурирующих с MetaTrader 5 торговых терминалах. На примере создания советника, имеющего панель для обработки ОСО-ордеров, преследую 2 цели. С одной стороны есть желание осветить возможности Стандартной библиотеки, с другой - расширить инструментарий трейдера.
1. Сущность ОСО-ордеров
ОСО-ордера (one-cancels-the-other order) – это связка пары отложенных ордеров.
Между собой они связаны функцией взаимоотмены: если сработал первый, то не должен сработать второй, и наоборот.
Рис. 1. Связка ОСО-ордеров
На рис.1 приведена простая схема взаимозависимости ордеров. Она отражает сущностное определение: связка живет до тех пор, пока живут оба ордера. С точки зрения логики любой [один] ордер из пары есть необходимое, но недостаточное условие для существования связки.
В некоторых источниках замечается, что в связке обязательно один ордер лимитный, а другой – стоповый, причем ордера должны иметь одно направление (покупка или продажа). На мой взгляд, такое ограничение не способствует созданию гибких торговых стратегий. Предлагаю рассматривать в связке различные ОСО-ордера, а главное, пробовать эту связку запрограммировать.
2. Программирование связки ордеров
Для программирования задач, связанных с контролем над ОСО-ордерами, на мой взгляд, очень подходит ООП-инструментарий.
В следующих разделах рассмотрим новые виды типов данных, которые послужат нашим целям. На первом месте стоит класс CiOcoObject.
2.1. Класс CiOcoObject
Итак, нужно придумать какой-то программный объект, который отвечал бы за управление и контроль над двумя связанными ордерами.
По традиции создадим новый объект на основе родного для MQL5 базового класса CObject.
Тогда новый класс может выглядеть примерно так:
//+------------------------------------------------------------------+ //| Class CiOcoObject | //| Purpose: a class for OCO-orders | //+------------------------------------------------------------------+ class CiOcoObject : public CObject { //--- === Data members === --- private: //--- тикеты связки ulong m_order_tickets[2]; //--- флаг инициализации bool m_is_init; //--- id uint m_id; //--- === Methods === --- 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;}; };
Каждая связка ОСО-ордеров будет иметь свой идентификатор. Его значение создается посредством обращения к генератору случайных чисел (объекту класса CRandom).
С точки зрения интерфейса интерес представляют методы инициализации и деинициализации связки. Первый создает (инициализирует) связку, второй – удаляет (деинициализирует).
В качестве аргумента метод CiOcoObject::Init() принимает массив структур типа SOrderProperties. Этот вид структуры представляет собой свойства ордера, входящего в связку, т.е. ОСО-ордера.
2.2 Структура SOrderProperties
Рассмотрим состав полей вышеобозначенной структуры.
//+------------------------------------------------------------------+ //| Структура свойств ордера | //+------------------------------------------------------------------+ struct SOrderProperties { string symbol; // символ ENUM_PENDING_ORDER_TYPE order_type; // тип ордера double volume; // объем ордера uint price_offset; // отступ для цены исполнения,пп uint limit_offset; // отступ для цены лимита,пп uint sl; // stop loss,пп uint tp; // take profit,пп 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. Поле "Тип" с выпадающим списком возможных типов ордера
Если же использовать стандартное перечисление ENUM _ORDER_TYPE, то можно было бы задать тип рыночного ордера ORDER_TYPE_BUY или ORDER_TYPE_SELL, что совсем не нужно, т.к. работа идет с отложенными ордерами.
2.3. Инициализация связки
Как уже ранее отмечалось, инициализацией связки ордеров занимается метод CiOcoObject::Init().
По сути, он выставляет саму связку ордеров и фиксирует успех или неудачу появления новой связки. Я бы сказал, что это активный метод, т.к. он сам совершает торговые операции. При желании можно создать и пассивный метод. Он будет просто связывать между собой уже активные отложенные ордера, которые были выставлены независимо друг от друга.
Не буду приводить код всего метода. Отмечу, что здесь важно правильно рассчитать все цены (открытие, стоп, профит, лимит), чтобы метод торгового класса CTrade::OrderOpen() выполнил торговый приказ. Для этого нужно учитывать 2 момента: направление ордера (покупка или продажа) и расположение цены исполнения ордера относительно текущей (выше или ниже).
Этот метод вызывает пару приватных методов: BaseOrderType() и PendingType(). Первая определяет направление ордера, а вторая – тип отложенного ордера.
Если ордер размещен, то в массив m_order_tickets[] заносится его тикет.
Для тестирования данного метода я использовал простой скрипт Init_OCO.mq5.
#property script_show_inputs //--- #include "CiOcoObject.mqh" //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ sinput string Info_order1="+===--Ордер 1--====+"; // +===--Ордер 1--====+ input ENUM_PENDING_ORDER_TYPE InpOrder1Type=PENDING_ORDER_TYPE_SELL_LIMIT; // Тип input double InpOrder1Volume=0.02; // Объем input uint InpOrder1PriceOffset=125; // Отступ для цены исполнения, пп input uint InpOrder1LimitOffset=50; // Отступ для цены лимита, пп input uint InpOrder1SL=250; // Стоп-лосс, пп input uint InpOrder1TP=455; // Профит, пп input string InpOrder1Comment="OCO Order 1"; // Комментарий //--- sinput string Info_order2="+===--Ордер 2--====+"; // +===--Ордер 2--====+ input ENUM_PENDING_ORDER_TYPE InpOrder2Type=PENDING_ORDER_TYPE_SELL_STOP; // Тип input double InpOrder2Volume=0.02; // Объем input uint InpOrder2PriceOffset=125; // Отступ для цены исполнения, пп input uint InpOrder2LimitOffset=50; // Отступ для цены лимита, пп input uint InpOrder2SL=275; // Стоп-лосс, пп input uint InpOrder2TP=300; // Профит, пп input string InpOrder2Comment="OCO Order 2"; // Комментарий //--- globals CiOcoObject myOco; SOrderProperties gOrdersProps[2]; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- свойства 1-го ордера 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; //--- свойства 2-го ордера 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 новой ОСО-связки: %I32u",myOco.Id()); else Print("Ошибка выставления ОСО-связки!"); }
В нем можно задать различные свойства будущих ордеров связки. Всего в MetaTrader 5 имеется 6 различных типов отложенных ордеров.
Тогда может быть 15 вариантов (комбинаций) связок (при условии, что типы ордеров в связке разные).
C(k,N) = C(2,6) = 15
С помощью скрипта были протестированы все варианты. Приведу пример для связки "Buy-stop - Buy-stop-limit".
В параметрах скрипта нужно явно указать типы ордеров (рис.3).
Рис. 3. Связка ордера типа "buy-stop" с ордером типа "buy-stop-limit"
В журнале "Эксперты" появится такая информация:
QO 0 17:17:41.020 Init_OCO (GBPUSD.e,M15) Код результата выполнения запроса: 10009 JD 0 17:17:41.036 Init_OCO (GBPUSD.e,M15) Тикет нового ордера: 24190813 QL 0 17:17:41.286 Init_OCO (GBPUSD.e,M15) Код результата выполнения запроса: 10009 JH 0 17:17:41.286 Init_OCO (GBPUSD.e,M15) Тикет нового ордера: 24190814 MM 0 17:17:41.379 Init_OCO (GBPUSD.e,M15) Id новой ОСО-связки: 3782950319
Однако с помощью скрипта полноценно поработать с ОСО-ордерами не получится, если не прибегать к зацикливанию.
2.4. Деинициализация связки
Данный метод отвечает за контроль над связкой ордеров. Как только любой из ордеров перестанет находиться в списке активных, то связка "погибнет".
Полагаю, что коде советника стоит размещать данный метод в обработчиках OnTrade() или OnTradeTransaction(). Таким образом советник сможет оперативно обрабатывать активацию любого ордера связки.
//+------------------------------------------------------------------+ //| Деинициализация связки | //+------------------------------------------------------------------+ 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("Ордера #%d нет в списке активных ордеров.",curr_ord_ticket); //--- попытка удалить другой ордер if(order_obj.Select(other_ord_ticket)) { CTrade trade_obj; //--- if(trade_obj.OrderDelete(other_ord_ticket)) return true; } } } } //--- return false; }
Отмечу такой нюанс. В теле метода класса проверяется флаг инициализации связки. Если флаг будет сброшен, то попытка проверить ордера предпринята не будет. Такая методика не позволяет удалить 1 активный ордер, когда другой еще не был выставлен.
Расширим возможности скрипта, в котором выставлялась пара ордеров. Для этих целей создадим тестовый советник Control_OCO_EA.mq5.
По большому счету советник будет отличаться от скрипта только тем, что в его коде будет присутствовать блок обработки события Trade():
//+------------------------------------------------------------------+ //| Trade function | //+------------------------------------------------------------------+ void OnTrade() { //--- деинициализация ОСО-связки if(myOco.Deinit()) { Print("Связки ордеров больше нет!"); //--- сбросить связку CiOcoObject new_oco; myOco=new_oco; } }
На видео можно увидеть, как обе программы работают в терминале MetaTrader 5.
Однако в обеих тестовых программах есть свои недостатки.
Первая программа (скрипт) может только активным образом создать связку, но потом теряет контроль над ней.
Вторая программа (советник) пусть и контролирует связку, но не может после первичного создания повторно создавать другие связки. Чтобы программа (советник), работающая с ОСО-ордерами, получилась полноценной, в ее арсенал нужно добавить возможность выставлять сами ордера. Попробуем это сделать в следующем разделе.
3. Советник-контроллер
Чтобы выставлять и настраивать параметры ордеров связки, создадим на графике Панель "Управление ОСО-ордерами".
Она войдет в состав советника-контроллера (рис.4). Исходный код представлен в файле советника Panel_OCO_EA.mq5.
Рис. 4. Панель для создания ОСО-ордеров - исходное состояние
Чтобы выставить связку ОСО-ордеров, нужно выбрать тип для будущего ордера и заполнить поля.
Тогда единственная на панели кнопка изменит свою надпись (текстовое свойство, рис.5).
Рис. 5. Панель для создания ОСО-ордеров - новая связка
Объекты каких классов были задействованы при создании Панели?
Это классы Стандартной библиотеки:
- CAppDialog - главный диалог приложения;
- CPanel - прямоугольная метка;
- CLabel - текстовая метка;
- CComboBox - поле с выпадающим списком;
- CEdit - поле ввода;
- CButton - кнопка.
Естественно, что автоматом вызывались методы классов-родителей.
Теперь о коде. Нужно сказать, что та часть Стандартной библиотеки, что отведена под нужды создания панелей индикации и диалогов управления, довольно обширна.
Например, чтобы поймать событие закрытия выпадающего списка, придется заглянуть глубоко в стек вызовов (рис. 6).
Рис. 6. Стек вызовов
Для специфических событий разработчик задает свои макросы и нотацию в файле %MQL5\Include\Controls\Defines.mqh.
Для целей создания ОСО-связки я придумал пользовательское событие ON_OCO.
#define ON_OCO (101) // событие "создание ОСО-связки"
Вся работа по заполнению параметров будущих ордеров и генерирование связки проходит в теле обработчика OnChartEvent().
//+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ 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: // лимит { gOrdersProps[order_num].limit_offset=(uint)value; break; } case 3: // стоп { gOrdersProps[order_num].sl=(uint)value; break; } case 4: // профит { gOrdersProps[order_num].tp=(uint)value; break; } } } } //--- флаг создания ОСО-связки 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) //--- если задан отступ для цены лимита if(gOrdersProps[idx].limit_offset!=(uint)WRONG_VALUE) //--- если задан stop loss if(gOrdersProps[idx].sl!=(uint)WRONG_VALUE) //--- если задан take profit if(gOrdersProps[idx].tp!=(uint)WRONG_VALUE) continue; //--- сбросить флаг создания ОСО-связки is_to_fire_oco=false; break; } //--- создать ОСО-связку? 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("Новая связка"); myButton.Color(clrDarkBlue); myButton.ColorBackground(clrLightBlue); //--- отвечать на действия пользователя myButton.Enable(); } } } //--- обработка клика по кнопке else if(id==CHARTEVENT_OBJECT_CLICK) { //--- если это кнопка создания ОСО-связки if(!StringCompare(StringSubstr(sparam,0,6),"myFire")) //--- если отвечать на действия пользователя if(myButton.IsEnabled()) { //--- сгенерировать событие "создание ОСО-связки" EventChartCustom(0,ON_OCO,0,0.0,"OCO_fire"); Print("Поступил приказ на создание новой связки."); } } //--- обработка приказ на инициализацию новой связки else if(id==CHARTEVENT_CUSTOM+ON_OCO) { //--- инициализация ОСО-связки if(gOco.Init(gOrdersProps,gOcoList.Total()+1)) { PrintFormat("Id новой ОСО-связки: %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("Всего связок: %d",gOcoList.Total()); else PrintFormat("Ошибка добавления ОСО-связки %I32u в список!",gOco.Id()); } } else Print("Ошибка выставления ОСО-ордеров!"); //--- сбросить свойства Reset(); } }
Код обработчика не мал. Здесь я бы выделил несколько блоков.
Сначала главному диалогу отдается обработка всех события графика.
Затем идут блоки обработки различных событий:
- Изменение выпадающих списков - для определения типа ордеров;
- Редактирование полей ввода - для заполнения свойств ордеров;
- Клик по кнопке - для генерации события ON_OCO;
- Непосредственно реакция на событие ON_OCO - создание связки ордеров.
В советнике нет проверки правильности заполнения полей панели. Поэтому нужно самостоятельно проверять заполняемые значения, иначе советник выдаст ошибку выставления ОСО-ордеров.
В теле обработчика OnTrade() проверяется необходимость удаления связки и закрытия оставшегося ордера.
Заключение
В статье я попытался продемонстрировать богатство классов Стандартной библиотеки, которое может послужить для реализации специфических задач.
В частности, решалась проблема обработки ОСО-ордеров. Надеюсь, что код советника, имеющего Панель для обработки ОСО-ордеров, послужит отправной точкой для создания более сложных связок ордеров.
![Изучаем класс CCanvas. Реализация прозрачности графических объектов](https://c.mql5.com/2/17/CCanvas_class_Standard_library_MetaTrader5.png)
![Разнонаправленная торговля и хеджирование позиций в MetaTrader 5 с помощью API HedgeTerminal, часть 2](https://c.mql5.com/2/17/HedgeTerminalaArticle200x200_2p2.png)
![Рецепты MQL5 - Реализуем ассоциативный массив или словарь для быстрого доступа к данным](https://c.mql5.com/2/18/MQL5_Associative_Arrays__1.png)
![Разнонаправленная торговля и хеджирование позиций в MetaTrader 5 с помощью панели HedgeTerminal, часть 1](https://c.mql5.com/2/17/HedgeTerminalaArticle200x200_2.png)
![MQL5 - Язык торговых стратегий для клиентского терминала MetaTrader 5](https://c.mql5.com/i/registerlandings/logo-2.png)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Коллеги, кто-нибудь видел где-нибудь примеры использования библиотек которые лежат в MQL5\Include\Canvas\Charts, есть одна идея, как-раз можно использовать стандартную библиотеку, только без примеров, как-то туговато идёт освоение ))
Коллеги, кто-нибудь видел где-нибудь примеры использования библиотек которые лежат в MQL5\Include\Canvas\Charts, есть одна идея, как-раз можно использовать стандартную библиотеку, только без примеров, как-то туговато идёт освоение ))
Раздел Scripts\Examples\Canvas.