¿Dormir, o no dormir?

Sergey Gridnev | 4 mayo, 2016

Introducción

Durante su funcionamiento, un Asesor Experto a veces produce situaciones en las que tiene que hacer pausas entre las operaciones. Esto puede estar causado tanto por la necesita de alcanzar el requisito de retener cierto intervalo entre requisitos repetidos en el servidor de trade (en caso de errores de ejecución), y por esperar a algún evento en concreto (por ejemplo, a una reconexión con el servidor, a un contexto de trade gratis, etc.).

¿Dormir (Sleep()), o no dormir?

Para realizar pausas, MQL4 tiene una función que se llama Sleep(), que coge como parámetro el valor del intervalo de tiempo expresado en la cantidad de milisegundos. La función Sleep() detiene la ejecución del código del programa y permite que continúe sólo después de que haya pasado el intervalo de tiempo establecido.

En mi opinión, el uso de esta función tiene dos inconvenientes. Primero, el tiempo de máquina se utiliza de forma poco viable: durante la pausa de las acciones de un tipo, el programa podría realizar las acciones de otro tipo, independientemente de la anterior (por ejemplo, durante una pausa en trades, el programa podría realizar algunos cálculos, control de los ticks que llegan, etc.). Segundo, que es el más esencial, la función Sleep() no se puede llamar desde los indicadores personalizados (ver Documentación). ¡El lenguaje de programación debe utilizarse para programar!

Vamos a considerar cómo se realiza una pausa de diez segundos en un programa MQL4. A continuación se muestra un ejemplo de la función Sleep().

if ( /* condition that requires to hold a pause */ )
   Sleep(10000); // sleep for 10 seconds

// program block to be executed upon the expiry of the pause
// ...

Como alternativa, vamos a considerar un programa que se utiliza como una variable adicional. En caso de que necesite una pausa antes de realizar una acción, esa pausa debería iniciarla el valor de tiempo local de su final. Comparando el valor del tiempo local con el valor de su variable, puede establecer el hecho del final de la pausa. Para disfrutar de las ventajas de utilizar este método alternativo, debería organizar una ejecución cíclica del código del programa para el tiempo de espera del final de la pausa, para el que no es necesario configurar una pausa. Puede hacerlo utilizando un ciclo (por ejemplo, while), o con la correspondiente realización de la función start(), a la que pueden llamar los ciclos según el tick del ciclo entrante. La anterior alternativa es válida para los scripts personalizados, la última para los AEs e indicadores. Ambas se muestran a continuación.

Alternativa 1 (utilizando el ciclo "while").

int _time_waiting=0;
// ...
if ( ... ) // condition that requires to hold a pause
   _time_waiting = TimeLocal() + 10; // the pause ends in 10 seconds after the current local time
while ( TimeLocal() < _time_waiting )
{ // program block to be executed cyclically while waiting for the end of the pause
   // ...
}
// program block to be executed after the end of the pause
// ...

Alternativa 2 (utilizando el ciclo de llamadas de la función start()).

static int _time_waiting=0;
// ...
if ( ... ) // condition that requires to hold a pause
   _time_waiting = TimeLocal() + 10; // the pause ends in 10 seconds after the current local time
if ( TimeLocal() >= _time_waiting )
{ // program block to be executed after the end of the pause
   // ...
}
// program block to be executed at every tick, not related to waiting for the end of the pause
// ...

La diferencia principal entre las alternativas representadas, es que la alternativa 1 garantiza la ejecución del bloque del programa detenido durante el periodo de pausa, mientras que la alternativa 2 no. Esto está relacionado al hecho de que el flujo de ticks puede interrumpirse por algunos motivos, y no reiniciarse. Además, en la variable _time_waiting de la alternativa 2, se describe como estática, que preserva su valor entre las llamadas de la función start(), lo que no es necesario en la alternativa 1.

