Evento temporizador: OnTimer

El evento OnTimer es uno de los eventos estándar admitidos por los programas MQL5 (véase la sección Visión general de las funciones de gestión de eventos). Para recibir eventos de temporizador en el código del programa, debe describir una función con el siguiente prototipo:

void OnTimer(void)

El evento OnTimer es generado periódicamente por el terminal cliente para un Asesor Experto o un indicador que ha activado el temporizador utilizando la función EventSetTimer o EventSetMillisecondTimer (véase la sección siguiente).

¡Atención! En indicadores dependientes creados llamando a iCustom o IndicatorCreate desde otros programas, el temporizador no funciona, y el evento OnTimer no se genera. Esta es una limitación arquitectónica de MetaTrader 5.

Debe entenderse que la presencia de un manejador OnTimer y un temporizador habilitado no hace que el programa MQL sea multihilo. No se asigna más de un hilo por programa MQL (un indicador puede incluso compartir un hilo con otros indicadores en el mismo símbolo), por lo que la llamada de OnTimer y otros manejadores siempre se produce secuencialmente, de acuerdo con la cola de eventos. Si uno de los manejadores, incluido OnTimer, va a iniciar cálculos largos, esto suspenderá la ejecución de todos los demás eventos y secciones del código del programa.

Si necesita organizar el procesamiento de datos en paralelo, debe ejecutar varios programas MQL de forma simultánea (quizás, instancias del mismo programa en diferentes gráficos u objetos gráficos) e intercambiar comandos y datos entre ellos utilizando su propio protocolo; por ejemplo, utilizando eventos personalizados.

Como ejemplo, vamos a crear clases que pueden organizar varios temporizadores lógicos en un programa. Los periodos de todos los temporizadores lógicos se establecerán como un multiplicador del periodo base, es decir, el periodo de un único temporizador de hardware que suministra eventos al manejador estándar OnTimer. En este manejador, debemos llamar a cierto método de nuestra nueva clase MultiTimer que manejará todos los temporizadores lógicos.

void OnTimer()
{
   // call the MultiTimer method to check and call dependent timers when needed
   MultiTimer::onTimer();
}

La clase MultiTimer y las clases relacionadas de temporizadores individuales se combinarán en un archivo, MultiTimer.mqh.

La clase base para los temporizadores de trabajo será TimerNotification. En términos estrictos, podría tratarse de una interfaz, pero es conveniente volcar en ella algunos detalles de la implementación general: en concreto, almacenar la lectura del contador chronometer, mediante el cual nos aseguraremos de que el temporizador se dispara con un cierto multiplicador del periodo relativo del temporizador principal, así como un método para comprobar el momento en que el temporizador debe dispararse isTimeCome. Por eso TimerNotification es una clase abstracta. Carece de la implementación de dos métodos virtuales: notify, para las acciones cuando se dispara el temporizador, y getInterval, para obtener un multiplicador que determina el periodo de un temporizador concreto en relación con el periodo del temporizador principal.

class TimerNotification
{
protected:
   int chronometer// counter of timer checks (isTimeCome calls)
public:
   TimerNotification(): chronometer(0)
   {
   }
   
   // timer work event
   // pure virtual method, it is required to be described in the heirs
   virtual void notify() = 0;
   // returns the period of the timer (it can be changed on the go)
   // pure virtual method, it is required to be described in the heirs
   virtual int getInterval() = 0;
   // check if it's time for the timer to fire, and if so, call notify
   virtual bool isTimeCome()
   {
      if(chronometer >= getInterval() - 1)
      {
         chronometer = 0// reset the counter
         notify();        // notify application code
         return true;
      }
      
      ++chronometer;
      return false;
   }
};

Toda la lógica se proporciona en el método isTimeCome. Cada vez que se llama, el contador chronometer se incrementa, y si alcanza la última iteración según el método getInterval, se llama al método notify para notificar el código de la aplicación.

Por ejemplo, si el temporizador principal se inicia con un periodo de 1 segundo (EventSetTimer(1)), entonces el objeto hijo TimerNotification,, que devolverá 5 de getInterval, recibirá llamadas a su método notify cada 5 segundos.

Como ya hemos dicho, dichos objetos temporizadores serán gestionados por el objeto administrador MultiTimer. Sólo necesitamos un objeto de este tipo. Por lo tanto, su constructor se declara protegido y se crea una única instancia de forma estática dentro de la clase.

class MultiTimer
{
protected:
   static MultiTimer _mainTimer;
   
   MultiTimer()
   {
   }
   ...

Dentro de esta clase, organizamos el almacenamiento del array de objetos TimerNotification (veremos cómo se rellena dentro de pocos párrafos). Una vez que tenemos el array podemos escribir fácilmente el método checkTimers que hace un bucle a través de todos los temporizadores lógicos. Para el acceso externo, este método está duplicado por el método estático público onTimer, que ya hemos visto en el manejador global OnTimer. Como la única instancia del administrador se crea estáticamente, podemos acceder a ella desde un método estático.

   ...
   TimerNotification *subscribers[];
   
   void checkTimers()
   {
      int n = ArraySize(subscribers);
      for(int i = 0i < n; ++i)
      {
         if(CheckPointer(subscribers[i]) != POINTER_INVALID)
         {
            subscribers[i].isTimeCome();
         }
      }
   }
   
public:
   static void onTimer()
   {
      _mainTimer.checkTimers();
   }
   ...

El objeto TimerNotification se añade al arrayz subscribers mediante el método bind.

   void bind(TimerNotification &tn)
   {
      int in = ArraySize(subscribers);
      for(i = 0i < n; ++i)
      {
         if(subscribers[i] == &tnreturn// there is already such an object
         if(subscribers[i] == NULLbreak// found an empty slot
      }
      if(i == n)
      {
         ArrayResize(subscribersn + 1);
      }
      else
      {
         n = i;
      }
      subscribers[n] = &tn;
   }

El método está protegido contra la adición repetida del objeto y, si es posible, el puntero se coloca en un elemento vacío del array, si lo hay, lo que elimina la necesidad de ampliar el array. Pueden aparecer elementos vacíos en un array si se ha eliminado alguno de los objetos de TimerNotification mediante el método unbind (se pueden utilizar temporizadores de forma ocasional).

   void unbind(TimerNotification &tn)
   {
      const int n = ArraySize(subscribers);
      for(int i = 0i < n; ++i)
      {
         if(subscribers[i] == &tn)
         {
            subscribers[i] = NULL;
            return;
         }
      }
   }

Tenga en cuenta que el administrador no se apropia del objeto temporizador y no intenta llamar a delete. Si va a registrar objetos temporizadores asignados dinámicamente en el administrador, puede añadir el siguiente código dentro de if antes de la puesta a cero:

            if(CheckPointer(subscribers[i]) == POINTER_DYNAMICdelete subscribers[i];

Ahora queda entender cómo podemos organizar convenientemente las llamadas a bind/unbind, para no cargar el código de la aplicación con estas operaciones utilitarias. Si lo hace «manualmente», es fácil que olvide crear o, por el contrario, borrar el temporizador en algún sitio.

Vamos a desarrollar la clase SingleTimer derivada de TimerNotification, en la que implementamos las llamadas bind y unbind del constructor y destructor, respectivamente. Además, describimos en él la variable multiplier para almacenar el periodo del temporizador.

   class SingleTimerpublic TimerNotification
   {
   protected:
      int multiplier;
      MultiTimer *owner;
   
   public:
      // creating a timer with the specified base period multiplier, optionally paused
      // automatically register the object in the manager
      SingleTimer(const int mconst bool paused = false): multiplier(m)
      {
         owner = &MultiTimer::_mainTimer;
         if(!pausedowner.bind(this);
      }
   
      // automatically disconnect the object from the manager
      ~SingleTimer()
      {
         owner.unbind(this);
      }
   
      // return timer period
      virtual int getInterval() override 
      {
         return multiplier;
      }
   
      // pause this timer
      virtual void stop()
      {
         owner.unbind(this);
      }
   
      // resume this timer
      virtual void start()
      {
         owner.bind(this);
      }
   };

El segundo parámetro del constructor (paused) permite crear un objeto, pero no iniciar el temporizador inmediatamente. Dicho temporizador retardado puede activarse mediante el método start.

El esquema de suscripción de unos objetos a eventos en otros es uno de los patrones de diseño populares en la POO y se denomina «publisher/subscriber» (publicador/suscriptor).

Es importante señalar que esta clase también es abstracta porque no implementa el método notify. Basándonos en SingleTimer, vamos a describir las clases de temporizadores con funcionalidad adicional.

Empecemos por la clase CountableTimer. Permite especificar cuántas veces debe dispararse, tras lo cual se detendrá automáticamente. Con ella, en concreto, es fácil organizar una única acción diferida. El constructor CountableTimer tiene parámetros para establecer el periodo del temporizador, la bandera de pausa y el número de reintentos. Por defecto, el número de repeticiones no está limitado, por lo que esta clase se convertirá en la base de la mayoría de los temporizadores de aplicación.

class CountableTimerpublic MultiTimer::SingleTimer
{
protected:
   const uint repeat;
   uint count;
   
public:
   CountableTimer(const int mconst uint r = UINT_MAXconst bool paused = false):
      SingleTimer(mpaused), repeat(r), count(0) { }
   
   virtual bool isTimeCome() override
   {
      if(count >= repeat && repeat != UINT_MAX)
      {
         stop();
         return false;
      }
      // delegate the time check to the parent class,
      // increment our counter only if the timer fired (returned true)
      return SingleTimer::isTimeCome() && (bool)++count;
   }
   // reset our counter on stop
   virtual void stop() override
   {
      SingleTimer::stop();
      count = 0;
   }
 
   uint getCount() const
   {
      return count;
   }
   
   uint getRepeat() const
   {
      return repeat;
   }
};

Para utilizar CountableTimer tenemos que describir la clase derivada en nuestro programa de la siguiente manera:

// MultipleTimers.mq5 
class MyCountableTimerpublic CountableTimer
{
public:
   MyCountableTimer(const int sconst uint r = UINT_MAX):
      CountableTimer(sr) { }
   
   virtual void notify() override
   {
      Print(__FUNCSIG__multiplier" "count);
   }
};

En esta implementación del método notify, sólo registramos el período del temporizador y el número de veces que se disparó. Por cierto, este es un fragmento del indicador MultipleTimers.mq5, que utilizaremos como ejemplo de trabajo.

Llamemos a la segunda clase derivada de SingleTimer FunctionalTimer. Su propósito es proporcionar una implementación sencilla del temporizador para aquellos a los que les gusta el estilo funcional de programación y no les apetece escribir clases derivadas. El constructor de la clase FunctionalTimer tomará, además del punto, un puntero a una función de un tipo especial, TimerHandler.

// MultiTimer.mqh
typedef bool (*TimerHandler)(void);
   
class FunctionalTimerpublic MultiTimer::SingleTimer
{
   TimerHandler func;
public:
   FunctionalTimer(const int mTimerHandler f):
      SingleTimer(m), func(f) { }
      
   virtual void notify() override
   {
      if(func != NULL)
      {
         if(!func())
         {
            stop();
         }
      }
   }
};

En esta implementación del método notify, el objeto llama a la función por el puntero. Con una clase de este tipo, podemos definir una macro que, cuando se coloca delante de un bloque de sentencias entre llaves, lo «convertirá» en el cuerpo de la función del temporizador.

// MultiTimer.mqh
#define OnTimerCustom(POnTimer##P(); \
FunctionalTimer ft##P(POnTimer##P); \
bool OnTimer##P()

Entonces, en el código de la aplicación, puede escribir algo así:

// MultipleTimers.mq5
bool OnTimerCustom(3)
{
   Print(__FUNCSIG__);
   return true;        // continue the timer
}

Esta construcción declara un temporizador con un período de 3 y un conjunto de instrucciones dentro de paréntesis (aquí, sólo la impresión a un registro). Si esta función devuelve false, este temporizador se detendrá.

Vamos a analizar más detenidamente el indicador MultipleTimers.mq5. Como no proporciona visualización, especificaremos el número de diagramas igual a cero.

#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots   0

Para utilizar las clases de temporizadores lógicos, incluimos el archivo de encabezado MultiTimer.mqh y añadimos una variable de entrada para el periodo base (global) del temporizador.

#include <MQL5Book/MultiTimer.mqh>
   
input int BaseTimerPeriod = 1;

El temporizador base se inicia en OnInit.

void OnInit()
{
   Print(__FUNCSIG__" "BaseTimerPeriod" Seconds");
   EventSetTimer(BaseTimerPeriod);
}

Recordemos que el funcionamiento de todos los temporizadores lógicos está garantizado por la interceptación del evento global OnTimer.

void OnTimer()
{
   MultiTimer::onTimer();
}

Además de la clase de aplicación del temporizador MyCountableTimer anterior, vamos a describir otra clase del temporizador suspendido MySuspendedTimer.

class MySuspendedTimerpublic CountableTimer
{
public:
   MySuspendedTimer(const int sconst uint r = UINT_MAX):
      CountableTimer(srtrue) { }
   virtual void notify() override
   {
      Print(__FUNCSIG__multiplier" "count);
      if(count == repeat - 1// execute last time
      {
         Print("Forcing all timers to stop");
         EventKillTimer();
      }
   }
};

Un poco más abajo veremos cómo empieza. También es importante señalar aquí que después de alcanzar el número especificado de operaciones, este temporizador apagará todos los temporizadores llamando a EventKillTimer.

Ahora vamos a mostrar cómo (en el contexto global) se describen los objetos de diferentes temporizadores de estas dos clases.

MySuspendedTimer st(15);
MyCountableTimer t1(2);
MyCountableTimer t2(4);

El temporizador st de la clase MySuspendedTimer tiene periodo 1 (1*BaseTimerPeriod) y debe detenerse después de 5 operaciones.

Los temporizadores t1 y t2 de la clase MyCountableTimer tienen periodos 2 (2 * BaseTimerPeriod) y 4 (4 * BaseTimerPeriod), respectivamente. Con el valor por defecto BaseTimerPeriod = 1 todos los periodos representan segundos. Estos dos temporizadores se ponen en marcha inmediatamente después del inicio del programa.

También crearemos dos temporizadores de estilo funcional.

bool OnTimerCustom(5)
{
   Print(__FUNCSIG__);
   st.start();         // start delayed timer
   return false;       // and stop this timer object
}
   
bool OnTimerCustom(3)
{
   Print(__FUNCSIG__);
   return true;        // this timer keeps running
}

Tenga en cuenta que OnTimerCustom5 sólo tiene una tarea: 5 periodos después del inicio del programa, necesita iniciar un temporizador retardado st y terminar su propia ejecución. Teniendo en cuenta que el temporizador retardado debe desactivar todos los temporizadores después de 5 periodos, obtenemos 10 segundos de actividad del programa con los ajustes por defecto.

El temporizador OnTimerCustom3 debe activarse tres veces durante este periodo.

Así, tenemos 5 temporizadores con diferentes períodos: 1, 2, 3, 4, 5 segundos.

Analicemos un ejemplo de lo que se envía al registro (las marcas de tiempo se muestran esquemáticamente a la derecha).

                                                // time
17:08:45.174  void OnInit() 1 Seconds             |
17:08:47.202  void MyCountableTimer::notify()2 0    |
17:08:48.216  bool OnTimer3()                        |
17:08:49.230  void MyCountableTimer::notify()2 1      |
17:08:49.230  void MyCountableTimer::notify()4 0      |
17:08:50.244  bool OnTimer5()                          |
17:08:51.258  void MyCountableTimer::notify()2 2        |
17:08:51.258  bool OnTimer3()                           |
17:08:51.258  void MySuspendedTimer::notify()1 0        |
17:08:52.272  void MySuspendedTimer::notify()1 1         |
17:08:53.286  void MyCountableTimer::notify()2 3          |
17:08:53.286  void MyCountableTimer::notify()4 1          |
17:08:53.286  void MySuspendedTimer::notify()1 2          |
17:08:54.300  bool OnTimer3()                              |
17:08:54.300  void MySuspendedTimer::notify()1 3           |
17:08:55.314  void MyCountableTimer::notify()2 4            |
17:08:55.314  void MySuspendedTimer::notify()1 4            |
17:08:55.314  Forcing all timers to stop                    |

El primer mensaje del temporizador de dos segundos llega, como era de esperar, unos 2 segundos después del inicio (decimos «unos» porque el temporizador de hardware tiene una limitación en la precisión y, además, otra carga del ordenador afecta a la ejecución). Un segundo después, el temporizador de tres segundos se dispara por primera vez. El segundo golpe del temporizador de dos segundos coincide con la primera salida del temporizador de cuatro segundos. Tras una única ejecución del temporizador de cinco segundos, los mensajes del temporizador de un segundo comienzan a aparecer en el registro regularmente (su contador aumenta de 0 a 4). En su última iteración, detiene todos los temporizadores.