English Русский Deutsch 日本語 Português
preview
Desarrollo de un sistema de repetición (Parte 31): Proyecto Expert Advisor — Clase C_Mouse (V)

Desarrollo de un sistema de repetición (Parte 31): Proyecto Expert Advisor — Clase C_Mouse (V)

MetaTrader 5Probador | 1 marzo 2024, 09:59
255 0
Daniel Jose
Daniel Jose

Introducción

En el artículo anterior Desarrollo de un sistema de repetición (Parte 30): Proyecto Expert Advisor — Clase C_Mouse (IV), demostré cómo puedes modificar, añadiendo o adecuando a tu estilo un sistema de clases, con el fin de probar un recurso o modelo nuevo. De esta manera evitarás dependencias en tu código principal, manteniéndolo siempre robusto, estable y confiable, ya que cualquier nuevo recurso añadido será siempre colocado dentro del sistema principal solo después de que ya esté completamente adecuado el modelo que estarás creando. La gran cuestión en desarrollar un sistema de repetición/simulador, y tal vez esta sea de hecho la cuestión que hace este trabajo tan desafiante, es crear mecanismos que sean lo más cercano, por no decir idéntico, al sistema que usaremos cuando estemos en una cuenta REAL. No tiene mucho sentido tener un sistema con el fin de crear o ejecutar una repetición/simulación, si, en el momento en que se use la cuenta real, no venga a tener los mismos recursos presentes.

Al observar el sistema de la clase C_Mouse y de las clases de estudio mostradas en los artículos anteriores, se puede notar que durante el uso en el mercado real, sea cuenta demo o cuenta real, el cronómetro siempre le dirá cuándo la próxima barra iniciará. Pero al usar el sistema de repetición/simulador, no contamos con esto. Lo que nos surge es un mensaje. Este tipo de quiebre de simetría puede a primera vista parecer ser algo sin mucho valor. Pero si permites que cosas sin valor se vayan acumulando, sin corregirlas o eliminarlas, al final tendrás un montón de basura totalmente inútil y que solo te estorbará en las cuestiones que de hecho necesitas resolver. Desarrollar una manera de poner un cronómetro, de modo que durante una repetición/simulación, éste pueda decirnos cuánto tiempo falta, puede parecer a primera vista una tarea simple y de rápida solución. Muchos simplemente intentarían adaptar y usar el mismo sistema que se utiliza cuando tenemos el servidor comercial a nuestro lado. Pero aquí reside un punto que muchos quizás no consideran al pensar en tal solución. Cuando estás haciendo una repetición, y esto para no hablar del hecho de la simulación, el reloj no funciona de la misma manera. Esto por algunas razones que puedo enumerar de inmediato:

  • La repetición siempre se referirá al pasado. Así, el reloj de la plataforma, o del computador, no es de ninguna manera adecuado para marcar el paso del tiempo;
  • Cuando estamos ejecutando una repetición/simulación, podemos adelantar, pausar o incluso retroceder el tiempo. Este último caso ya no es posible, y esto viene de larga data y por varias razones que fueron explicadas en el transcurso de los artículos anteriores. Aún así, podrás adelantar o pausar el sistema. Entonces, cronometrar el tiempo, de la misma manera que hacemos cuando el servidor comercial está de nuestro lado, deja de ser adecuado.

Para que tengas una idea de con qué estamos realmente lidiando y lo complicado que puede llegar a ser poner un cronómetro en el sistema de repetición/simulación, mira la figura 01.

Figura 01

Figura 01 - Cronómetro en un mercado real

En esta figura 01, vemos cómo funciona el cronómetro que indica en qué momento surgirá una nueva barra en el gráfico. Esto de forma muy resumida. Noten que, de vez en cuando, tendremos la generación de un evento OnTime. Este disparará el evento Update que hará la actualización del valor del cronómetro. Así podemos visualizar el tiempo restante para que surja la nueva barra. Pero para que la función Update sepa cuál es el valor a ser presentado, ella pregunta a la función GetBarTime cuánto tiempo aún tardará en aparecer la barra. Y GetBarTime hará uso de la función TimeCurrent que no se ejecuta en el servidor, sino que realmente capturará la hora local en el servidor. De esta manera, podemos saber cuánto tiempo ha pasado desde que el servidor disparó la última barra y sobre este valor podemos calcular cuánto tiempo aún resta hasta que la nueva barra surja. Esta es la parte fácil de la historia, ya que no necesitamos preocuparnos si el sistema está pausado o ha avanzado una cantidad determinada de tiempo. Esto sucederá cuando usamos un activo que tiene datos provenientes directamente del servidor comercial. Cuando estamos trabajando con repetición/simulación, la cosa se vuelve mucho más complicada. El gran problema no es el hecho de que estemos manejando repetición/simulación. El problema es idear alguna forma de que, cuando estemos haciendo una repetición o simulación, podamos burlar la llamada TimeCurrent. Ya que es en este punto donde todo el problema ocurre. Pero esto debe ser hecho con el mínimo de modificaciones. Solo queremos burlar el sistema de la llamada TimeCurrent. Sin embargo, cuando estemos ante un servidor, queremos que el sistema funcione como se mostró en la figura 01.

Afortunadamente existe una manera, usando MetaTrader 5, de hacer esto, con el mínimo de dificultad posible y causando un número considerablemente bajo de modificaciones o adiciones al código ya implementado. Mostrar cómo podemos hacer esto será el tema de este artículo.


Planificación

Planificar cómo hacer esto tal vez sea la parte simple de la historia, ya que simplemente vamos a enviar el valor de tiempo calculado por el servicio de repetición/simulador al sistema. Esta es la parte fácil. Bastará con usar una variable global de terminal para esto, y listo. Ya he mostrado en esta secuencia cómo usamos tales variables, esto cuando presenté el indicador de control con el fin de poder decir al servicio qué es lo que el usuario está deseando hacer. Muchos piensan y creen que podemos solo enviar datos del tipo double mediante estas variables. Pero estas mismas personas se olvidan de un hecho: los números binarios son solo números binarios. Ellos de ninguna manera representan algún tipo de información que no sea ceros y unos. Así que puedes transmitir cualquier tipo de información por estas variables globales de terminal. Siempre y cuando, claro, consigas colocar los bits de una manera lógica para después poder recomponer la información cuando sea necesario saber qué fue transmitido. Afortunadamente, el tipo DateTime, a diferencia de lo que muchos piensan, es en realidad un valor ulong. Utilizamos 64 bits, es decir, 8 bytes de información son necesarios para acomodar un valor DateTime, donde tendremos la representación de una fecha y hora completas, incluyendo año, mes, día, hora, minuto y segundos. Eso es todo lo que necesitamos para sortear la llamada TimeCurrent, ya que la misma utiliza y retorna este mismo valor en 8 bytes. Dado que el tipo Double utiliza precisamente 64 bits para transferir la información dentro de la plataforma, ahí está nuestra solución.

Pero no todo es tan sencillo. Tenemos algunos problemillas que serán mejor comprendidos durante la explicación de la implementación de este sistema. A pesar de ser bastante simple y fácil de ser construido, tiene algunos pequeños contratiempos relacionados con la manera en que el servicio tendrá que ser modificado para poder suministrar a la clase con la información que podrá ser presentada en el cronómetro. Al final de la construcción, tendremos el siguiente resultado, visto en la figura 02.

Figura 02

Figura 02 - Cronómetro genérico.

Este cronómetro genérico será capaz de responder adecuadamente y de manera totalmente transparente, para que el usuario tenga una experiencia en la repetición/simulación muy cercana a la que tendría en una cuenta demo/real. Es justamente este el propósito al pensar en crear tal sistema: que la experiencia sea la misma, para que el usuario sepa exactamente cómo actuar, sin necesidad de reaprender desde cero a usar el sistema.


Implementación

Esta fase es la parte más interesante de este sistema. En este momento veremos cómo podemos sortear la llamada TimeCurrent de manera adecuada. El usuario no debe percibir si está usando una repetición o si está en contacto con el servidor. Para comenzar a implementar el sistema, necesitamos añadir, como debe haber quedado claro en los temas anteriores, una nueva variable global de terminal. Al mismo tiempo, necesitamos medios correctos para enviar la información de DateTime vía variable Double. Para esto, utilizaremos el archivo de cabecera Interprocess.mqh. Añadimos los siguientes puntos:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#define def_SymbolReplay                "RePlay"
#define def_GlobalVariableReplay        def_SymbolReplay + " Infos"
#define def_GlobalVariableIdGraphics    def_SymbolReplay + " ID"
#define def_GlobalVariableServerTime    def_SymbolReplay + " Time"
#define def_MaxPosSlider                400
#define def_ShortName                   "Market " + def_SymbolReplay
//+------------------------------------------------------------------+
union u_Interprocess
{
   union u_0
   {
      double  df_Value;       // Value of the terminal global variable...
      ulong   IdGraphic;      // Contains the Graph ID of the asset...
   }u_Value;
   struct st_0
   {
      bool    isPlay;         // Indicates whether we are in Play or Pause mode...
      bool    isWait;         // Tells the user to wait...
      ushort  iPosShift;      // Value between 0 and 400...
   }s_Infos;
   datetime   ServerTime;
};
//+------------------------------------------------------------------+

En este punto definimos el nombre de la variable global de terminal que será usada para comunicación. Ya en este, definimos la variable que será usada para acceder a los datos en el formato datetime. Hecho esto, pasamos a la clase C_Replay, y vamos directamente a añadir en el destructor de la clase, la siguiente línea:

~C_Replay()
   {
      ArrayFree(m_Ticks.Info);
      ArrayFree(m_Ticks.Rate);
      m_IdReplay = ChartFirst();
      do
      {
         if (ChartSymbol(m_IdReplay) == def_SymbolReplay)
         ChartClose(m_IdReplay);
      }while ((m_IdReplay = ChartNext(m_IdReplay)) > 0);
      for (int c0 = 0; (c0 < 2) && (!SymbolSelect(def_SymbolReplay, false)); c0++);
      CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX);
      CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX);
      CustomSymbolDelete(def_SymbolReplay);
      GlobalVariableDel(def_GlobalVariableReplay);
      GlobalVariableDel(def_GlobalVariableIdGraphics);
      GlobalVariableDel(def_GlobalVariableServerTime);
      Print("Finished replay service...");
   }

Añadiendo esta línea, garantizamos que cuando el servicio de repetición/simulación sea cerrado, removeremos la variable global de terminal, responsable de pasar el valor de tiempo dentro del cronómetro. Ahora, necesitamos hacer que esta variable global de terminal sea de hecho creada en el momento correcto. No debemos de ninguna manera crear la misma durante el procedimiento de bucle, ya que este puede ser disparado en momentos diferentes. Y el sistema, tan pronto como entre, necesita ya tener acceso al contenido de la variable global de terminal. De cierta forma, consideré hacer uso de un sistema de creación similar al del indicador controlador. Pero ya que en este momento podemos tener o no el Expert Advisor presente en el gráfico, vamos, al menos por ahora, a hacer que la variable de terminal responsable del cronómetro sea creada por el servicio. Así tendremos un control mínimamente adecuado a lo que estamos haciendo. Quizás en el futuro esto pueda cambiar. Entonces, el punto de creación se ve en el código abajo:

bool ViewReplay(ENUM_TIMEFRAMES arg1)
   {
#define macroError(A) { Print(A); return false; }
      u_Interprocess info;
                                
      if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0)
         macroError("Asset configuration is not complete, it remains to declare the size of the ticket.");
      if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0)
         macroError("Asset configuration is not complete, need to declare the ticket value.");
      if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0)
         macroError("Asset configuration not complete, need to declare the minimum volume.");
      if (m_IdReplay == -1) return false;
      if ((m_IdReplay = ChartFirst()) > 0) do
      {
         if (ChartSymbol(m_IdReplay) == def_SymbolReplay)
         {
            ChartClose(m_IdReplay);
            ChartRedraw();
         }
      }while ((m_IdReplay = ChartNext(m_IdReplay)) > 0);
      Print("Waiting for [Market Replay] indicator permission to start replay ...");
      info.ServerTime = m_Ticks.Info[m_ReplayCount].time;
      CreateGlobalVariable(def_GlobalVariableServerTime, info.u_Value.df_Value);
      info.u_Value.IdGraphic = m_IdReplay = ChartOpen(def_SymbolReplay, arg1);
      ChartApplyTemplate(m_IdReplay, "Market Replay.tpl");
      CreateGlobalVariable(def_GlobalVariableIdGraphics, info.u_Value.df_Value);
      while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(750);

      return ((!_StopFlag) && (ChartSymbol(m_IdReplay) != ""));
#undef macroError
   }

En medio de todo este enredo de líneas, tenemos este punto que inicializa el valor de la variable. Esto es para que el sistema ya tenga un valor con el cual trabajar, o para que podamos probar si la repetición/simulador está de hecho sincronizada. Justo después tenemos esta llamada que realiza la creación e inicialización de la variable global de terminal. Este código de inicialización puede ser visto justo abajo:

void CreateGlobalVariable(const string szName, const double value)
   {
      GlobalVariableDel(szName);
      GlobalVariableTemp(szName);     
      GlobalVariableSet(szName, value);
   }

Nota que el código de creación e inicialización es muy directo. Está montado de manera que, si la plataforma es cerrada y se hace la petición para almacenar las variables globales de terminal, las variables usadas en el sistema de repetición/simulador no serán almacenadas. Esto no es ningún tipo de problema, ya que no queremos ni necesitamos que tales valores sean almacenados y recuperados después. Con esto, ya podemos comenzar a considerar la lectura de la variable por el cronómetro. Pero si esto se hace en este momento, el valor contenido allí siempre será el mismo, no teniendo necesariamente ninguna utilidad práctica para nosotros. Sería como si el servicio de repetición/simulador estuviera pausado. Queremos que, en el momento en que esté activo, es decir, en modo play, el cronómetro se mueva. Así tendremos una simulación de lo que serían los requerimientos a la función TimeCurrent, donde recibimos los datos pertinentes de la hora y fecha encontrados en el servidor. Para que esto ocurra, necesitamos que cada segundo, aproximadamente, el valor de la variable global cambie. Lo correcto es tener un temporizador para esto. Pero ya que no podemos darnos el lujo de hacer esto, necesitamos de otro medio para generar tal cambio en el valor de la variable global,

para intentar cronometrar las cosas. Vamos a necesitar hacer algunas adiciones aún en la clase C_Replay. Estas pueden ser vistas en el código abajo:

bool LoopEventOnTime(const bool bViewBuider)
   {
      u_Interprocess Info;
      int iPos, iTest, iCount;
                                
      if (!m_Infos.bInit) ViewInfos();
      iTest = 0;
      while ((iTest == 0) && (!_StopFlag))
      {
         iTest = (ChartSymbol(m_IdReplay) != "" ? iTest : -1);
         iTest = (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value) ? iTest : -1);
         iTest = (iTest == 0 ? (Info.s_Infos.isPlay ? 1 : iTest) : iTest);
         if (iTest == 0) Sleep(100);
      }
      if ((iTest < 0) || (_StopFlag)) return false;
      AdjustPositionToReplay(bViewBuider);
      Info.ServerTime = m_Ticks.Info[m_ReplayCount].time;
      GlobalVariableSet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
      iPos = iCount = 0;
      while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag))
      {
         iPos += (int)(m_ReplayCount < (m_Ticks.nTicks - 1) ? m_Ticks.Info[m_ReplayCount + 1].time_msc - m_Ticks.Info[m_ReplayCount].time_msc : 0);
         CreateBarInReplay(true);
         while ((iPos > 200) && (!_StopFlag))
         {
            if (ChartSymbol(m_IdReplay) == "") return false;
            GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value);
            if (!Info.s_Infos.isPlay) return true;
            Info.s_Infos.iPosShift = (ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
            GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
            Sleep(195);
            iPos -= 200;
	    iCount++;
            if (iCount > 4)
            {
               iCount = 0;
               GlobalVariableGet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
               Info.ServerTime += 1;
               GlobalVariableSet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
            }
         }
      }                               
      return (m_ReplayCount == m_Ticks.nTicks);
   }                               

Añadimos una nueva variable, que nos ayudará a intentar acercarnos un poco al tiempo donde una nueva barra va a iniciar. Pero esto no se hará de cualquier manera. Ya que podemos avanzar el tiempo, necesitamos hacer que el valor indicado por nuestro "servidor" sea reajustado. Esto se hará en este momento, donde la función ya estará indicando un nuevo punto de observación. Incluso si permanecemos durante un tiempo en modo pausa, necesitamos garantizar que el valor aún será el adecuado. El problema real ocurre justamente cuando vamos a hacer el ajuste del temporizador. No podemos simplemente capturar un valor cualquiera. El sistema podría estar ligeramente adelantado o retrasado con respecto al tiempo real visto en el reloj del computador. Por esto, intentamos en este primer momento acercarnos al tiempo correcto. Tenemos un temporizador incorporado en el generador de barras. Usaremos este para guiarnos. Da un tick cada aproximadamente 195 milisegundos, esto nos hace acercarnos a una cuenta de 5 unidades. Ya que empezamos con el valor cero, probaremos si el valor del contador es mayor que 4. Cuando ocurra, incrementaremos una unidad de tiempo, es decir 1 segundo, y este valor será colocado en la variable global de terminal, para que el resto del sistema pueda usarlo. Entonces todo el ciclo se repetirá.

Puedes imaginar que esto nos hace saber cuándo aparecerá una nueva barra. Sí, esa es la idea, pero existen pequeñas variaciones de un tiempo a otro. Pero a medida que se van acumulando, la cosa termina estando bastante fuera de sincronía. Así necesitamos hacer que la sincronía se mantenga lo más cerca posible de algo aceptable. No que de hecho alcanzaremos un punto perfecto. Pero en la gran mayoría de los casos, podemos acercarnos bastante. Para esto modificaremos un poco más el procedimiento anterior, de manera de garantizar un aumento de la sincronía entre la barra y el cronómetro. Los cambios que propongo, se ven en el fragmento abajo:

//...

   Sleep(195);
   iPos -= 200;
   iCount++;
   if (iCount > 4)
   {
      iCount = 0;
      GlobalVariableGet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
      Info.ServerTime += 1;
      Info.ServerTime = ((Info.ServerTime + 1) < m_Ticks.Info[m_ReplayCount].time ? Info.ServerTime : m_Ticks.Info[m_ReplayCount].time);
      GlobalVariableSet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
   }

//...

Puedes hasta pensar que esto es una locura. De cierta forma debo concordar, que es un poco de locura de mi parte. Pero añadiendo esta línea en particular, podemos hacer que la sincronía se mantenga dentro de niveles aceptables. En caso de que el activo tenga una buena liquidez al punto de que las operaciones salgan en un período relativamente corto de tiempo, preferiblemente en menos de 1 segundo, tendremos un sistema bastante sincronizado. Pudiendo acercarse enormemente a un sistema casi perfecto. Pero esto es solo conseguido si el activo realmente tiene una liquidez adecuada. Te podrías estar preguntando: ¿En sano juicio, qué locura es esta, que está sucediendo en esta línea destacada?!?! Vamos a pensar un poco: Cada 5 ciclos de aproximadamente 195 milisegundos, tendremos la ejecución del código con el fin de actualizar el cronómetro. Esto hace que la tasa de actualización sea alrededor de 975 milisegundos, es decir, queda faltando 25 milisegundos en cada ciclo. Pero este valor no es de hecho constante. A veces, es un poco mayor y otras veces, un poco menor. No es adecuado intentar ajustar la sincronía usando un nuevo comando sleep con el fin de hacer que el sistema quede parado cubriendo esta diferencia. A primera vista esto funcionaría. Pero conforme el tiempo fuera pasando, esta microdiferencia acabaría por ser suficientemente grande al punto de que todo el sistema quedaría fuera de sincronía. Para resolver este problema, hacemos algo un poco diferente. En lugar de intentar fijar la temporización exactamente, usamos la propia barra para generar la sincronía. Cuando la función CreateBarInReplay es ejecutada, siempre apuntará al tick actual. Comparando el valor de tiempo de este tick con el valor de tiempo que está en la variable global, podremos tener en algunos casos, un valor mayor que una unidad, en este caso 1 segundo. Si este valor es inferior, es decir, la variable Info.ServerTime esté retrasada por cuenta de los 25 milisegundos acumulados en el tiempo. El valor presente en el tiempo del tick será usado, ajustando así la diferencia y haciendo que el cronómetro se mantenga bien cerca del valor perfecto. Pero como informé al inicio de la explicación, este mecanismo ajusta de forma automática el sistema en caso de que el activo que estamos usando, tenga una liquidez adecuada. Si la negociación se queda mucho tiempo parada, con operaciones saliendo cada 5 o 10 minutos, esto entre una operación y otra, la precisión del sistema de cronometraje será afectada. Ya que a cada segundo él irá en promedio atrasándose en 25 milisegundos (este es un valor medio, no es un valor exacto).

Hecho esto, podemos pasar a la siguiente parte. Que está en el archivo de cabecera C_Study.mqh, donde haremos que el sistema informe los datos de manera que tengamos una estimación adecuada, de cuándo surgirá una nueva barra en el gráfico.


Adaptando la clase C_Study

Para comenzar de hecho las modificaciones. La primera cosa a ser hecha es el cambio se ve justo abajo:

void Update(void)
   {
      switch (m_Info.Status)
      {
         case eCloseMarket: m_Info.szInfo = "Closed Market";                             break;
         case eAuction    : m_Info.szInfo = "Auction";                                   break;
         case eInReplay   :
         case eInTrading  : m_Info.szInfo = TimeToString(GetBarTime(), TIME_SECONDS);    break;
         case eInReplay   : m_Info.szInfo = "In Replay";                                 break;
         default          : m_Info.szInfo = "ERROR";
      }
      Draw();
   }

Nótese que eliminamos la línea que se encuentra borrada, y la subimos a un nivel superior, de manera que ella disfrutará del mismo estatus y la misma función usada durante el uso del sistema en un mercado físico. Así nivelamos las cosas, es decir tú tendrás el mismo comportamiento en ambas situaciones, sea en el repetición/simulador, sea en una cuenta demo/real. Ahora que ya hicimos esto, podemos ver la primera modificación hecha en el código del procedimiento GetBarTime. Esto se puede ver justo a continuación:

const datetime GetBarTime(void)
   {
      datetime dt;
      u_Interprocess info;
                                
      if (m_Info.Status == eInReplay)
      {
         if (!GlobalVariableGet(def_GlobalVariableServerTime, info.u_Value.df_Value)) return ULONG_MAX;
         dt = info.ServerTime;
      }else dt = TimeCurrent();
                                
      if (m_Info.Rate.time <= dt)
         m_Info.Rate.time = iTime(GetInfoTerminal().szSymbol, PERIOD_CURRENT, 0) + PeriodSeconds();

      return m_Info.Rate.time - dt;
   }

Aquí es donde la magia sucede dentro del sistema de estudios. En la versión antigua de esta misma función, el cronómetro era ajustado por esta llamada. Pero ella no es adecuada para trabajar en el sistema de repetición/simulador. Por esto estamos creando un método para eludir justamente esta llamada, con el fin de conseguir que el sistema pueda tener los mismos conceptos e información, independientemente de dónde esté siendo usada. Por esto, hemos añadido los siguientes puntos que son destacados en verde. Estos puntos serán usados durante el período que el código esté en un gráfico, cuyo activo es el usado en la repetición. Observen que es algo simple, no contando con nada tan extraordinario. Simplemente tomamos el valor informado y colocado en la variable global y lo usamos como si estuviera viniendo del servidor comercial. Esta es la parte donde, de hecho, eludimos el sistema. Pero todavía tenemos un problema aquí. Cuando estemos en un tiempo gráfico bajo, donde el número de operaciones no sea suficientemente alto para que la construcción de las barras se dé de manera adecuada. Y esto ocurre especialmente cuando los datos usados, contienen una subasta en el intradía (caso muy común en algunos activos) tendremos una falla. Tal fallo se presentará como un gap entre la información que está siendo presentada y la visualizada. No es que el servicio haya dejado de hacer las actualizaciones. Pero el indicador no mostrará ningún tipo de información, lo que te dejará a oscuras, sin saber de hecho qué está pasando. Incluso si el activo no ha entrado en subasta, puede ser bastante común en algunos pares negociados en el mercado de forex tener solo una operación en un momento diferente al de la apertura de la barra. Esto puede ser visto en los archivos adjuntos, donde notarás que, al inicio del día, hay un gap entre el punto de apertura de la barra y el punto donde realmente ocurrió una operación. Durante esa fase, no se vería ningún tipo de información que diga qué está pasando. Esto necesita ser corregido de alguna forma, ya sea por el hecho de tener una operación fuera del punto donde el sistema espera, o por el motivo de que el activo usado haya entrado en subasta. Necesitamos realmente dejar las cosas lo más cercanas posible a la realidad.

Para resolver estas cuestiones, tenemos que hacer dos cambios en el código de la clase. Esto de forma más inmediata. La primera modificación se ve justo abajo:

void Update(void)
   {
      datetime dt;
                                
      switch (m_Info.Status)
      {
         case eCloseMarket:
            m_Info.szInfo = "Closed Market";
            break;
         case eInReplay   :
         case eInTrading  :
            dt = GetBarTime();
            if (dt < ULONG_MAX)
            {
               m_Info.szInfo = TimeToString(dt, TIME_SECONDS);
               break;
            }
         case eAuction    :
            m_Info.szInfo = "Auction";
            break;
         default          :
            m_Info.szInfo = "ERROR";
      }
      Draw();
   }

Este código Update, visto arriba, a pesar de parecer extraño y complicado, es mucho más simple y amigable de lo que parece. Lo que tenemos aquí es el siguiente escenario. En caso de que estemos en un sistema de repetición o incluso en un mercado real, y recibamos de la función GetBarTime un valor de ULONG_MAX, tendremos impreso el mensaje de subasta. Si el valor es menor que este ULONG_MAX, y esto siempre será verdadero en situaciones normales, tendremos el valor del cronómetro siendo impreso.

Con base en esta información, ya podemos volver a la función GetBarTime y a generar los datos que necesitamos para que la función Update imprima el dato correcto en el gráfico, para que el usuario sepa cómo las cosas están siendo conducidas. Así, la nueva función GetBarTime puede ser vista en el código a seguir.

const datetime GetBarTime(void)
   {
      datetime dt;
      u_Interprocess info;
      int i0 = PeriodSeconds();
                                
      if (m_Info.Status == eInReplay)
      {
         if (!GlobalVariableGet(def_GlobalVariableServerTime, info.u_Value.df_Value)) return ULONG_MAX;
         dt = info.ServerTime;
         if (dt == ULONG_MAX) return ULONG_MAX;
      }else dt = TimeCurrent();
      if (m_Info.Rate.time <= dt)
         m_Info.Rate.time = (datetime)(((ulong) dt / i0) * i0)) + i0;

      return m_Info.Rate.time - dt;
   }

Este amigable código resuelve completamente nuestro problema, al menos por ahora, ya que tendremos que hacer adiciones al código del servicio. Pero por ahora vamos a entender qué está pasando aquí. En el caso de un mercado físico donde usamos la función TimeCurrent, nada cambia; permanecemos de la misma manera. Esto, en principio. Pero cuando estamos en un sistema de repetición, la cosa cambia de forma muy peculiar. Entonces, atención para entender cómo el sistema realmente consigue mostrar las cosas, independientemente de lo que esté pasando con los datos de la repetición o simulación. En caso de que el servicio coloque el valor ULONG_MAX, en la variable global del terminal, o si esta variable no se encuentra. Esta función GetBarTime retornará el valor ULONG_MAX. Así el procedimiento Update informará que estamos en modo subasta.  Esto se hace en estos puntos. No será posible avanzar más en lo que respecta al cronómetro. Ahora viene la parte interesante que resuelve nuestro segundo problema. A diferencia de cuando estamos usando el sistema en un activo que está conectado al servidor comercial, donde siempre estaremos sincronizados, cuando estamos usando la repetición/simulación, las cosas pueden salirse un poco de control, y podemos encontrarnos con algunas situaciones bastante inusuales. Para resolver este problema, utilizamos este cálculo que sirve tanto para un mercado físico como para nuestro sistema en desarrollo. Lo que estamos haciendo en este cálculo es reemplazar el antiguo método de saber cuál fue el momento de apertura de la barra actual. Así conseguimos solucionar ambos problemas que había al usar la repetición/simulador.

Pero tenemos que volver a la clase C_Replay, para hacer que el sistema pueda indicar cuándo el activo entró en subasta. Esta parte es relativamente simple, ya que todo lo que tenemos que hacer es lanzar el valor ULONG_MAX, dentro de la variable global de terminal. Vean que se dijo relativamente simple, ya que tenemos otros problemas a la vista. Pero vamos a ver cómo realmente esto se llevará a cabo en la práctica.


Adecuamos la clase C_Replay al sistema de comunicación

La primera cosa que haremos en la clase C_Replay es modificar el siguiente código:

bool ViewReplay(ENUM_TIMEFRAMES arg1)
   {
#define macroError(A) { Print(A); return false; }
      u_Interprocess info;
                                
      if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0)
         macroError("Asset configuration is not complete, it remains to declare the size of the ticket.");
      if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0)
         macroError("Asset configuration is not complete, need to declare the ticket value.");
      if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0)
         macroError("Asset configuration not complete, need to declare the minimum volume.");
      if (m_IdReplay == -1) return false;
      if ((m_IdReplay = ChartFirst()) > 0) do
      {
         if (ChartSymbol(m_IdReplay) == def_SymbolReplay)
         {
            ChartClose(m_IdReplay);
            ChartRedraw();
         }
      }while ((m_IdReplay = ChartNext(m_IdReplay)) > 0);
      Print("Waiting for [Market Replay] indicator permission to start replay ...");
      info.ServerTime = ULONG_MAX;
      info.ServerTime = m_Ticks.Info[m_ReplayCount].time;
      CreateGlobalVariable(def_GlobalVariableServerTime, info.u_Value.df_Value);
      info.u_Value.IdGraphic = m_IdReplay = ChartOpen(def_SymbolReplay, arg1);
      ChartApplyTemplate(m_IdReplay, "Market Replay.tpl");
      CreateGlobalVariable(def_GlobalVariableIdGraphics, info.u_Value.df_Value);
      while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(750);

      return ((!_StopFlag) && (ChartSymbol(m_IdReplay) != ""));
#undef macroError
   }

Observa que hemos eliminado una línea y en su lugar hemos puesto otra. Exactamente, esto hará que al iniciar el servicio de repetición/simulador y el activo sea lanzado en el gráfico, se indique el mensaje de subasta. Mostrándonos que el sistema ahora está comunicándose de forma adecuada, ya que antes de dar play al sistema debemos pensar que realmente se encuentra en proceso de subasta. Esta fue la parte fácil, ahora vamos a la parte complicada. Sucede que, cuando el servicio ya está en ejecución, necesitamos saber si el activo entró o no en subasta. Este tipo de cosa tiene sus razones. El activo puede entrar en subasta por diversos motivos, y cada uno de ellos puede ser completamente inesperado, ya sea porque ocurrió una variación mucho mayor de lo que de hecho podría suceder, porque el book simplemente se bloqueó, o porque una orden acabó limpiando el book completamente, entre otros. El motivo por el cual el activo entró en subasta, de hecho, no nos interesa. Lo que nos interesa es lo que sucede cuando entra en subasta. Existen reglas específicas que dicen cómo se llevará a cabo la subasta. Pero la regla que más nos importa es: ¿Cuál es el tiempo mínimo que el activo necesita estar en subasta? Este es el punto. Si este tiempo es inferior a 1 minuto, con toda seguridad, el activo en un mercado real puede entrar y salir de la subasta sin que el sistema de repetición/simulador pueda de hecho detectarlo. O, mejor dicho, no será posible percibir esta variación, ya que estará siempre ocurriendo dentro del menor plazo que puede ser usado para definir el tiempo de una barra.

El mecanismo más simple para ser usado en una repetición/simulación a fin de detectar que el activo entró en subasta es justamente este: verificar la diferencia de tiempo entre una barra y otra. En caso de que esta diferencia sea superior a 1 minuto, debemos informar al usuario que el activo acaba de entrar en proceso de subasta, quedando así suspendido por todo aquel período. Este tipo de mecanismo será bastante útil después. Por ahora vamos a preocuparnos solo en desarrollar e implementar el mismo, dejando otras cuestiones para otro momento. Vamos a ver cómo resolver este problema. Esto puede verse en el código a continuación:

bool LoopEventOnTime(const bool bViewBuider)
   {
      u_Interprocess Info;
      int iPos, iTest, iCount;
                                
      if (!m_Infos.bInit) ViewInfos();
      iTest = 0;
      while ((iTest == 0) && (!_StopFlag))
      {
         iTest = (ChartSymbol(m_IdReplay) != "" ? iTest : -1);
         iTest = (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value) ? iTest : -1);
         iTest = (iTest == 0 ? (Info.s_Infos.isPlay ? 1 : iTest) : iTest);
         if (iTest == 0) Sleep(100);
      }
      if ((iTest < 0) || (_StopFlag)) return false;
      AdjustPositionToReplay(bViewBuider);
      Info.ServerTime = m_Ticks.Info[m_ReplayCount].time;
      GlobalVariableSet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
      iPos = iCount = 0;
      while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag))
      {
         iPos += (int)(m_ReplayCount < (m_Ticks.nTicks - 1) ? m_Ticks.Info[m_ReplayCount + 1].time_msc - m_Ticks.Info[m_ReplayCount].time_msc : 0);
         CreateBarInReplay(true);
         while ((iPos > 200) && (!_StopFlag))
         {
            if (ChartSymbol(m_IdReplay) == "") return false;
            GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value);
            if (!Info.s_Infos.isPlay) return true;
            Info.s_Infos.iPosShift = (ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
            GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
            Sleep(195);
            iPos -= 200;
            iCount++;
            if (iCount > 4)
            {
               iCount = 0;
               GlobalVariableGet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
               if ((m_Ticks.Info[m_ReplayCount].time - m_Ticks.Info[m_ReplayCount - 1].time) > 60) Info.ServerTime = ULONG_MAX; else
               {
                  Info.ServerTime += 1;
                  Info.ServerTime = ((Info.ServerTime + 1) < m_Ticks.Info[m_ReplayCount].time ? Info.ServerTime : m_Ticks.Info[m_ReplayCount].time);
               };
               GlobalVariableSet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
            }
         }
      }                               
      return (m_ReplayCount == m_Ticks.nTicks);
   }                               

Noten que probamos la diferencia de tiempo, entre un tick y otro. En caso de que esta diferencia sea mayor a 60 segundos, es decir, mayor que el menor tiempo de creación de una barra. Vamos a informar esto como siendo una llamada de subasta, entonces todo el sistema de repetición/simulador indicará una subasta. Si la diferencia de tiempo es inferior o igual a 60 segundos, significa que el activo aún permanece en actividad y que el cronómetro deberá mantenerse funcionando, conforme fue visto durante este artículo. Con esto concluimos otra etapa.


Conclusión

Aquí vimos cómo añadir de una manera bastante práctica, segura, robusta y eficaz el cronómetro de indicación de surgimiento de una nueva barra. Pasamos por diversos momentos donde parecía que no era posible, pero nada de hecho es imposible. Puede ser solo un poco más difícil de superar o cuyo objetivo es más trabajoso de alcanzar, pero siempre podemos encontrar un camino adecuado para solucionar los problemas. La principal cosa aquí fue mostrar que siempre debemos intentar crear una solución que, cuando sea utilizada, deba ser aplicable a todas las situaciones donde sea exigida. No tiene sentido programar algo para que el usuario, o incluso tú, aprendas y analices las posibilidades, y cuando vayas a aplicar tal modelo este necesite usar herramientas diferentes de aquellas usadas para desarrollar el análisis. Esto termina desmotivando completamente el uso de una de las herramientas o desmotivando la investigación. Entonces, ten siempre esto en mente: Si es para hacer algo, que esto sea hecho de manera adecuada, que no sea hecha solo para la fase de pruebas y cuando la vayas a usar en la práctica ella tenga un comportamiento totalmente diferente.

Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/11378

Archivos adjuntos |
Files_-_BOLSA.zip (1358.24 KB)
Files_-_FOREX.zip (3743.96 KB)
Files_-_FUTUROS.zip (11397.51 KB)
Desarrollo de un sistema de repetición (Parte 32): Sistema de órdenes (I) Desarrollo de un sistema de repetición (Parte 32): Sistema de órdenes (I)
De todas las cosas desarrolladas hasta ahora, esta, como seguramente también notarás y con el tiempo estarás de acuerdo, es la más desafiante de todas. Lo que tenemos que hacer es algo simple: hacer que nuestro sistema simule lo que hace un servidor comercial en la práctica. Esto de tener que implementar una forma de simular exactamente lo que haría el servidor comercial parece simple. Al menos en palabras. Pero necesitamos hacer esto de manera que, para el usuario del sistema de repetición/simulación, todo suceda de la manera más invisible o transparente posible.
Desarrollo de un sistema de repetición (Parte 30): Proyecto Expert Advisor — Clase C_Mouse (IV) Desarrollo de un sistema de repetición (Parte 30): Proyecto Expert Advisor — Clase C_Mouse (IV)
Aquí te mostraré una técnica que puede ayudarte mucho en varios momentos de tu vida como programador. En contra de lo que muchos dicen, lo limitado no es la plataforma, sino los conocimientos del individuo que lo dice. Lo que se explicará aquí es que con un poco de sentido común y creatividad, se puede hacer que la plataforma MetaTrader 5 sea mucho más interesante y versátil, sin tener que crear programas locos ni nada por el estilo puedes crear un código sencillo, pero seguro y fiable. Utiliza tu ingenio para domar el código con el fin de modificar algo que ya existe, sin eliminar ni añadir una sola línea al código original.
Interfaz gráfica: consejos y recomendaciones para crear una biblioteca gráfica en MQL Interfaz gráfica: consejos y recomendaciones para crear una biblioteca gráfica en MQL
Hoy abarcaremos los conceptos básicos de las bibliotecas GUI para comprender cómo funcionan estas o incluso comenzar a crear bibliotecas propias.
Redes neuronales: así de sencillo (Parte 58): Transformador de decisión (Decision Transformer-DT) Redes neuronales: así de sencillo (Parte 58): Transformador de decisión (Decision Transformer-DT)
Continuamos nuestro análisis de los métodos de aprendizaje por refuerzo. Y en el presente artículo, presentaremos un algoritmo ligeramente distinto que considera la política del Agente en un paradigma de construcción de secuencias de acciones.