Generación de eventos personalizados

Además de los eventos estándar, el terminal admite la capacidad de generar mediante programación eventos personalizados, cuya esencia y contenido vienen determinados por el programa MQL. Dichos eventos se añaden a la cola general de eventos del gráfico y pueden ser procesados en la función OnChartEvent por todos los programas interesados.

Se reserva un rango especial de 65536 identificadores enteros para los eventos personalizados: de CHARTEVENT_CUSTOM a CHARTEVENT_CUSTOM_LAST, ambos inclusive. En otras palabras: el evento personalizado debe tener el ID CHARTEVENT_CUSTOM + n, donde n está entre 0 y 65535. CHARTEVENT_CUSTOM_LAST es exactamente igual a CHARTEVENT_CUSTOM + 65535.

Los eventos personalizados se envían al gráfico mediante la función EventChartCustom.

bool EventChartCustom(long chartId, ushort customEventId,
long lparam, double dparam, string sparam)

chartId es el identificador del gráfico receptor del evento, mientras que 0 indica el gráfico actual; customEventId es el identificador del evento (seleccionado por el desarrollador del programa MQL). Este identificador se añade automáticamente al valor CHARTEVENT_CUSTOM y se convierte a un tipo entero. Este valor se pasará al manejador de OnChartEvent como primer argumento. Otros parámetros de EventChartCustom corresponden a los parámetros de eventos estándar de OnChartEvent con los tipos long, double y string, y pueden contener información arbitraria.

La función devuelve true en caso de que el evento de usuario se haya puesto en cola correctamente, o false en caso de error (el código de error estará disponible en _LastError).

A medida que nos acerquemos a la parte más compleja e importante de nuestro libro, dedicada directamente a la automatización del trading, empezaremos a resolver problemas aplicados que serán útiles en el desarrollo de robots de trading. Ahora, en el contexto de la demostración de las capacidades de los eventos personalizados, vamos a pasar al análisis multidivisa (o, más en general, multisímbolo) del entorno de trading.

Un poco antes, en el capítulo sobre indicadores, hemos visto los indicadores multidivisa, pero no prestamos atención a un punto importante: a pesar de que los indicadores procesaban cotizaciones de diferentes símbolos, el cálculo en sí se lanzó en el manejador OnCalculate, que se activó con la llegada de un nuevo tick de un único símbolo: el símbolo de trabajo del gráfico. Resulta que los ticks de otros instrumentos, en esencia, se omiten. Por ejemplo, si el indicador trabaja sobre el símbolo A, cuando llega su tick simplemente tomamos los últimos ticks conocidos de otros símbolos (B, C, D), pero es probable que otros ticks hayan conseguido colarse entre cada uno de ellos.

Si coloca un indicador multidivisa en el instrumento más líquido (donde se reciben ticks con más frecuencia), esto no es tan crítico. Sin embargo, diferentes instrumentos pueden ser más rápidos que otros en diferentes momentos del día, y si un algoritmo analítico o de negociación requiere la respuesta más rápida posible a las nuevas cotizaciones de todos los instrumentos de la cartera, nos enfrentamos al hecho de que la solución actual no nos conviene.

Por desgracia, el evento estándar de llegada de un nuevo tick funciona en MQL5 solo para un símbolo, que es el símbolo de trabajo del gráfico actual. En los indicadores, se llama al manejador OnCalculate en esos momentos, y el manejador OnTick se llama en los Asesores Expertos.

Por lo tanto, es necesario inventar algún mecanismo para que el programa MQL pueda recibir notificaciones sobre ticks en todos los instrumentos de interés. Aquí es donde los eventos personalizados nos ayudarán. Por supuesto, esto no es necesario para los programas que analizan un solo instrumento.

A continuación desarrollaremos un ejemplo del indicador EventTickSpy.mq5 que, siendo lanzado sobre un símbolo X específico, podrá enviar notificaciones de tick desde su función OnCalculate utilizando EventChartCustom. Como resultado, en el manejador OnChartEvent, que está especialmente preparado para recibir tales notificaciones, será posible recoger notificaciones de diferentes instancias del indicador desde diferentes símbolos.

Este ejemplo se ofrece a título ilustrativo. Posteriormente, cuando estudiemos el trading automatizado multidivisa, adaptaremos esta técnica para un uso más cómodo en los Asesores Expertos.

En primer lugar, pensemos en un número de evento personalizado para el indicador. Dado que vamos a enviar notificaciones de tick para muchos símbolos diferentes de una lista dada, podemos elegir aquí diferentes tácticas. Por ejemplo, puede seleccionar un identificador de evento, y pasar el número del símbolo en la lista y/o el nombre del símbolo en los parámetros lparam y sparam, respectivamente. O puede tomar alguna constante (mayor que e igual a CHARTEVENT_CUSTOM) y obtener números de eventos añadiendo el número de símbolo a esta constante (entonces tenemos todos los parámetros libres, en particular, lparam y dparam, y pueden utilizarse para transferir precios Ask, Bid o algún otro).