Alguna redundancia del código de las alternativas, compradas con la realización de la pausa a través de la llamada de la función Sleep(), es un pago por deshacerse del tiempo de parada. Sin embargo, ningún tiempo de parada genera el problema de los usos repetidos de la variable _time_waiting en el código del programa, porque no se puede iniciar un nuevo valor hasta que la pausa anterior haya terminado. Este problema se puede resolver al menos de dos maneras; la elección de una alternativa o de la otra, depende del estilo y preferencias del programador.

1) Puede utilizar su propia variable para cada grupo de condiciones para almacenar el tiempo de expiración de la pausa correspondiente; o

2) Puede describir el _time_waiting como una gama.

Creo que es razonable crear una pequeña librería con la realización de las funciones de temporización. Esta temporización debe tener una cantidad suficiente de variables de contadores y tener las funciones de inicialización, añadir y eliminar de los extractos controlados. Además, puede realizar tanto la cuenta atrás (el temporizador de wait) y un contador hacia arriba (stopwatch). En este artículo se adjunta un ejemplo de cómo realizar dicha librería (el archivo ExLib_Timer.mq4 y el texto de fuente que contiene). A continuación se ofrece un fichero de cabecera con los prototipos de las funciones realizadas:

#import "ExLib_Timer.ex4"
//
// Note.
// The counter identifier <CounterID> can take any values except "0",
// since "0" is reserved for the designation of nonactivated counters
// in a general array.
//
void timer_Init();
// start initialization
//
int timer_NumberTotal();
// request for the total amount of timer counters
// Return:
//    total amount of counters
//
int timer_NumberUsed();
// request for the amount of activated counters
// Return:
//    amount of activated counters
//
int timer_NumberFree();
// request for the amount of nonactivated (free) counters
// Return:
//    amount of free counters
//
void timer_ResetAll();
// resetting (zeroing) all counters
//
bool timer_Reset(int CounterID);
// resetting (zeroing) a counter by ID
// Parameters:
//    <CounterID> - counter identifier
// Return:
//    true  - resetted
//    false - no counter with such ID is found
//
void timer_DeleteAll();
// deletion of all counters
//
bool timer_Delete(int CounterID);
// deleting a counter by ID
// Parameters:
//    <CounterID> - counter identifier
// Return:
//    true  - deleted
//    false - no counter with such ID is found
//

bool timer_StartWaitable(int CounterID, int timeslice);
// starting a counter of the "wait timer" type
// Parameters:
//    <CounterID> - counter identifier (if it is not available, a counter is added)
//    <timeslice> - wait period (seconds)
// Return:
//    true  - started
//    false - no free counters
//
bool timer_StartStopwatch(int CounterID);
// starting a counter of the "stopwatch" type
// Parameters:
//    <CounterID> - counter identifier (if it is not available, a counter is added)
// Return:
//    true  - started
//    false - no free counters
//
int timer_GetCounter(int CounterID);
// request for the indications of the counter with ID <CounterID>
// Return:
//    for a counter of the "stopwatch" type, it is the amount of seconds elapsed since the start
//    for a counter of the "wait timer" type, it is the amount of seconds left till the end of wait period
// Note:
//    in case of no counter with ID <CounterID>, -1 is returned
//
int timer_GetType(int CounterID);
// request for a counter with ID <CounterID>
// Parameters:
//    <CounterID> - counter identifier
// Return:
//     1 - counter of the "stopwatch" type
//    -1 - counter of the "wait timer" type
//     0 - no counter with such ID is found
//
int timer_GetCounterIDbyPOS(int CounterPOS);
// request for a counter ID by its position among the activated ones
// Parameters:
//    <CounterPOS> - the number of the counter position among the activated ones
// Return:
//    counter identifier or "0" if a nonexisting position is specified
// Note:
//    <CounterPOS> can take the values ranging from 0 up to the amount of the activated counters,
//    returned by function timer_NumberUsed(). Otherwise, it returns 0
//
#import

Las diferencias entre utilizar la llamada de la función Sleep() y la realización alternativa de una pausa haciendo entradas en un registro, se demuestra claramente con un pequeño Asesor Experto, cuyo texto se ofrece más abajo. La realización de la alternativa de la pausa se configura por el parámetro de entrada de Use_Sleep: "true" para utilizar Sleep(); o por "false" para rechazar el uso de Sleep().

#include "include/ExLib_Timer.mqh"
// --
//---- input parameters
extern bool Use_Sleep = false;
// =true  - using function "Sleep()"
// =false - using a timer
//+------------------------------------------------------------------+
//| expert initialization function                                   |
//+------------------------------------------------------------------+
int init()
{
   timer_Init();
   if ( Use_Sleep )
      Print("init(). * Using function Sleep() *");
   else
      Print("init(). * Using a timer *");
   timer_StartWaitable(101, 10);
   timer_StartStopwatch(102);
   return(0);
}
//+------------------------------------------------------------------+
//| expert deinitialization function                                 |
//+------------------------------------------------------------------+
int deinit()
{
   timer_DeleteAll();
   Comment("");
   return(0);
}
//+------------------------------------------------------------------+
//| expert start function                                            |
//+------------------------------------------------------------------+
int start()
{
   Comment("Waitable:",timer_GetCounter(101)," / Stopwatch:",timer_GetCounter(102));
   Print("start() - initial block -");
// --
   if ( Use_Sleep )
   {
      //  - using function "Sleep()" -
      Sleep( 10000 ); // sleep for 10 seconds
      // program block to be executed after the pause ends
      Print("start() - block to be executed after the pause -");
   }
   else
   {
      //  - using a timer -
      if ( timer_GetType(101) == 0 )
         timer_StartWaitable(101, 10); // sleep for 10 seconds
         
      if ( timer_GetCounter(101) == 0 )
      {
         // program block to be executed after the pause ends
         timer_Delete(101);
         Print("start() - block to be executed after the pause -");
      }
   }
// --
   Print("start() - end block -");
   return(0);
}

Además del AE de arriba, hay dos AEs demo más adjuntos a este artículo.
Ex_Timer_OrderLimits_TrailByTime.mq4 demuestra cómo realizar trailing SL y TP por tiempo, y
Ex_Timer_OrderSend_wMinTimeLimit.mq4 demuestra cómo colocar una orden menos frecuente que una vez en el periodo establecido en el gráfico actual.

¿Inconvenientes menores, o "La manzana podrida"?

Desafortunadamente, dicha librería no puede remplazar por completo a Sleep(). Hay dos inconvenientes que deben ponerse en conocimiento del lector.

Primero, al utilizar Sleep(), la ejecución del programa continua inmediatamente después de que termine la pausa. En caso de utilizar un temporizador, sólo continúa después de la llegada del primer tick tras la pausa. Esto puede resultar en que el bloque retrasado se ejecute demasiado tarde o, como se mencionó arriba, no se ejecute (por ejemplo, en caso de error crítico en el canal del enlace).

Segundo, la función Sleep() permite establecer una pausa con un paso de 0,1 s, mientras que el temporizador sólo las hace de 1 s.

Conclusiones

Como ocurre a menudo, las ventajas de la realización de la alternativa de pausa descrita arriba, sobre el uso de Sleep() no son infalibles, hay puntos más sutiles. Todo tiene sus fortalezas y sus debilidades, dependiendo de los propósitos que se tengan. Sleep(), o no Sleep(), qué sacrificar y qué preferir: no hay que decidir esto en general, sino dentro del marco del proyecto a realizar.

Archivos adjuntos:

Función Librería:

Ejemplo de uso:

¡Importante! Los AEs de "Ex_Timer_OrderLimits_TrailByTime" y "Ex_Timer_OrderSend_wMinTimeLimit" se consideran únicamente para su uso en cuentas demo.