MQL5 Cookbook: ОСО 주문
소개
이 글은 OCO와 같은 유형의 주문 쌍을 다루는 데 중점을 둡니다. 이 메커니즘은 MetaTrader 5와 경쟁하는 일부 거래 터미널에서 구현됩니다. 저는 OCO 주문 처리를 위한 패널이 있는 EA 생성의 예를 통해 두 가지 목표를 추구합니다. 한편으로는 표준 라이브러리의 기능을 설명하고 다른 한편으로는 트레이더의 도구 세트를 확장하고 싶습니다.
1. OCO 명령의 본질
OCO 주문(one-cancel-the-other order)은 두 개의 보류 중인 주문 쌍을 나타냅니다.
그들은 상호 취소 기능에 의해 연결됩니다. 첫 번째 트리거가 트리거되면 두 번째 트리거가 제거되어야 하며 그 반대의 경우도 마찬가지입니다.
그림 1 OCO 주문 쌍
그림 1은 간단한 차수 상호 의존 방식을 보여줍니다. 그것은 본질적인 정의를 반영합니다. 두 주문이 모두 존재하는 한 쌍은 존재합니다. 논리의 관점에서 쌍의 [일] 순서는 쌍이 존재하기 위한 필수 조건이지만 충분 조건은 아닙니다.
일부 소식통은 쌍에 하나의 지정가 주문과 하나의 중지 주문이 있어야 하며 주문에는 하나의 방향(구매 또는 판매)이 있어야 한다고 말합니다. 내 생각에 그러한 제한은 유연한 거래 전략을 만드는 데 도움이 되지 않습니다. 저는 다양한 OCO 주문이 쌍에서 분석되어야 하며 가장 중요한 것은 이 쌍을 프로그래밍하려고 시도할 것을 제안합니다.
2. 프로그래밍 순서 쌍
내 생각에 ООP 도구 세트는 OCO 주문에 대한 제어와 연결된 프로그래밍 작업에 가장 적합합니다.
다음 섹션에서는 우리의 목적에 부합하는 새로운 데이터 유형에 대해 설명합니다. CiOcoObject 클래스가 우선입니다.
2.1. CiOcoObject 클래스
따라서 우리는 두 개의 상호 연결된 주문을 제어하는 소프트웨어 개체를 만들어야 합니다.
전통적으로 CObject 추상 클래스를 기반으로 새 개체를 생성해 보겠습니다.
이 새 클래스는 다음과 같이 보일 수 있습니다.
//+------------------------------------------------------------------+ //| 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;}; };-->
각 OCO 주문 쌍에는 고유한 식별자가 있습니다. 그 값은 난수 생성기를 통해 설정됩니다(CRandom 클래스의 개체).
쌍 초기화 및 초기화 해제 방법은 인터페이스 컨텍스트에서 중요합니다. 첫 번째는 쌍을 생성(초기화)하고 두 번째는 쌍을 제거(초기화 해제)합니다.
CiOcoObject::Init() 메소드는 SOrderProperties 유형의 구조 배열을 인수로 받아들입니다. 이 유형의 구조는 쌍의 주문 속성, 즉 OCO 주문을 나타냅니다.
2.2 SOrderProperties 구조
앞서 언급한 구조의 필드를 살펴보자.
//+------------------------------------------------------------------+ //| 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 }-->
따라서 초기화 방법이 작동하도록 하려면 이전에 두 개의 요소로 구성된 구조 배열을 채워야 합니다. 간단히 말해서, 우리는 그것이 배치될 주문 프로그램을 설명해야 합니다.
ENUM_PENDING_ORDER_TYPE 유형의 열거가 구조에서 사용됩니다.
//+------------------------------------------------------------------+ //| 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() 거래 클래스 메소드가 거래 주문을 수행할 수 있도록 하는 것이 중요합니다. 이를 위해 주문 방향(구매 또는 판매)과 현재 가격에 대한 주문 실행 가격의 포지션(이상 또는 이하)의 두 가지를 고려해야 합니다.
이 메소드는 BaseOrderType() 및 PendingType()과 같은 몇 가지 비공개 메소드를 호출합니다. 첫 번째 항목은 주문 방향을 정의하고 두 번째 항목은 보류 중인 주문 유형을 결정합니다.
주문이 접수되면 티켓이 m_order_tickets[] 배열에 기록됩니다.
이 방법을 테스트하기 위해 간단한 Init_OCO.mq5 스크립트를 사용했습니다.
#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!"); }-->
여기에서 쌍의 미래 주문에 대한 다양한 속성을 설정할 수 있습니다. MetaTrader 5에는 6가지 유형의 보류 주문이 있습니다.
이 컨텍스트에서 쌍의 변형(조합)이 있을 수 있습니다(쌍에 다른 순서가 있는 경우).
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) 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는 지연 없이 모든 페어 주문의 활성화를 처리할 수 있습니다.
//+------------------------------------------------------------------+ //| 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; }-->
한 가지 세부 사항을 언급하고 싶습니다. 페어 초기화 플래그는 클래스 메소드의 본문에서 확인됩니다. 플래그가 지워진 경우 주문 확인 시도가 이루어지지 않습니다. 이 접근 방식은 다른 하나가 아직 배치되지 않은 경우 하나의 활성 주문을 삭제하는 것을 방지합니다.
몇 가지 주문이 이루어진 스크립트에 기능을 추가해 보겠습니다. 이를 위해 Control_OCO_EA.mq5 테스트 EA를 생성합니다.
일반적으로 EA는 코드의 Trade() 이벤트 처리 블록만 스크립트와 다릅니다.
//+------------------------------------------------------------------+ //| Trade function | //+------------------------------------------------------------------+ void OnTrade() { //--- OCO pair deinitialization if(myOco.Deinit()) { Print("No more order pair!"); //--- clear pair CiOcoObject new_oco; myOco=new_oco; } }-->
비디오는 MetaTrader 5 터미널에서 두 프로그램의 작업을 보여줍니다.
그러나 두 테스트 프로그램 모두 약점이 있습니다.
첫 번째 프로그램(스크립트)은 쌍을 능동적으로 생성할 수만 있지만 그런 다음에는 제어를 잃습니다.
두 번째 프로그램(Expert Advisor)은 쌍을 제어하지만 첫 번째 쌍을 생성한 후에는 다른 쌍을 반복적으로 생성할 수 없습니다. OCO 주문 프로그램(스크립트)을 완전한 기능으로 만들려면 주문 기회가 있는 도구 집합을 확장해야 합니다. 다음 섹션에서 그렇게 할 것입니다.
3. EA 제어
페어 주문의 매개변수를 배치하고 설정하기 위해 차트에 OCO 주문 관리 패널을 생성해 보겠습니다.
제어 EA의 일부가 됩니다(그림 4). 소스 코드는 Panel_OCO_EA.mq5에 있습니다.
그림 4. OCO 주문 생성 패널: 초기 상태
미래 주문 유형을 선택하고 필드를 작성하여 한 쌍의 OCO 주문을 해야 합니다.
그러면 패널에 있는 유일한 버튼의 레이블이 변경됩니다(텍스트 속성, 그림 5).
그림 5. OCO 주문 생성 패널: 새 쌍
표준 라이브러리의 다음 클래스는 패널을 구성하는 데 사용되었습니다.
- CAppDialog는 기본 응용 프로그램 대화 상자입니다.
- CPanel은 직사각형 레이블입니다.
- CLabel은 텍스트 레이블입니다.
- CComboBox는 드롭다운 목록이 있는 필드입니다.
- CEdit는 입력 필드입니다.
- CButton은 버튼입니다.
물론 부모 클래스 메소드는 자동으로 호출됩니다.
이제 코드로 넘어갑니다. 표준 라이브러리에서 표시 패널 및 대화 상자 생성 전용으로 사용되는 부분이 상당히 큽니다.
예를 들어, 드롭다운 목록 닫기 이벤트를 포착하려면 호출 스택을 자세히 조사해야 합니다(그림 6).
그림 6. 호출 스택
개발자는 특정 이벤트에 대해 %MQL5\Include\Controls\Defines.mqh 파일에 매크로와 표기법을 설정합니다.
OCO 쌍을 만들기 위해 ON_OCO 맞춤 이벤트를 만들었습니다.
#define ON_OCO (101) // OCO pair creation event-->
미래 주문의 매개변수가 채워지고 OnChartEvent() 핸들러 본문에서 쌍이 생성됩니다.
//+------------------------------------------------------------------+ //| 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(); } }-->
핸들러 코드는 작지 않습니다. 여러 블록에 중점을 두고 싶습니다.
모든 차트 이벤트의 첫 번째 처리는 기본 대화 상자에 제공됩니다.
다음은 다양한 이벤트 처리 블록입니다.
- 주문 유형을 정의하기 위한 드롭다운 목록 변경;
- 주문 속성을 채우기 위한 입력 필드 편집;
- ON_OCO 이벤트 생성 버튼을 클릭하십시오.
- ON_OCO 이벤트 응답: 주문 쌍 생성.
EA는 패널 필드 작성의 정확성을 확인하지 않습니다. 이것이 우리가 스스로 값을 확인해야 하는 이유입니다. 그렇지 않으면 EA에 OCO 주문 오류가 표시됩니다.
OnTrade() 핸들러 본문에서 쌍을 제거하고 나머지 주문을 닫을 필요가 있는지 확인합니다.
결론
일부 특정 작업을 수행하는 데 사용할 수 있는 표준 라이브러리 클래스의 풍부함을 보여주려고 했습니다.
특히, 우리는 OCO 주문 처리 문제를 다루고 있었습니다. OCO 주문 처리를 위한 패널이 있는 EA의 코드가 더 복잡한 주문 쌍 생성을 위한 출발점이 되기를 바랍니다.
MetaQuotes 소프트웨어 사를 통해 러시아어가 번역됨.
원본 기고글: https://www.mql5.com/ru/articles/1582