Nos centraremos en la opción cuando hay un código de evento. Declarémoslo en la macro TICKSPY. Este será el valor por defecto, que el usuario puede cambiar para evitar colisiones (aunque poco probables) con otros programas si es necesario.

#define TICKSPY 0xFEED // 65261

Este valor se toma a propósito por estar bastante alejado del primer CHARTEVENT_CUSTOM permitido.

Durante el lanzamiento inicial (interactivo) del indicador, el usuario debe especificar la lista de instrumentos cuyos ticks debe seguir el indicador. Para ello, describiremos la variable de cadena de entrada SymbolList con una lista de símbolos separados por comas.

El identificador del evento de usuario se establece en el parámetro message.

Por último, necesitamos el identificador del gráfico receptor para pasar el evento. Para ello proporcionaremos el parámetro Chart. El usuario no debe editarlo: en la primera instancia del indicador lanzada manualmente, el gráfico se conoce implícitamente al adjuntarlo al gráfico. En otras copias del indicador que nuestra primera instancia ejecutará programáticamente, este parámetro rellenará el algoritmo con una llamada a la función ChartID (véase más adelante).

input string SymbolList = "EURUSD,GBPUSD,XAUUSD,USDJPY"// List of symbols separated by commas (example)
input ushort message = TICKSPY;                          // Custom message
input longchart = 0;                                     // Receiving chart (do not edit)

En el parámetro SymbolList, por ejemplo, se indica una lista con cuatro herramientas comunes. Edítelo según sea necesario para adaptarlo a su Market Watch.

En el manejador OnInit, convertimos la lista al array de símbolos Symbols, y luego en un bucle ejecutamos el mismo indicador para todos los símbolos del array, excepto para el actual (por norma, existe tal coincidencia porque el símbolo actual ya está siendo procesado por esta copia inicial del indicador).

string Symbols[];
   
void OnInit()
{
   PrintFormat("Starting for chart %lld, msg=0x%X [%s]"ChartMessageSymbolList);
   if(Chart == 0)
   {
      if(StringLen(SymbolList) > 0)
      {
         const int n = StringSplit(SymbolList, ',', Symbols);
         for(int i = 0i < n; ++i)
         {
            if(Symbols[i] != _Symbol)
            {
               ResetLastError();
               // run the same indicator on another symbol with different settings,
               // in particular, we pass our ChartID to receive notifications back
               iCustom(Symbols[i], PERIOD_CURRENTMQLInfoString(MQL_PROGRAM_NAME),
                  ""MessageChartID());
               if(_LastError != 0)
               {
                  PrintFormat("The symbol '%s' seems incorrect"Symbols[i]);
               }
            }
         }
      }
      else
      {
         Print("SymbolList is empty: tracking current symbol only!");
         Print("To monitor other symbols, fill in SymbolList, i.e."
            " 'EURUSD,GBPUSD,XAUUSD,USDJPY'");
      }
   }
}

Al principio de OnInit se muestra en el registro información sobre la instancia del indicador lanzada para que quede claro lo que está sucediendo.

Si eligiéramos la opción con códigos de evento separados para cada carácter, tendríamos que llamar a iCustom de la siguiente manera (añadir i a message):

   iCustom(Symbols[i], PERIOD_CURRENTMQLInfoString(MQL_PROGRAM_NAME), "",
      Message + iChartID());

Tenga en cuenta que el valor distinto de cero del parámetro Chart implica que esta copia se lanza mediante programación y que debe supervisar un único símbolo, es decir, el símbolo de trabajo del gráfico. Por lo tanto, no necesitamos pasar una lista de símbolos al ejecutar las copias esclavas.

En la función OnCalculate, a la que se llama cuando se recibe un nuevo tick, enviamos el evento personalizado Message al gráfico Chart llamando a EventChartCustom. En este caso no se utiliza el parámetro lparam (igual a 0). En el parámetro dparam, pasamos el precio actual (último) price[0] (esto es, Bid o Last, según el tipo de precio en el que se base el gráfico: también es el precio del último tick procesado por el gráfico), y pasamos el nombre del símbolo en el parámetro sparam.

int OnCalculate(const int rates_totalconst int prev_calculated,
   const intconst double &price[])
{
   if(prev_calculated)
   {
      ArraySetAsSeries(pricetrue);
      if(Chart > 0)
      {
         // send a tick notification to the parent chart
         EventChartCustom(ChartMessage0price[0], _Symbol);
      }
      else
      {
         OnSymbolTick(_Symbolprice[0]);
      }
   }
  
   return rates_total;
}

En la instancia original del indicador, en la que el parámetro Chart es 0, llamamos directamente a una función especial, una especie de manejador de ticks multiactivos OnSymbolTick. En este caso, no es necesario llamar a EventChartCustom: aunque dicho mensaje seguirá llegando al gráfico y a esta copia del indicador, la transmisión tarda varios milisegundos y carga la cola en vano.

