Recetas MQL5 - órdenes ОСО
Introducción
En el artículo, se hablará del trabajo con el tipo de conexión de órdenes OCO. Este mecanismo ha sido implementado en ciertos terminales comerciales que compiten con MetaTrader 5. Partiendo del ejemplo de creación de un asesor que dispone de un panel para el procesamiento de órdenes ОСО, persigo dos objetivos. Por una parte, deseo arrojar luz sobre las posibilidades de la Biblioteca Estándar, por otra, ampliar el instrumental del trader.
1. Esencia de las órdenes ОСО
Las órdenes ОСО (one-cancels-the-other order) son la unión de una pareja de órdenes pendientes.
Están unidas entre sí por una función de cancelación mutua: si se activa la primera, entonces no se debe activar la segunda, y al contrario.
Fig. 1. Conexión de órdenes OCO
En la fig. 1 se muestra un esquema sencillo de la interdependencia entre órdenes. En él se muestra una definición esencial: la conexión existe mientras existan ambas órdenes. Desde el punto de la lógica de cualquier [una] orden de la pareja, existen condiciones imprencisdibles, pero insuficientes para la existencia de la unión.
En ciertas fuentes se hace notar que en la conexión, obligatoriamente una de las órdenes es de límite, en otras, de stop, además, las órdenes deberán tener una dirección (compra o venta). Mi punto de vista es que semejante limitación no posibilita la creación de estrategias comerciales flexibles. Propongo estudiar en la conexión diferentes órdenes OCO y, lo más importante, intentar programar esta conexión.
2. Programando las conexiones de órdenes
Para programar tareas relacionadas con el control sobre órdenes OCO, a mi entender, viene muy bien el instrumental de POO.
En los siguientes apartados veremos las nuevas clases de tipos de datos que nos servirán para nuestros objetivos. En primer lugar, tenemos la clase CiOcoObject.
2.1. Clase CiOcoObject
Y bien, hay que pensar algún objeto de programación que sea responsable de la gestión y el control de las dos órdenes conectadas.
Crearemos un nuevo objeto de manera tradicional, utilizando como pilar la clase básica familiar para MQL5 CObject, .
Entonces, la nueva clase puede tener el aspecto siguiente:
//+------------------------------------------------------------------+ //| Class CiOcoObject | //| Purpose: a class for OCO-orders | //+------------------------------------------------------------------+ class CiOcoObject : public CObject { //--- === Data members === --- private: //--- incidencias de la conexión ulong m_order_tickets[2]; //--- bandera de la inicialización bool m_is_init; //--- id uint m_id; //--- === Methods === --- public: //--- constructor/destructor void CiOcoObject(void){m_is_init=false;}; void ~CiOcoObject(void){}; //--- constructor de copia void CiOcoObject(const CiOcoObject &_src_oco); //--- operador de asignación void operator=(const CiOcoObject &_src_oco); //--- inicialización/desinicialización bool Init(const SOrderProperties &_orders[],const uint _bunch_cnt=1); bool Deinit(void); //--- obtener id uint Id(void) const {return m_id;}; private: //--- tipos de órdenes ENUM_ORDER_TYPE BaseOrderType(const ENUM_ORDER_TYPE _ord_type); ENUM_BASE_PENDING_TYPE PendingType(const ENUM_PENDING_ORDER_TYPE _pend_type); //--- establecer id void Id(const uint _id){m_id=_id;}; };
Cada conexión de las órdenes OCO tendrá su propio identificador. Su valor se crea mediante el llamamiento al generador de cifras aleatorias (al objeto de clase CRandom).
Desde el punto de vista del interfaz, resultan interesantes los métodos de inicialización y desinicialización de la conexión. El primero crea (inicializa) la conexión, y el segundo la elimina (desinicializa).
Como argumento, el método CiOcoObject::Init() adopta la matriz de estructuras del tipo SOrderProperties. Este tipo de estructuras supone en sí mismo las propiedades de la orden que entra en la conexión, es decir, la orden OCO.
2.2 Estructura SOrderProperties
Veamos los componentes de los campos de la estructura designada más arriba:
//+------------------------------------------------------------------+ //| Estructura de propiedades de la orden | //+------------------------------------------------------------------+ struct SOrderProperties { string symbol; // símbolo ENUM_PENDING_ORDER_TYPE order_type; // tipo de orden double volume; // volumen de la orden uint price_offset; // retroceso para el precio de ejecución, en pips uint limit_offset; // retroceso para el precio límite, en pips uint sl; // stop loss, en pips uint tp; // take profit, en pips ENUM_ORDER_TYPE_TIME type_time; // tipo según expiración datetime expiration; // expiración string comment; // comentarios };
Y bien, para que el método de inicialización funcione, hay que rellenar antes la matriz de las estructuras de los dos elementos. Dicho de fomra más sencilla, hay que indicar al programa qué órdenes deberá emitir.
En la estructura se usa una enumeración del tipo ENUM_PENDING_ORDER_TYPE:
//+------------------------------------------------------------------+ //| Tipo de orden pendiente | //+------------------------------------------------------------------+ 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 };
En general, lo que hace es doblar la enumeración estándar ENUM _ORDER_TYPE, pero con su ayuda se pueden elegir categóricamente solo órdenes pendientes, o para ser más exactos, solo tipos de estas órdenes.
Entonces, al iniciar el programa y elegir el tipo de orden correspondiente en los parámetros de entrada, no habrá errores (fig.2).
Fig. 2. Campo "Tipo" con la lista desplegable de los posibles tipos de orden
Si usamos la enumeración estándar ENUM _ORDER_TYPE, entonces se podría establecer el tipo de orden de mercado ORDER_TYPE_BUY o ORDER_TYPE_SELL, lo cual es totalmente innecesario, ya que el trabajo se realiza con órdenes pendientes.
2.3. Inicialización de la conexión
Como ya se destacó con anterioridad, de la inicialización de la conexión de órdenes se ocupa el método CiOcoObject::Init().
En esencia, se encarga de emitir la propia conexión de las órdenes y registra el éxito o el fracaso de la aparición de la nueva conexión. Yo diría que se trata de un método activo, ya que realiza operaciones comerciales por sí mismo. Si se desea, es posible crear un método pasivo. Sencillamente conectará entre sí órdenes pendientes ya activas, que fueron emitidas independientemente la una de la otra.
No voy a mostrar el código del método al completo. Destacaré que aquí es importante calcular correctamente todos los precios (apertura, stop, profit, límite), para que el método de la clase comercial CTrade::OrderOpen() ejecute la orden comercial. Para esto hay que tener en cuenta 2 momentos: la dirección de la orden (compra o venta) y la ubicación del precio de ejecución de la orden con respecto al actual (más alto o más bajo).
Este método llama un par de métodos privados: BaseOrderType() y PendingType(). El primero determina la dirección de la orden, el segundo, el tipo de orden pendiente.
Si la orden está ubicada, entonces en la matriz m_order_tickets[] se introduce su incidencia.
Para poner a prueba este método, he usado el sencillo script Init_OCO.mq5.
#property script_show_inputs //--- #include "CiOcoObject.mqh" //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ sinput string Info_order1="+===--Orden 1--====+"; // +===--Orden 1--====+ input ENUM_PENDING_ORDER_TYPE InpOrder1Type=PENDING_ORDER_TYPE_SELL_LIMIT; // Tipo input double InpOrder1Volume=0.02; // Volumen input uint InpOrder1PriceOffset=125; // Retraso para los precios de ejecución, en pips input uint InpOrder1LimitOffset=50; // Retraso para los precios de límite, en pips input uint InpOrder1SL=250; // Stop-loss, en pips input uint InpOrder1TP=455; // Profit, en pips input string InpOrder1Comment="Orden OCO 1"; // Comentarios //--- sinput string Info_order2="+===--Orden 2--====+"; // +===--Orden 2--====+ input ENUM_PENDING_ORDER_TYPE InpOrder2Type=PENDING_ORDER_TYPE_SELL_STOP; // Tipo input double InpOrder2Volume=0.02; // Volumen input uint InpOrder2PriceOffset=125; // Retraso para los precios de ejecución, en pips input uint InpOrder2LimitOffset=50; // Retraso para los precios de límite, en pips input uint InpOrder2SL=275; // Stop-loss, en pips input uint InpOrder2TP=300; // Profit, en pips input string InpOrder2Comment="Orden OCO 2"; // Comentarios //--- globals CiOcoObject myOco; SOrderProperties gOrdersProps[2]; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- propiedades de la 1 orden 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; //--- propiedades de la 2 orden 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; //--- inicialización de la conexión if(myOco.Init(gOrdersProps)) PrintFormat("Id de la nueva conexión OCO: %I32u",myOco.Id()); else Print("¡Error en la emisión de la conexión OCO!"); }
En este se pueden establecer diferentes propiedades de las futuras órdenes de la conexión. En total, en MetaTrader 5, existen 6 tipos diferentes de órdenes pendientes .
Entonces pueden darse 15 variantes (combinaciones) de conexiones (con la condición de que los tipos de orden en la conexión sean diferentes).
C(k,N) = C(2,6) = 15
Con ayuda del script fueron puestas a prueba todas las combinaciones. Pondré un ejemplo para la conexión "Buy-stop - Buy-stop-limit".
En los parámetros del script, hay que indicar claramente los tipos de orden (fig.3).
Fig. 3. Conexión de una orden del tipo "buy-stop" con una orden del tipo "buy-stop-limit"
En la revista "Expertos" aparecerá esta información:
QO 0 17:17:41.020 Init_OCO (GBPUSD.e,M15) Código del resultado de la ejecución del requerimiento: 10009 JD 0 17:17:41.036 Init_OCO (GBPUSD.e,M15) Incidencia de la nueva orden: 24190813 QL 0 17:17:41.286 Init_OCO (GBPUSD.e,M15) Código del resultado de la ejecución del requerimiento: 10009 JH 0 17:17:41.286 Init_OCO (GBPUSD.e,M15) Incidencia de la nueva orden: 24190814 MM 0 17:17:41.379 Init_OCO (GBPUSD.e,M15) Id de la nueva conexión OCO: 3782950319
Sin embargo, con ayuda del script, no podremos trabajar al completo con órdenes OCO, si no recurrimos a los ciclos.
2.4. Desinicialización de la conexión
Este método es responsable del control de la conexión de las órdenes. En cuanto cualquiera de las órdenes deje de encontrarse en la lista de activas, entonces la conexión "morirá".
Supongo que en el código del asesor es conveniente ubicar este método en los procesadores OnTrade() o OnTradeTransaction(). De esta forma, el asesor puede procesar de manera operativa la activación de cualquier orden de conexión.
//+------------------------------------------------------------------+ //| Desinicialización de la conexión | //+------------------------------------------------------------------+ bool CiOcoObject::Deinit(void) { //--- si la conexión está incializada if(this.m_is_init) { //--- comprobar las órdenes for(int ord_idx=0;ord_idx<ArraySize(this.m_order_tickets);ord_idx++) { //--- orden de conexión actual ulong curr_ord_ticket=this.m_order_tickets[ord_idx]; //--- otra orden de conexión int other_ord_idx=!ord_idx; ulong other_ord_ticket=this.m_order_tickets[other_ord_idx]; //--- COrderInfo order_obj; //--- si no existe orden actual if(!order_obj.Select(curr_ord_ticket)) { PrintFormat("La orden #%d no existe en la lista de órdenes activas.",curr_ord_ticket); //--- intento de eliminar otra orden if(order_obj.Select(other_ord_ticket)) { CTrade trade_obj; //--- if(trade_obj.OrderDelete(other_ord_ticket)) return true; } } } } //--- return false; }
Llamo la atención sobre el siguiente detalle. En el cuerpo del método de la clase, se comprueba la bandera de inicialización de la conexión. Si la bandera está quitada, entonces no se realizará el intento de comprobar la orden. Este método no permite eliminar 1 orden activa cuando otra no ha sido emitida aún.
Ampliamos las posibilidades del script en el que se emitió un par de órdenes. Con este objetivo, creamos el asesor de prueba Control_OCO_EA.mq5.
En general, el asesor se diferenciará del script solo en el hecho de que en su código estará presente un bloque de procesamiento del evento Trade():
//+------------------------------------------------------------------+ //| Trade function | //+------------------------------------------------------------------+ void OnTrade() { //--- desinicialización de la conexión OCO if(myOco.Deinit()) { Print("¡La conexión de órdenes ya no existe!"); //--- desechar la conexión CiOcoObject new_oco; myOco=new_oco; } }
En el vídeo se puede ver cómo ambos programas funcionan en el terminal MetaTrader 5.
Sin embargo, en ambos programas de prueba podemos encontrar defectos.
El primer programa (script) puede crear solo de una forma activa una conexión, pero luego pierde el control sobre ella.
El segundo programa (asesor), aunque controle la conexión, no puede crear otras conexiones después de una primera creación. Para que un programa (asesor), que trabaje con órdenes OCO, esté completo, a su arsenal debemos añadir la posibilidad de que emita las propias órdenes. Probemos a hacerlo en el siguiente apartado.
3. Asesor-controlador
Para emitir y ajustar los parámetros de las órdenes de la conexión, crearemos en el gráfico el Panel "Gestión de órdenes OCO".
Este entrará en los componentes del asesor-controlador (fig.4). El código fuente se adjunta en el archivo del asesor Panel_OCO_EA.mq5.
Fig. 4. Panel para la creación de órdenes OCO - estado inicial
Para emitir una conexión de órdenes OCO, hay que elegir el tipo para la futura orden y rellenar los campos.
Entonces, el único botón en el panel cambiará su inscripción (propiedad de texto, fig.5).
Fig. 5. Panel para la creación de órdenes OCO - nueva conexión
¿Los objetos de qué clases se han implicado para crear el Panel?
Se trata de las clases de la Biblioteca estándar:
- CAppDialog - diálogo principal de la aplicación;
- CPanel - etiqueta rectangular;
- CLabel - etiqueta de texto;
- CComboBox - campo con lista desplegable;
- CEdit - campo de edición;
- CButton - botón.
Naturalmente, se han llamado de forma automática los métodos de las clases-parentales.
Y ahora, sobre el código. Hay que decir que la parte de la Biblioteca estándar que está reconducida para las necesidades derivadas de la creación de paneles de indicaciones y diálogos de gestión, es bastante amplia.
Por ejemplo, para captar el evento de cierre de la lista desplegable, se deberá mirar a fondo en la pila de llamadas (fig. 6).
Fig. 6. Pila de llamadas
Para los eventos concretos, el procesador establece sus macros y la anotación en el archivo %MQL5\Include\Controls\Defines.mqh.
Con objeto de crear las conexiones OCO, he inventado el evento personalizado ON_OCO.
#define ON_OCO (101) // evento "creación de conexión ОСО"
Todo el trabajo de rellenado de los parámetros de las órdenes futuras y la generación de conexiones tiene lugar en el cuerpo del procesador OnChartEvent().
//+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- procesamiento de todos los eventos del gráfico con el diálogo principal myDialog.ChartEvent(id,lparam,dparam,sparam); //--- procesamiento de las listas desplegables if(id==CHARTEVENT_CUSTOM+ON_CHANGE) { //--- si se trata de la lista del Panel if(!StringCompare(StringSubstr(sparam,0,7),"myCombo")) { static ENUM_PENDING_ORDER_TYPE prev_vals[2]; //--- índice de la lista 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); //--- recordar el cambio del tipo de orden if(prev_vals[combo_idx]!=curr_val) { prev_vals[combo_idx]=curr_val; gOrdersProps[combo_idx].order_type=curr_val; } } } //--- procesamiento de los campos de edición else if(id==CHARTEVENT_OBJECT_ENDEDIT) { //--- si se trata del campo de edición del Panel if(!StringCompare(StringSubstr(sparam,0,6),"myEdit")) { //--- encontrar objeto for(int idx=0;idx<ArraySize(myEdits);idx++) { string curr_edit_obj_name=myEdits[idx].Name(); long curr_edit_obj_id=myEdits[idx].Id(); //--- si los nombres coinciden if(!StringCompare(sparam,curr_edit_obj_name)) { //--- obtener el valor actual del campo double value=StringToDouble(myEdits[idx].Text()); //--- determinar el índice de la matriz gOrdersProps[] int order_num=(idx<gEditsHalfLen)?0:1; //--- determinar el número del campo de la estructura gOrdersProps int jdx=idx; if(order_num) jdx=idx-gEditsHalfLen; //--- rellenar el campo de la estructura gOrdersProps switch(jdx) { case 0: // volumen { gOrdersProps[order_num].volume=value; break; } case 1: // ejecución { gOrdersProps[order_num].price_offset=(uint)value; break; } case 2: // límite { 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; } } } } //--- bandera de creación de una conexión OCO bool is_to_fire_oco=true; //--- comprobar el rellenado de las estructuras for(int idx=0;idx<ArraySize(gOrdersProps);idx++) { //--- si se ha establecido el tipo de orden if(gOrdersProps[idx].order_type!=WRONG_VALUE) //--- si se ha establecido el volumen if(gOrdersProps[idx].volume!=WRONG_VALUE) //--- si se ha establecido el precio de ejecución if(gOrdersProps[idx].price_offset!=(uint)WRONG_VALUE) //--- si se ha establecido el retraso para el precio límite if(gOrdersProps[idx].limit_offset!=(uint)WRONG_VALUE) //--- si se ha establecido el stop loss if(gOrdersProps[idx].sl!=(uint)WRONG_VALUE) //--- si se ha establecido el take profit if(gOrdersProps[idx].tp!=(uint)WRONG_VALUE) continue; //--- reiniciar bandera de creación de la conexión OCO is_to_fire_oco=false; break; } //--- ¿crear una conexión OCO? if(is_to_fire_oco) { //--- rellenar los campos de comentarios for(int ord_idx=0;ord_idx<ArraySize(gOrdersProps);ord_idx++) gOrdersProps[ord_idx].comment=StringFormat("OCO Order %d",ord_idx+1); //--- cambiar las propiedades del botón myButton.Text("Nueva conexión"); myButton.Color(clrDarkBlue); myButton.ColorBackground(clrLightBlue); //--- responder a las acciones del usuario myButton.Enable(); } } } //--- procesamiento del click en el botón else if(id==CHARTEVENT_OBJECT_CLICK) { //--- si se trata del botón de creación de una conexión OCO if(!StringCompare(StringSubstr(sparam,0,6),"myFire")) //--- si se responde a las acciones del usuario if(myButton.IsEnabled()) { //--- generar el evento "creación de una conexión OCO" EventChartCustom(0,ON_OCO,0,0.0,"OCO_fire"); Print("Ha llegado la orden de crear una nueva conexión."); } } //--- procesamiento de la orden de inicialización de la nueva conexión else if(id==CHARTEVENT_CUSTOM+ON_OCO) { //--- inicialización de la conexión OCO if(gOco.Init(gOrdersProps,gOcoList.Total()+1)) { PrintFormat("Id de la nueva conexión OCO: %I32u",gOco.Id()); //--- copiar la conexión CiOcoObject *ptr_new_oco=new CiOcoObject(gOco); if(CheckPointer(ptr_new_oco)==POINTER_DYNAMIC) { //--- añadir a la lista int node_idx=gOcoList.Add(ptr_new_oco); if(node_idx>-1) PrintFormat("Conexiones totales: %d",gOcoList.Total()); else PrintFormat("¡Error al añadir la conexión OCO %I32u a la lista!",gOco.Id()); } } else Print("¡Error al emitir las órdenes OCO!"); //--- restablecer propiedades Reset(); } }
El código del procesador no es pequeño. Aquí sería conveniente dividirlo en varios bloques.
Primero se da al diálogo principal el procesamiento de todos los eventos del gráfico.
A continuación, van los bloques de procesamiento de diferentes eventos:
- El cambio de las listas desplegables es para determinar el tipo de órdenes;
- La edición de los campos de edición es para rellenar las propiedades de las órdenes;
- El click en el botón es para la generación del evento ON_OCO;
- La reacción inmediata al evento ON_OCOes la creación de la conexión de las órdenes.
En el asesor no hay comprobación de la corrección del rellenado de los campos del panel. Por eso es necesario comprobar por nosotros mismos los valores rellenados, ya que de otra forma dará error en la emisión de las órdenes OCO.
En el cuerpo del procesador OnTrade() se comprueba la necesidad de eliminar las conexiones y cierres de la orden restante.
Conclusión
En el artículo he intentado demostrar la riqueza de las clases de la Biblioteca estándar, que puede servir para llevar a cabo tareas específicas.
En concreto, se ha resuelto el problema del procesamiento de las órdenes OCO. Espero que el código del asesor que tiene el Panel para el procesamiento de las órdenes OCO sirva de punto de partida para la creación de conexiones más complejas de órdenes.
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/1582
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso