Descargar MetaTrader 5

Procesando los eventos de transacciones en el Expert Advisor por medio de la función OnTrade()

16 diciembre 2013, 11:41
KlimMalgin
0
867

Introducción

Cualquier operador que escriba expertos en MQL, tendrá que enfrentarse, antes o después, a la necesidad de obtener información sobre el funcionamiento del experto. O puede que necesite implementar mensajería móvil (SMS) o notificaciones por e-mail sobre las acciones de los expertos. En cualquier caso, debemos "capturar" ciertos eventos del mercado o las acciones realizadas por un experto y comunicarlo a los usuarios.

En este artículo me gustaría contaros como podemos implementar el procesamiento de los eventos sobre transacciones y mostraros mi implementación.

En este artículo vamos a considerar el procesamiento de los siguientes eventos:

  • Posiciones
    1. Abrir
    2. Añadir
    3. Modificar (cambia Stop Loss y Take Profit)
    4. Revertir
    5. Cerrar toda la posición
    6. Cerrar parte de la posición
  • Órdenes pendientes
    1. Cursar
    2. Modificar

1. ¿Cómo funciona? 

Antes de empezar describiré, en términos generales, cómo ocurren los eventos de transacciones y explicaré todos los detalles sobre la marcha.

En MQL5 existen eventos predefinidos y personalizados. A nosotros nos interesan los eventos predefinidos, particularmente el evento Trade.

El evento Trade se genera cada vez que se completa una transacción. Cada vez que se genera el evento Trade se llama a la función OnTrade(). El procesamiento de las órdenes y posiciones se produce en el interior de la función OnTrade().

2. Plantilla Expert

Vamos a crear un nuevo asesor experto. En MetaEditor haga clic en en Archivo -> Nuevo para ejecutar el MQL5 Wizard. Seleccione el asesor experto y haga clic en Siguiente. En el cuadro de diálogo "Propiedades generales del asesor experto" introduzca el nombre del asesor experto y sus datos si es necesario. Yo he nombrado a mi asesor experto "TradeControl". Puede usar este nombre o el suyo propio, esto no es lo importante. No especificaremos ningún parámetro ya que estos se van a crear sobre la marcha cuando escribamos el experto.

¡Terminado! Ya hemos creado la plantilla del asesor experto, ahora tenemos que incorporarle la función OnTrade().

Obtendremos como resultado el siguiente código:

//+------------------------------------------------------------------+
//|                                              TradeControl_en.mq5 |
//|                                             Copyright KlimMalgin |
//|                                                                  |
//+------------------------------------------------------------------+
#property copyright "KlimMalgin"
#property link      ""
#property version   "1.00"
//+------------------------------------------------------------------+
//| Función de inicialización del Expert                             |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Función de deinicialización del Expert                           |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Función OnTrade                                                  |
//+------------------------------------------------------------------+
void OnTrade()
  {
//---

//---
  }
//+------------------------------------------------------------------+
//| Función Expert tick |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }
//+------------------------------------------------------------------+

3. Trabajando con posiciones

Comencemos con un evento de transacciones simple: la apertura y cierre de posiciones. En primer lugar, debemos saber qué procesos tienen lugar cuando pulsamos los botones "Vender" y "Comprar".

Si hacemos una llamada en la función OnTrade():

Alert("El evento ha ocurrido");

Veremos que, tras la apertura por parte de la función OnTrade() y paralelamente a ella, nuestra alerta se ejecuta cuatro veces:

Figura 1. Alertas

Figura 1. Alertas

¿Por qué se llama cuatro veces a la función OnTrade()? y ¿cómo podemos responder a estas alertas? Para comprender esto revisemos la documentación:

 Función OnTrade

La función es llamada cuando ocurre el evento Trade. Esto ocurre cuando se cambia la lista de órdenes cursadas, posiciones abiertas, historial de órdenes e historial de contratos.

Aquí debo mencionar algo:

Al escribir este artículo y contactar con los programadores, me dí cuenta de que ¡los cambios en el historial no generan una llamada a la función OnTrade()! Lo que ocurre es que la función OnTrade() solo es llamada ¡cuando se cambia la lista de órdenes cursadas y la de posiciones abiertas! Al desarrollar controladores de eventos de transacciones puede encontrarse con que las órdenes ejecutadas y los contratos pueden aparecer en el historial con cierto retardo y, en esta situación, no podrá procesarlas mientras la función OnTrade() se está ejecutando.  

Volvamos ahora a los eventos. Como hemos visto, cuando abrimos a través del mercado, el evento Trade se genera 4 veces:

  1. Crear una orden para abrir a través del mercado.
  2. Ejecutar la transacción.
  3. Pasar la orden completa al historial.
  4. Apertura de posición.

  Para seguir este proceso en el terminal, preste atención a la lista de órdenes en la pestaña "Trade" de la ventana de MetaTrader : 

Figura 2. Lista de órdenes en la pestaña "Trade"

Figura 2. Lista de órdenes en la pestaña "Trade"

Una vez que abrimos una posición (p.ej. abajo), en la lista de órdenes aparece una orden que tiene el estado comenzada (Fig. 2). Esto modifica la lista de órdenes cursadas y se llama al evento "Trade". La función OnTrade se activa la primera vez. Y entonces se ejecuta un contrato por parte de la orden creada. En esta etapa, la función OnTrade() es ejecutada por segunda vez. En el momento en el que el contrato es ejecutado, la orden completa y su contrato son enviados al historial y se llama a la función OnTrade() por tercera vez. En esta etapa, el contrato ejecutado abre una posición y se llama a la función OnTrade() por cuarta vez.

Para "capturar" el momento de apertura de la posición, cada vez que se llama a OnTrade() debemos analizar la lista de órdenes, el historial de órdenes y el historial de contratos. ¡Esto es, precisamente, lo que vamos a hacer a continuación!

De acuerdo, la función OnTrade() es llamada y necesitamos saber si el número de órdenes ha cambiado en la pestaña "Trade". Para hacer esto, debemos comparar el número de órdenes en la lista en el momento de la llamada previa a OnTrade() y también ahora. Para conocer cuántas órdenes hay en la lista en este momento usaremos la función OrdersTotal(). Y para conocer cuántas órdenes se han listado en la llamada previa tendremos que mantener el valor de OrdersTotal() en cada llamada a OnTrade(). Para esto vamos a crear una variable especial:

int OrdersPrev = 0;        // Número de órdenes en el momento de la llamada previa a OnTrade()

Al final de la función OnTrade() a la variable OrdersPrev se le asignará el valor de OrdersTotal().

También deberíamos considerar la situación en la que ejecutamos el asesor experto y todavía existen órdenes pendientes en la lista. El experto debería ser capaz de detectarlas y, por tanto, a la función OnInit() se le debe asignar el valor de OrdersTotal(). Los cambios que acabamos de realizar en el experto quedarían como se muestra a continuación: 

int OrdersPrev = 0;        // Número de órdenes en el momento de la llamada previa a OnTrade()


//+------------------------------------------------------------------+
//| Función de inicialización del Experto 			     |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   OrdersPrev = OrdersTotal();
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Función OnTrade 						     |
//+------------------------------------------------------------------+
void OnTrade()
  {
//---

OrdersPrev = OrdersTotal();
//---
  }

Ahora que conocemos el número de órdenes para las llamadas actuales y previas, podemos averiguar cuándo aparece la orden en la lista y cuándo esta, por cualquier motivo, ha desaparecido. Para hacer esto usaremos la siguiente condición:

if (OrdersPrev < OrdersTotal())
{
  // Apareció la orden
}
else if(OrdersPrev > OrdersTotal())
{
  // Desapareció la orden
}

Por tanto, resulta que si en la llamada previa tenemos menos órdenes que ahora, la orden aparece en la lista (no pueden aparecer múltiples órdenes simultáneamente), pero si ocurre lo contrario, p.ej. tenemos ahora menos órdenes que en una llamada previa de OnTrade(), entonces la orden puede ser ejecutada o cancelada por algún motivo. Casi todo el trabajo con posiciones comienza con estas dos condiciones.

Solo Stop Loss y Take Profit han de ser configuradas de forma distinta. Voy a añadir el código a la función OnTrade(), que trabaja con posiciones. Veámoslo a continuación:

void OnTrade()
  {
//---
Alert("Ha ocurrido un evento Trade");

HistorySelect(start_date,TimeCurrent());

if (OrdersPrev < OrdersTotal())
{
   OrderGetTicket(OrdersTotal()-1);// Selecciona la última orden con la que trabajar
   _GetLastError=GetLastError();
   Print("Error #",_GetLastError);ResetLastError();
   //--
   if (OrderGetInteger(ORDER_STATE) == ORDER_STATE_STARTED)
   {
      Alert(OrderGetTicket(OrdersTotal()-1),"Order has arrived for processing");
      LastOrderTicket = OrderGetTicket(OrdersTotal()-1);    // Guarda el ticket de la orden para usarlo posteriormente
   }
   
}
else if(OrdersPrev > OrdersTotal())
{
   state = HistoryOrderGetInteger(LastOrderTicket,ORDER_STATE);

   // Si no se encuentra una orden, genera error
   _GetLastError=GetLastError();
   if (_GetLastError != 0){Alert("Error #",_GetLastError," Order is not found!");LastOrderTicket = 0;}
   Print("Error #",_GetLastError," state: ",state);ResetLastError();


   // Si la orden se ejecuta completamente
   if (state == ORDER_STATE_FILLED)
   {
      // Entonces analiza el último contrato
      // --
      Alert(LastOrderTicket, "Order executed, going to deal");
      switch(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_ENTRY))
      {
         
         // Entrando al mercado
         case DEAL_ENTRY_IN:
         Alert(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_ORDER), 
         " order invoked deal #",HistoryDealGetTicket(HistoryDealsTotal()-1));
         
            switch(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE))
            {
               case 0:
               // Si los volúmenes de posición y contrato son iguales, la posición ha sido abierta
                  if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) 
                  && (PositionGetDouble(POSITION_VOLUME) == HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME)))
                  {
                     Alert("Una posición Buy ha sido abierta en el par", 
                           HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); 
                  }
                  else
               // Si los volúmenes de posición y contrato no son iguales, entonces la posición ha sido incrementada
                  if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) 
                  && (PositionGetDouble(POSITION_VOLUME) > HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME)))
                  {
                     Alert("Una posición Buy ha sido incrementada en el par ", 
                           HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); 
                  }
               break;
               
               case 1:
               // Si los volúmenes de posición y contrato son iguales, la posición ha sido abierta
                  if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) 
                  && (PositionGetDouble(POSITION_VOLUME) == HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME)))
                  {
                     Alert("Una posición Sell ha sido abierta en el par ", 
                           HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); 
                  }
                  else
               // Si los volúmenes de posición y contrato no son iguales, entonces la posición ha sido incrementada
                  if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) 
                  && (PositionGetDouble(POSITION_VOLUME) > HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME)))
                  {
                     Alert("Una posición Buy ha sido incrementada en el par ", 
                           HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); 
                  }
                  
               break;
               
               default:
                  Alert("Código no procesado del tipo: ",
                        HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE));
               break;
            }
         break;
         
         // Saliendo del mercado
         case DEAL_ENTRY_OUT:
         Alert(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_ORDER), 
         " order invoked deal #",HistoryDealGetTicket(HistoryDealsTotal()-1));
         
            switch(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE))
            {
               case 0:
               // Si la posición que hemos intentado cerrar todavía existe, entonces hemos cerrado solo una parte de ella
                  if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) == true)
                  {
                     Alert("Parte de la posición Sell ha sido cerrada en el par ", 
                           HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL),
                           " with profit = ",
                           HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_PROFIT));
                  }
                  else
               // Si no se encuentra la posición, entonces esta completamente cerrada
                  if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) == false)
                  {
                     Alert("Una posición Sell ha sido cerrada en el par ",
                           HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL),
                           " with profit = ",
                           HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_PROFIT));
                  }
               break;
               
               case 1:
               // Si la posición que hemos intentado cerrar todavía existe, entonces hemos cerrado solo una parte de ella
                  if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) == true)
                  {
                     Alert("Parte de una posición Buy ha sido cerrada en el par ", 
                           HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL),
                           " with profit = ",
                           HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_PROFIT));
                  }
                  else
               // Si no se encuentra la posición, entonces esta completamente cerrada
                  if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) == false)
                  {
                     Alert("Una posición Buy ha sido cerrada en el par",
                           HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL),
                           " with profit = ",
                           HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_PROFIT));
                  }
                  
               break;
               
               default:
                  Alert("Código no procesado del tipo: ",
                        HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE));
               break;
            }
         break;
         
         // Inverso
         case DEAL_ENTRY_INOUT:
         Alert(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_ORDER), 
         " order invoked deal #",HistoryDealGetTicket(HistoryDealsTotal()-1));
         
            switch(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE))
            {
               case 0:
                  Alert("Sell se ha invertido a Buy en el par ",
                        HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL),
                        " resulting profit = ",
                        HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_PROFIT)); 
               break;
               
               case 1:
                  Alert("Buy se ha invertido a Sell en el par ",
                        HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL),
                        " resulting profit = ",
                        HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_PROFIT)); 
               break;
               
               default:
                  Alert("Código no procesado del tipo: ",
                        HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE));
               break;
            }
         break;
         
         // Indica el registro de stado
         case DEAL_ENTRY_STATE:
            Alert("Indica el registro del estado. Código no procesado del tipo: ", 
            HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE));
         break;
      }
      // --
   }
}

OrdersPrev = OrdersTotal();

//---
  }

Asegúrese también de que al principio del programa ha declarado las siguientes variables:

datetime start_date = 0;   // Fecha a partir de la que comenzamos a leer el historial

int OrdersPrev = 0;        // Número de órdenes en el momento de la llamada previa a OnTrade()
int PositionsPrev = 0;     // Número de posiciones en el momento de la llamada previa a OnTrade()
ulong LastOrderTicket = 0; // Ticket de la última orden procesada

int _GetLastError=0;       // Código de error 
long state=0;              // Estado de la orden

Volvamos ahora a los contenidos de OnTrade().

Puede comentar la alerta al comienzo, pero dejaré que Siguiente vaya a la función HistorySelect(). Esto genera un historial de contratos y órdenes para el período de tiempo señalado que viene definido por dos parámetros de la función. Si esta función no es llamada antes de ir al historial de contratos y órdenes, no obtendremos ninguna información ya que las listas del historial estarán vacías. Después de llamar a HistorySelect() se evalúan las condiciones tal y como se escribió antes.

Cuando llegan nuevas órdenes primero las seleccionamos y verificamos los errores:

OrderGetTicket(OrdersTotal()-1);// Seleccionar la última orden
_GetLastError=GetLastError();
Print("Error #",_GetLastError);ResetLastError();

Después de seleccionar la orden obtenemos el código de error usando la función GetLastError(). A continuación, usando la función Print() escribimos el código y usando la función ResetLastError() restablecemos el código de error a cero y, de esta forma, en la próxima llamada a GetLastError() para otras situaciones no nos aparecerá el mismo error.

Después de verificar los errores, si la orden se ha seleccionado con éxito verificamos su estado:

if (OrderGetInteger(ORDER_STATE) == ORDER_STATE_STARTED)
{
   Alert(OrderGetTicket(OrdersTotal()-1),"La orden ha llegado para el procesado");
   LastOrderTicket = OrderGetTicket(OrdersTotal()-1);    // Guarda el ticket de la orden para usarlo posteriormente
}

Si la orden tiene el estado iniciada, p.ej. ha sido corregida pero no aceptada, entonces se espera a que sea ejecutada próximamente y simplemente damos un Alert() notificando que la orden está siendo procesada y guardamos su ticket en la próxima llamada a OnTrade(). En lugar de Alert() puede usar cualquier otro tipo de notificación.

En el código anterior, la línea

OrderGetTicket(OrdersTotal()-1)

devolverá el ticket de la última orden a partir de la lista completa de órdenes.

OrdersTotal()-1 indica que necesitamos obtener la última orden. Como la función OrdersTotal() devuelve el número total de órdenes (p.ej. si el orden es 1, OrdersTotal() devolverá 1) y el número de índice de orden se cuenta desde 0, para obtener el número de índice de la última orden debemos restar 1 del total de números de órdenes (si OrdersTotal() devuelve 1, el número de índice de esta orden será igual a 0). Y la función OrderGetTicket() devolverá el ticket de la orden cuyo número le haya sido pasado.

Lla primera condición fue la considerada en la primera llamada de OnTrade(). A continuación le sigue la segunda condición que se cumple en la segunda llamada a OnTrade(), cuando la orden es ejecutada, cedida al historial y la posición se abre.

Si la orden no se encuentra en la lista, entonces se cedió al historial, ¡debe estar ahí! Por tanto, recurrimos al historial de órdenes usando la función HistoryOrderGetInteger() para obtener el estado de la orden. Y para leer los datos históricos de una orden concreta necesitamos su ticket. Por ello, si se cumple la primera condición, el ticket de la orden de llegada ha sido almacenado en la variable LastOrderTicket.

De este modo, obtenemos el estado de la orden, indicando el ticket de la orden como primer parámetro para HistoryOrderGetInteger() y el tipo propiedad requerida como segundo. Después de intentar obtener el estado de la orden obtenemos el código de error y lo escribimos en el diario. Esto es necesario en caso de que nuestra orden, con la que necesitaremos trabajar, no ha sido aún introducida en el historial y recurrimos a él (la experiencia demuestra que esto es muy probable. He escrito sobre esto al comienzo de este artículo).

Si ocurre un error, se detiene el proceso al no haber datos con los que trabajar y ninguna de las siguientes condiciones se cumplen. Y si la llamada de HistoryOrderGetInteger() tuvo éxito y la orden tiene el estado "orden completamente ejecutada":

// Si la orden está completamente ejecutada
if (state == ORDER_STATE_FILLED)

Entonces realizamos otra notificación:

// Analizamos el último contrato
// --
  Alert(LastOrderTicket, "Orden ejecutada, yendo al contrato");

Y ahora vamos a procesar la transacción que fue invocada por esta orden. En primer lugar averiguamos la dirección de la transacción (propiedad DEAL_ENTRY). La dirección no es la de comprar o vender sino la de entrando al mercado, saliendo del mercado, inverso o indicación del registro del estado. De esta forma, usando la propiedad DEAL_ENTRY podemos averiguar si la orden fue establecida a la posición abierta, cerrada o inversa. 

Para analizar el contrato y sus resultados, vamos a recurrir al historial usando la siguiente construcción:

switch(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_ENTRY))
{
  ...
}

Funciona de igual forma que con los demás:

HistoryDealsTotal() devuelve el número total de contratos. Para obtener el número del último contrato restamos 1 al valor de HistoryDealsTotal(). El número resultante para el contrato se pasa a la función HistoryDealGetTicket(), que a su vez pasa el tique del contrato seleccionado a la función HistoryDealGetInteger(). Y la función HistoryDealGetInteger()  devolverá la dirección del contrato.

Vamos a examinar en detalle la dirección de entrando al mercado. Trataremos las demás direcciones en breve, ya que serán procesadas casi de la misma forma:

El valor de la expresión obtenido de HistoryDealGetInteger() se compara con los valores de los bloques de la instancia hasta que se encuentra una coincidencia. Supongamos que entramos al mercado, p.ej. abriendo una orden de venta. El primer bloque será entonces ejecutado:

// Entrando en al mercado
case DEAL_ENTRY_IN:

Al comienzo del bloque se nos notifica sobre la creación de un contrato. Al mismo tiempo, esta notificación asegura que todo marcha correctamente y que el contrato está siendo procesado.

Tras la notificación viene otro bloque de opción que analiza el tipo de contrato:

   switch(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE))
   {
      case 0:
      // Si los volúmenes de posición y contrato son iguales, la posición ha sido abierta
         if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) 
         && (PositionGetDouble(POSITION_VOLUME) == HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME)))
         {
            Alert("La posición Buy ha sido abierta en el par", 
                  HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); 
         }
         else
      // Si los volúmenes de posición y contrato no son iguales, entonces la posición ha sido incrementada
         if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) 
         && (PositionGetDouble(POSITION_VOLUME) > HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME)))
         {
            Alert("La posición Buy se ha incrementado en el par ", 
                  HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); 
         }
      break;
      
      case 1:
      // Si los volúmenes de posición y contrato son iguales, la posición ha sido abierta
         if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) 
         && (PositionGetDouble(POSITION_VOLUME) == HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME)))
         {
            Alert("La posición Sell ha sido abierta en el par ", 
                  HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); 
         }
         else
      // Si los volúmenes de posición y contrato no son iguales, entonces la posición ha sido incrementada
         if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) 
         && (PositionGetDouble(POSITION_VOLUME) > HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME)))
         {
            Alert("La posición Sell se ha incrementado en el par ", 
                  HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); 
         }
         
      break;
      
      default:
         Alert("Código no procesado del tipo: ",
               HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE));
      break;
   }

Obtiene información sobre el contrato a partir del historial -de la misma forma que anteriormente, excepto la propiedad especificada. En esta ocasión debemos especificar el DEAL_TYPE para saber qué contrato, comprar o vender, se ha realizado. Solo analizo los tipos "comprar" y "vender", pero además de estos hay otros cuatro. No obstante, estos cuatro tipos restantes son menos frecuentes, por lo que en lugar de cuatro solo se utiliza un bloque por defecto. Este genera un evento Alert() con el código tipo.

Como habrá podido advertir en el código, no solo se procesan las aperturas de las posiciones "comprar" y "vender" sino también su incremento. Para saber cuándo se ha incrementado la posición y cuando se ha abierto, necesitamos comparar el volumen del contrato y la posición ejecutado como consecuencia de este contrato. Si el volumen de la posición es igual al volumen del contrato ejecutado, entonces la posición ha sido abierta, mientras que si el volumen de la posición y el del contrato son distintos, entonces la posición ha sido incrementada. Esto es aplicable tanto a las posiciones "comprar" (en el caso del bloque "0") como a las posiciones "vender" (en el caso del bloque "1"). El último bloque es el bloque por defecto que gestiona las situaciones distintas a "comprar" y "vender". El proceso completo consiste en la notificación sobre el código tipo devuelto por la función HistoryDealGetInteger().

Y para terminar, un último aspecto sobre el trabajo con posiciones. El procesamiento de los cambios producidos en los valores de Stop Loss y Take Profit. Para saber cuál ha sido el parámetro que ha cambiado en una posición necesitamos comparar los estados actuales de dichos parámetros con los anteriores. Los valores actuales de los parámetros de la posición siempre pueden obtenerse mediante funciones de servicio, pero los parámetros previos deben haberse guardado.

Para ello, vamos a escribir una función especial que guardará los parámetros de la posición en la matriz de estructuras:

void GetPosition(_position &Array[])
  {
   int _GetLastError=0,_PositionsTotal=PositionsTotal();

   int temp_value=(int)MathMax(_PositionsTotal,1);
   ArrayResize(Array, temp_value);

   _ExpertPositionsTotal=0;
   for(int z=_PositionsTotal-1; z>=0; z--)
     {
      if(!PositionSelect(PositionGetSymbol(z)))
        {
         _GetLastError=GetLastError();
         Print("OrderSelect() - Error #",_GetLastError);
         continue;
        }
      else
        {
            // Si se encuentra la posición, pone su posición en la matriz
            Array[z].type         = PositionGetInteger(POSITION_TYPE);
            Array[z].time         = PositionGetInteger(POSITION_TIME);
            Array[z].magic        = PositionGetInteger(POSITION_MAGIC);
            Array[z].volume       = PositionGetDouble(POSITION_VOLUME);
            Array[z].priceopen    = PositionGetDouble(POSITION_PRICE_OPEN);
            Array[z].sl           = PositionGetDouble(POSITION_SL);
            Array[z].tp           = PositionGetDouble(POSITION_TP);
            Array[z].pricecurrent = PositionGetDouble(POSITION_PRICE_CURRENT);
            Array[z].comission    = PositionGetDouble(POSITION_COMMISSION);
            Array[z].swap         = PositionGetDouble(POSITION_SWAP);
            Array[z].profit       = PositionGetDouble(POSITION_PROFIT);
            Array[z].symbol       = PositionGetString(POSITION_SYMBOL);
            Array[z].comment      = PositionGetString(POSITION_COMMENT);
        _ExpertPositionsTotal++;
        }
     }

   temp_value=(int)MathMax(_ExpertPositionsTotal,1);
   ArrayResize(Array,temp_value);
  }

Para usar esta función debemos añadir el siguiente código al bloque de declaración de las variables globales:

/*
 *
 * Estructura que almacena información sobre las posiciones
 *
 */
struct _position
{

long     type,          // Tipo de posición
         magic;         // Número mágico para la posición
datetime time;          // Tiempo de apertura de la posición

double   volume,        // Volumen de la posición
         priceopen,     // Precio de la posición
         sl,            // Nivel de Stop Loss para la posición abierta
         tp,            // Nivel de Take Profit para la posición abierta
         pricecurrent,  // Precio actual de Symbol
         comission,     // Comisión
         swap,          // Swap acumulado
         profit;        // Beneficio actual

string   symbol,        // Símbolo por el que la posición ha sido abierta
         comment;       // Comentario sobre la posición
};

int _ExpertPositionsTotal = 0;

_position PositionList[],     // Matriz que almacena información sobre la posición
          PrevPositionList[];

El prototipo de la función GetPosition() se encuentra desde hace tiempo en los artículos de www.mql4.com, pero no podría encontrarlo ahora y no puedo indicar la fuente. No voy a tratar aquí el papel de esta función en detalle. El caso es que como parámetro ha pasado una matriz del tipo _position (estructura con campos correspondientes a los campos de la posición) para la que toda la información sobre las posiciones abiertas actualmente y los valores de sus parámetros ha sido pasada.

Para hacer un seguimiento correcto de los cambios en los parámetros de la posición vamos a crear dos matrices del tipo _position. Estas son PositionList[] (el estado actual de las posiciones) y PrevPositionList[] (el estado previo de las posiciones).

Para empezar a trabajar con posiciones debemos añadir la próxima llamada a la función OnInit() y también al final de OnTrade().

GetPosition(PrevPositionList);

Debemos también añadir la siguiente llamada al comienzo de OnTrade():

GetPosition(PositionList);

Ahora, en las matrices PositionList[] y PrevPositionList[] tendremos información disponible sobre la llamada actual y previa a OnTrade() respectivamente.

Vamos a ver ahora el código para el seguimiento de los cambios en sl y tp:

if ((PositionsPrev == PositionsTotal()) && (OrdersPrev == OrdersTotal()))
{
   string _alerts = "";
   bool modify = false;
     
   for (int i=0;i<_ExpertPositionsTotal;i++)
   {
      if (PrevPositionList[i].sl != PositionList[i].sl)
      {
         _alerts += "On pair "+PositionList[i].symbol+" Stop Loss changed from "+ PrevPositionList[i].sl +" to "+ PositionList[i].sl +"\n";
         modify = true;
      }
      if (PrevPositionList[i].tp != PositionList[i].tp)
      {
         _alerts += "On pair "+PositionList[i].symbol+" Take Profit changed from "+ PrevPositionList[i].tp +" to "+ PositionList[i].tp +"\n";
         modify = true;
      }
      
   }
   if (modify == true)
   {
      Alert(_alerts);
      modify = false;
   }
}

Como vemos, el código no es demasiado extenso, pero esto es debido a la gran cantidad de trabajo que hemos realizado antes. Vamos a profundizar en ello.

Comienza con la condición siguiente:

if ((PositionsPrev == PositionsTotal()) && (OrdersPrev == OrdersTotal()))

Aquí vemos que no se han eliminado ni las órdenes ni las posiciones. Si la condición se cumple, lo más probable es que los parámetros de algunas posiciones u órdenes hayan cambiado.

Al comienzo de la función se declaran dos variables:

  • _alerts, que guarda todas las notificaciones sobre los cambios.
  • modify, que nos permite visualizar en pantalla los mensajes sobre los cambios solo si estos se han producido.

A continuación, en el lazo comprobamos la coincidencia de los valores de Take Profit y Stop Loss en la llamada anterior y actual de OnTrade() para cada posición. La información sobre todas las discordancias se guardarán en la variable _alerts y se mostrarán posteriormente en la función Alert(). A propósito, el procesamiento de la modificación de órdenes pendientes se realizará de la misma forma.

Por ahora, vamos a finalizar con las posiciones y a cursar las órdenes pendientes.

4. Trabajando con órdenes

Comencemos con el evento correspondiente a la realización de órdenes pendientes.

Cuando aparece una nueva orden pendiente, el evento Trade solo se genera una vez, ¡pero con eso es suficiente para que sea procesado! Ponemos el código que se encarga de las órdenes pendientes en el cuerpo del operador:

if (OrdersPrev < OrdersTotal())

Y obtenemos lo siguiente:

if (OrdersPrev < OrdersTotal())
{
   OrderGetTicket(OrdersTotal()-1);// Selecciona la última orden con la que trabajar
   _GetLastError=GetLastError();
   Print("Error #",_GetLastError);ResetLastError();
   //--
   if (OrderGetInteger(ORDER_STATE) == ORDER_STATE_STARTED)
   {
      Alert(OrderGetTicket(OrdersTotal()-1),"La orden ha llegado para el procesado");
      LastOrderTicket = OrderGetTicket(OrdersTotal()-1);    // Guardando el ticket de la orden para un trabajo posterior
   }
   
   state = OrderGetInteger(ORDER_STATE);
   if (state == ORDER_STATE_PLACED)
   {
      switch(OrderGetInteger(ORDER_TYPE))
      {
         case 2:
            Alert("Pending order Buy Limit #", OrderGetTicket(OrdersTotal()-1)," accepted!");
         break;
         
         case 3:
            Alert("Pending order Sell Limit #", OrderGetTicket(OrdersTotal()-1)," accepted!");
         break;
         
         case 4:
            Alert("Pending order Buy Stop #", OrderGetTicket(OrdersTotal()-1)," accepted!");
         break;
         
         case 5:
            Alert("Pending order Sell Stop #", OrderGetTicket(OrdersTotal()-1)," accepted!");
         break;
         
         case 6:
            Alert("Pending order Buy Stop Limit #", OrderGetTicket(OrdersTotal()-1)," accepted!");
         break;
                 
         case 7:
            Alert("Pending order Sell Stop Limit  #", OrderGetTicket(OrdersTotal()-1)," accepted!");
         break;         
      }
   }
}

Aquí, el código que funciona con órdenes pendientes comienza de la siguiente forma:

   state = OrderGetInteger(ORDER_STATE);
   if (state == ORDER_STATE_PLACED)
   {

En primer lugar se verifica el estado de la orden. La orden debe tener el estado ORDER_STATE_PLACED, p.ej. debería ser aceptada. Y si la condición se cumple, le sigue entonces el operador switch, que muestra un mensaje en función del tipo de orden.

A continuación trabajaremos con los eventos que se producen cuando se modifican las órdenes. La modificación de órdenes es similar a la modificación de las posiciones. Igualmente, se crea la estructura que guarda las propiedades de las órdenes: 

/*
 *
 * Estructura que guarda información sobre las órdenes
 *
 */
struct _orders
{

datetime time_setup,       // Momento en que se cursa la orden
         time_expiration,  // Momento del vencimiento de la orden
         time_done;        // Momento de la ejecución o cancelación de la orden
         
long     type,             // Tipo de orden
         state,            // Estado de la orden
         type_filling,     // Tipo de ejecución por recordatorio
         type_time,        // Duración de la orden
         ticket;           // Ticket de la orden
         
long     magic,            // Identificador del Expert Advisor que cursó una orden
                           // (para asegurar que cada Experto
                           // debe usar su propio y único número)
                           
         position_id;      // Identificador de la posición en la orden
                           // cuando es ejecutado. Cada orden ejecutada invoca a un
                           // contrato, que abre una posición nueva o cambia 
                           // una existente. El identificador de esa posición se sitúa en
                           // la orden ejecutada en este momento.
                           
double volume_initial,     // Volumen inicial al cursar la orden
       volume_current,     // Volumen no llenado
       price_open,         // Precio indicado en la orden
       sl,                 // Nivel de Stop Loss
       tp,                 // Nivel de Take Profit 
       price_current,      // Precio actual por el símbolo de la orden
       price_stoplimit;    // Precio de cursar una orden Limit cuando la orden Stop Limit es lanzada
       
string symbol,             // Símbolo por el que la orden ha sido cursada
       comment;            // Comentario
                           
};

int _ExpertOrdersTotal = 0;

_orders OrderList[],       // Matrices que guardan información sobre las órdenes
        PrevOrderList[];

Cada campo de la estructura corresponde a una de las propiedades de la orden. Después de declarar la estructura, se declara la variable de tipo int y dos matrices de _orders. La variable _ExpertOrdersTotal guardará el número total de órdenes y las matrices OrderList[] y PrevOrderList[] almacenarán la información sobre las órdenes en la llamada actual y previa de OnTrade(), respectivamente.

La función es como se muestra a continuación:

void GetOrders(_orders &OrdersList[])
  {
   
   int _GetLastError=0,_OrdersTotal=OrdersTotal();

   int temp_value=(int)MathMax(_OrdersTotal,1);
   ArrayResize(OrdersList,temp_value);

   _ExpertOrdersTotal=0;
   for(int z=_OrdersTotal-1; z>=0; z--)
     {
      if(!OrderGetTicket(z))
        {
         _GetLastError=GetLastError();
         Print("GetOrders() - Error #",_GetLastError);
         continue;
        }
      else
        {
        OrdersList[z].ticket          = OrderGetTicket(z);
        OrdersList[z].time_setup      = OrderGetInteger(ORDER_TIME_SETUP);
        OrdersList[z].time_expiration = OrderGetInteger(ORDER_TIME_EXPIRATION);
        OrdersList[z].time_done       = OrderGetInteger(ORDER_TIME_DONE);
        OrdersList[z].type            = OrderGetInteger(ORDER_TYPE);
        
        OrdersList[z].state           = OrderGetInteger(ORDER_STATE);
        OrdersList[z].type_filling    = OrderGetInteger(ORDER_TYPE_FILLING);
        OrdersList[z].type_time       = OrderGetInteger(ORDER_TYPE_TIME);
        OrdersList[z].magic           = OrderGetInteger(ORDER_MAGIC);
        OrdersList[z].position_id     = OrderGetInteger(ORDER_POSITION_ID);
        
        OrdersList[z].volume_initial  = OrderGetDouble(ORDER_VOLUME_INITIAL);
        OrdersList[z].volume_current  = OrderGetDouble(ORDER_VOLUME_CURRENT);
        OrdersList[z].price_open      = OrderGetDouble(ORDER_PRICE_OPEN);
        OrdersList[z].sl              = OrderGetDouble(ORDER_SL);

        OrdersList[z].tp              = OrderGetDouble(ORDER_TP);
        OrdersList[z].price_current   = OrderGetDouble(ORDER_PRICE_CURRENT);
        OrdersList[z].price_stoplimit = OrderGetDouble(ORDER_PRICE_STOPLIMIT);
        
        OrdersList[z].symbol          = OrderGetString(ORDER_SYMBOL);
        OrdersList[z].comment         = OrderGetString(ORDER_COMMENT);
        
        _ExpertOrdersTotal++;
        }
     }

   temp_value=(int)MathMax(_ExpertOrdersTotal,1);
   ArrayResize(OrdersList,temp_value);

  }

De forma similar a la función GetPosition(), lee la información sobre las propiedades de cada orden cursada y la pone en la matriz que le ha sido pasada como parámetro de entrada. El código de la función debe situarse al final del experto y sus llamadas son como se muestra a continuación:

GetOrders(PrevOrderList);

Situada en OnInit() y al final de OnTrade().

GetOrders(OrderList);

Situada al comienzo de OnTrade().

Veamos ahora el código que procesará la modificación de las órdenes. Es un lazo que complementa al código de modificación de las posiciones:

   for (int i = 0;i<_ExpertOrdersTotal;i++)
   {
      if (PrevOrderList[i].sl != OrderList[i].sl)
      {
         _alerts += "Order "+OrderList[i].ticket+" has changed Stop Loss from "+ PrevOrderList[i].sl +" to "+ OrderList[i].sl +"\n";
         modify = true;
      }
      if (PrevOrderList[i].tp != OrderList[i].tp)
      {
         _alerts += "Order "+OrderList[i].ticket+" has changed Take Profit from "+ PrevOrderList[i].tp +" to "+ OrderList[i].tp +"\n";
         modify = true;
      }
   }

El lazo procesa todas las órdenes y compara los valores de Stop Loss y Take Profit en las llamadas actuales y previas de OnTrade(). Si hay diferencias, se guardan en la variable _alerts y cuando se completa el lazo serán mostradas en pantalla por la función Alert().

Este es el código que se sitúa en el cuerpo del operador:

if ((PositionsPrev == PositionsTotal()) && (OrdersPrev == OrdersTotal()))
{

Justo a continuación del lazo que trabaja con posiciones.

Pero el trabajo con eventos de operaciones de trading no ha finalizado aún. Este artículo solo trata los principios fundamentales del trabajo con el evento Trade. En general, las posibilidades que ofrece este método son mucho mayores que las tratadas en este artículo.

Conclusión

La capacidad para trabajar con eventos de transacciones (como parte del lenguaje MQL5) es una poderosa herramienta que permite no solo implementar rápidamente algoritmos de verificación de órdenes y generar informes de trading, sino que también reduce el coste de los recursos del sistema y el volumen del código fuente que, sin duda, beneficia a todos los programadores.

Traducción del ruso hecha por MetaQuotes Software Corp.
Artículo original: https://www.mql5.com/ru/articles/40

Archivos adjuntos |
tradecontrol_en.mq5 (41.86 KB)
Indicadores personalizados para principiantes en MQL5 Indicadores personalizados para principiantes en MQL5

Cualquier materia parece complicada y difícil de aprender para un principiante. Materias que ahora nos parecen muy simples y claras. Pero no olvidemos que todos tenemos que aprender desde cero, incluso nuestro propio idioma. Lo mismo ocurre con el lenguaje de programación MQL5 que ofrece grandes posibilidades para desarrollar nuestras propias estrategias de trading. Podemos empezar a aprenderlo comenzando con nociones más básicas y los ejemplos más sencillos. En este artículo vamos a considerar la interacción de un indicador técnico con el terminal de cliente con un ejemplo de indicador personalizado SMA.

Usando los punteros de objeto en MQL5 Usando los punteros de objeto en MQL5

Por defecto, todos los objetos en MQL5 se pasan por referencia, pero hay una posibilidad de usar los punteros de objeto. Sin embargo, es necesario realizar una comprobación del puntero ya que el objeto puede no ser inicializado. En este caso, el programa MQL5 terminará con un error crítico y descargado. Los objetos, creados automáticamente, no causan este error, por lo que, en esencia, son muy seguros. En este artículo intentaremos comprender la diferencia entre la referencia del objeto y el puntero del mismo y veremos cómo escribir código seguro con el uso de punteros.

Cómo llamar a los indicadores en MQL5 Cómo llamar a los indicadores en MQL5

Con la nueva versión del lenguaje de programación MQL no solo ha cambiado la forma de trabajar con los indicadores, sino que también hay nuevas formas de crearlos. Además, dispone de mayor flexibilidad al trabajar con buffers de indicadores, ya que ahora es posible indicar la dirección de indexado y obtener exactamente el número de indicadores que desee. Este artículo explica los métodos básicos para llamar a los indicadores y obtener los datos a partir del buffer de cada indicador.

Los estilos de representación en MQL5 Los estilos de representación en MQL5

Hay 6 estilos de representación en MQL4 y 18 en MQL5. Por ello, merece la pena dedicar un artículo a una introducción sobre los estilos de representación en MQL5. En este artículo vamos a tratar en profundidad los estilos de representación en MQL5. Además, crearemos un indicador para mostrar estos estilos y configurar el trazado.