El único propósito de OnSymbolTick en esta demostración es imprimir el nombre del símbolo y el nuevo precio en el registro.

void OnSymbolTick(const string &symbolconst double price)
{
   Print(symbol" "DoubleToString(price,
      (int)SymbolInfoInteger(symbolSYMBOL_DIGITS)));
}

Por supuesto, la misma función es llamada desde el manejador OnChartEvent en la copia receptora (fuente) del indicador, siempre que nuestro mensaje haya sido recibido. Recordemos que el terminal llama a OnChartEvent sólo en la copia interactiva del indicador (aplicada al gráfico) y no aparece en aquellas copias que hemos creado «invisibles» mediante iCustom.

void OnChartEvent(const int id,
   const long &lparamconst double &dparamconst string &sparam)
{
   if(id >= CHARTEVENT_CUSTOM + Message)
   {
      OnSymbolTick(sparamdparam);
      // OR (if using custom event range):
      // OnSymbolTick(Symbols[id - CHARTEVENT_CUSTOM - Message], dparam);
   }
}

Podríamos evitar enviar, bien el precio o bien el nombre del símbolo en nuestro evento, ya que en el indicador inicial (que inició el proceso) se conoce la lista general de símbolos (que inició el proceso), y por lo tanto podríamos indicarle de alguna manera el número del símbolo de la lista. Esto podría hacerse en el parámetro lparam o, como se ha mencionado anteriormente, añadiendo un número a la constante de base del evento de usuario. A continuación, el indicador original, mientras recibe eventos, podría tomar un símbolo por índice del array y obtener toda la información sobre el último tick usando SymbolInfoTick, incluidos los distintos tipos de precios.

Vamos a ejecutar el indicador en el gráfico EURUSD con la configuración por defecto, incluyendo la lista de prueba «EURUSD,GBPUSD,XAUUSD,USDJPY». He aquí el registro:

16:45:48.745 (EURUSD,H1) Starting for chart 0, msg=0xFEED [EURUSD,GBPUSD,XAUUSD,USDJPY]
16:45:48.761 (GBPUSD,H1) Starting for chart 132358585987782873, msg=0xFEED []
16:45:48.761 (USDJPY,H1) Starting for chart 132358585987782873, msg=0xFEED []
16:45:48.761 (XAUUSD,H1) Starting for chart 132358585987782873, msg=0xFEED []
16:45:48.777 (EURUSD,H1) XAUUSD 1791.00
16:45:49.120 (EURUSD,H1) EURUSD 1.13068 *
16:45:49.135 (EURUSD,H1) USDJPY 115.797
16:45:49.167 (EURUSD,H1) XAUUSD 1790.95
16:45:49.167 (EURUSD,H1) USDJPY 115.796
16:45:49.229 (EURUSD,H1) USDJPY 115.797
16:45:49.229 (EURUSD,H1) XAUUSD 1790.74
16:45:49.369 (EURUSD,H1) XAUUSD 1790.77
16:45:49.572 (EURUSD,H1) GBPUSD 1.35332
16:45:49.572 (EURUSD,H1) XAUUSD 1790.80
16:45:49.791 (EURUSD,H1) XAUUSD 1790.80
16:45:49.791 (EURUSD,H1) USDJPY 115.796
16:45:49.931 (EURUSD,H1) EURUSD 1.13069 *
16:45:49.931 (EURUSD,H1) XAUUSD 1790.86
16:45:49.931 (EURUSD,H1) USDJPY 115.795
16:45:50.056 (EURUSD,H1) USDJPY 115.793
16:45:50.181 (EURUSD,H1) XAUUSD 1790.88
16:45:50.321 (EURUSD,H1) XAUUSD 1790.90
16:45:50.399 (EURUSD,H1) EURUSD 1.13066 *
16:45:50.727 (EURUSD,H1) EURUSD 1.13067 *
16:45:50.773 (EURUSD,H1) GBPUSD 1.35334

Observe que en la columna con (símbolo,marco temporal), que es la fuente del registro, vemos en primer lugar las instancias del indicador inicial en cuatro símbolos solicitados.

Tras el lanzamiento, el primer tick fue XAUUSD, no EURUSD. Otros símbolos aparecen con una intensidad aproximadamente igual, intercalados. Los ticks de EURUSD están marcados con asteriscos, para que pueda hacerse una idea de cuántos otros ticks se habrían perdido sin las notificaciones.

Las marcas de tiempo se han guardado en la columna de la izquierda como referencia.

Los lugares en los que coinciden dos precios de dos eventos consecutivos del mismo símbolo suelen indicar que el precio Ask ha cambiado (simplemente no lo mostramos aquí).

Un poco más adelante, tras estudiar la API de MQL5 de trading, aplicaremos el mismo principio para responder a los ticks multidivisa en los Asesores Expertos.