English Русский 中文 Deutsch 日本語 Português
preview
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 21):  FOREX (II)

Desarrollo de un sistema de repetición — Simulación de mercado (Parte 21): FOREX (II)

MetaTrader 5Probador | 1 noviembre 2023, 09:35
404 0
Daniel Jose
Daniel Jose

Introducción

En el artículo anterior, "Desarrollo de un sistema de repetición — Simulación de mercado (Parte 20): FOREX (I), comenzamos a armar, o mejor dicho, a adaptar el sistema de repetición/simulación. Esto de tal manera que fuera posible utilizar datos de mercado, como los de FOREX, para al menos poder hacer una repetición de este mercado.

Allí fue necesario realizar varios cambios y ajustes en el sistema para promover esta capacidad. Pero no solo nos enfocamos en el mercado de divisas en ese artículo. Puedes ver que logramos abarcar una amplia gama de posibles tipos de mercados, ya que ahora podemos no solo ver el precio de la última operación, sino también utilizar el sistema de representación basado en el precio de la oferta (BID), que es bastante común en algunos tipos específicos de activos o mercados.

Pero eso no es todo, también se implementó una forma de hacer que el sistema de repetición/simulación no se quedara "bloqueado" cuando los datos de ticks indicaban que el activo no tenía operaciones en un período dado. Ahora el sistema se encuentra en modo de espera hasta que todo ese tiempo se consume, liberando el sistema para ser utilizado nuevamente. Ahora no tenemos este tipo de problema, aunque nuevos desafíos y problemas todavía persisten, esperando ser corregidos y así mejorar aún más nuestro sistema de repetición/simulación.


Resolvamos el problema del archivo de configuración

Pero empecemos por hacer que el archivo de configuración sea más amigable. Si lo has notado, se volvió un poco complicado de trabajar. No quiero que el usuario se enoje al intentar configurar una repetición o simulación y al iniciar el servicio no se reporte ningún tipo de error, pero el gráfico quede completamente en blanco y no se pueda acceder al indicador de control.

Como resultado, al intentar usar el servicio de repetición/simulación, puedes obtener algo similar a lo que se muestra en la figura 01:

Figura 01

Figura 01: Resultado de una falla en la secuencia de carga


Esta falla no se debe a un mal uso o falta de comprensión de cómo trabajar con el sistema. Se debe al hecho de que cualquier dato colocado en el activo personalizado se ha eliminado por completo. Ya sea debido a algún cambio en la configuración del activo o a la destrucción de los datos presentes en él.

Pero de todos modos, este fallo será más común cuando, después de cargar las barras previas de 1 minuto, hagamos la carga de las transacciones de ticks. Al hacer esto, el sistema se dará cuenta de que necesita cambiar la forma en que representa el precio, y al hacer este cambio, todas las barras previas serán eliminadas, como se mostró en el artículo anterior.

Entonces, para resolver este problema, primero necesitaríamos declarar la carga de los ticks antes de cargar las barras previas. Esto soluciona el problema, pero al mismo tiempo obliga al usuario a seguir un tipo de estructura en el archivo de configuración que, en mi opinión, no tiene mucho sentido. La razón es que, al desarrollar la programación responsable de analizar y ejecutar lo que está en el archivo de configuración, podemos permitir que el usuario declare las cosas en cualquier orden, siempre y cuando se respete una cierta sintaxis.

Básicamente, solo hay una forma de resolver el problema. La forma en que se crea este camino nos permite generar pequeñas variaciones, pero en esencia, es siempre la misma. Esto, desde una perspectiva de programación. El camino a seguir, en resumen, es leer por completo el archivo de configuración y luego acceder a las cosas en el orden en que deben ser accedidas, de manera que todo funcione en perfecta armonía.

Una de las variaciones en esta técnica es utilizar las funciones FileSeek y FileTell para poder leer en una secuencia específica. Pero entre acceder al archivo solo una vez y luego acceder a las cosas directamente en la memoria, o leer el archivo en partes hasta hacer el mismo trabajo que se haría si ya estuviera en la memoria.

Personalmente, prefiero cargarlo completamente en la memoria y luego hacer la lectura en partes, de manera que tengamos una secuencia específica que necesitamos para que la carga se realice sin que obtengamos el resultado mostrado en la figura 01. Por lo tanto, esta será la técnica que se utilizará: cargaré el archivo de configuración en la memoria y luego cargaré los datos de manera que el activo pueda ser utilizado en la repetición o simulación.

Para crear una forma en que la programación se sobreponga a la secuencia de comandos dentro del archivo de configuración, primero debemos crear un conjunto de variables. Pero no programaré todo desde cero. Aprovecharemos las rutinas ya incluidas en MQL5 para ayudarnos.

Para ser más preciso, vamos utilizar la biblioteca estándar de MQL5. Es cierto que utilizaremos solo una pequeña fracción de todo lo que se encuentra allí. La gran ventaja de hacerlo de esta manera es que las rutinas presentes allí ya han sido probadas de manera exhaustiva. Y si en algún momento en el futuro se realizan mejoras en su programación, cualesquiera que sean, nuestro sistema absorberá automáticamente esas mejoras. Así que programar las cosas se vuelve mucho más sencillo, ya que necesitaremos menos tiempo en la fase de pruebas y optimización.

Entonces, veamos qué necesitaremos. Esto se muestra en el código a continuación:

#include "C_FileBars.mqh"
#include "C_FileTicks.mqh"
//+------------------------------------------------------------------+
#include <Arrays\ArrayString.mqh>
//+------------------------------------------------------------------+
class C_ConfigService : protected C_FileTicks
{
// ... Código interno da classe ....
}

Aquí vamos a usar esta inclusión, que se proporciona en MQL5. Esto será más que suficiente para nuestro propósito.

Una vez hecho esto, necesitamos algunas variables globales privadas para la clase:

    private :
//+------------------------------------------------------------------+
        enum eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE};
        string m_szPath;
        struct st001
        {
            CArrayString *pTicksToReplay, *pBarsToTicks, *pTicksToBars, *pBarsToPrev;
        }m_GlPrivate;

Estas variables, que están dentro de una estructura, son en realidad punteros a la clase que vamos a utilizar. Es importante que notes que trabajar con punteros es diferente a trabajar con variables. Los punteros son estructuras que indican una posición en la memoria, una dirección para ser más precisos. Las variables, por otro lado, solo contienen valores.

Un gran detalle en esta historia es que, por defecto, y para facilitar la vida de muchos programadores, especialmente aquellos que están comenzando en el campo de la programación, es que en realidad MQL5 no utiliza exactamente el mismo concepto de punteros que se utilizan en C/C++. Aquellos que han programado o programan en C/C++ saben cuán útiles, pero al mismo tiempo peligrosos y confusos pueden ser los punteros. Pero aquí en MQL5, los desarrolladores han intentado eliminar la mayor parte de la confusión y el peligro asociados con el uso de punteros. Sin embargo, para fines prácticos, debes tener en cuenta que, de hecho, lo que vamos a hacer es utilizar punteros para acceder a la clase CArrayString.

Si deseas obtener más información sobre los métodos de la clase CArrayString, consulta la documentación disponible para ella.Solo tienes que hacer clic en CArrayString y ver lo que ya está disponible para nuestro uso. Observa que es bastante completa y se adapta perfectamente a lo que vamos a hacer.

Bien, dado que estamos utilizando punteros para acceder a las cosas, primero debemos realizar una preinicialización de los mismos. Esto se hace en el siguiente fragmento...

bool SetSymbolReplay(const string szFileConfig)
   {

// ... Código interno ...

      m_GlPrivate.pTicksToReplay = m_GlPrivate.pTicksToBars = m_GlPrivate.pBarsToTicks = m_GlPrivate.pBarsToPrev = NULL;
// ... Restante do código ...

   }

Quizás el mayor error de los programadores al utilizar punteros, especialmente en C/C++, es que intentan utilizar estos punteros antes de inicializarlos. El peligro de los punteros radica en que nunca apuntan a una posición de memoria que podamos utilizar antes de inicializarlos. El hecho de intentar leer, y especialmente escribir, en una región desconocida de la memoria probablemente signifique que estás escribiendo en una parte crítica o leyendo basura. Al hacerlo, el sistema suele colapsar, produciendo bloqueos u otros problemas similares. Así que ten mucho cuidado al usar punteros en tus programas.

Tomando estos cuidados, vamos a ver cómo quedó en nuestra rutina de configuración del sistema de repetición/simulación. Esto se puede ver en su totalidad en el código a continuación:

bool SetSymbolReplay(const string szFileConfig)
   {
#define macroFileName ((m_szPath != NULL ? m_szPath + "\\" : "") + szInfo)
      int    file,
             iLine;
      char   cError,
             cStage;

          string szInfo;

          bool   bBarsPrev;
                                                                

          if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
      {

             Print("Falha na abertura do arquivo de configuração [", szFileConfig, "]. Serviço sendo encerrado...");

             return false;
      }

          Print("Carregando dados para replay. Aguarde....");

          ArrayResize(m_Ticks.Rate, def_BarsDiary);
      m_Ticks.nRate = -1;
      m_Ticks.Rate[0].time = 0;
      iLine = 1;
      cError = cStage = 0;
      bBarsPrev = false;
      m_GlPrivate.pTicksToReplay = m_GlPrivate.pTicksToBars = m_GlPrivate.pBarsToTicks = m_GlPrivate.pBarsToPrev = NULL;

          while ((!FileIsEnding(file)) && (!_StopFlag) && (cError == 0))
      {

             switch (GetDefinition(FileReadString(file), szInfo))
         {

                case Transcription_DEFINE:
               cError = (WhatDefine(szInfo, cStage) ? 0 : 1);

                   break;

                case Transcription_INFO:

                   if (szInfo != "") switch (cStage)
               {

                      case 0:
                     cError = 2;

                         break;

                      case 1:

                         if (m_GlPrivate.pBarsToPrev == NULL) m_GlPrivate.pBarsToPrev = new CArrayString();
                     (*m_GlPrivate.pBarsToPrev).Add(macroFileName);
                     pFileBars = new C_FileBars(macroFileName);
                     if ((m_dtPrevLoading = (*pFileBars).LoadPreView()) == 0) cError = 3; else bBarsPrev = true;
                     delete pFileBars;
                     break;
                  case 2:
                     if (m_GlPrivate.pTicksToReplay == NULL) m_GlPrivate.pTicksToReplay = new CArrayString();
                     (*m_GlPrivate.pTicksToReplay).Add(macroFileName);
                     if (LoadTicks(macroFileName) == 0) cError = 4;
                     break;
                  case 3:
                     if (m_GlPrivate.pTicksToBars == NULL) m_GlPrivate.pTicksToBars = new CArrayString();
                     (*m_GlPrivate.pTicksToBars).Add(macroFileName);
                     if ((m_dtPrevLoading = LoadTicks(macroFileName, false)) == 0) cError = 5; else bBarsPrev = true;
                     break;
                  case 4:
                     if (m_GlPrivate.pBarsToTicks == NULL) m_GlPrivate.pBarsToTicks = new CArrayString();
                     (*m_GlPrivate.pBarsToTicks).Add(macroFileName);
                     if (!BarsToTicks(macroFileName)) cError = 6;
                     break;
                  case 5:
                     if (!Configs(szInfo)) cError = 7;
                     break;
               }
               break;
            };
            iLine += (cError > 0 ? 0 : 1);
         }
         FileClose(file);
         Cmd_TicksToReplay(cError);
         Cmd_BarsToTicks(cError);
         bBarsPrev = (Cmd_TicksToBars(cError) ? true : bBarsPrev);
         bBarsPrev = (Cmd_BarsToPrev(cError) ? true : bBarsPrev);
         switch(cError)
         {
            case 0:
               if (m_Ticks.nTicks <= 0)
               {
                  Print("Não existem ticks para serem usados. Serviço esta sendo encerrado...");
                  cError = -1;
               }else if (!bBarsPrev) FirstBarNULL();
               break;
            case 1  : Print("O comando na linha ", iLine, " não é reconhecido pelo sistema...");    break;
            case 2  : Print("O sistema não esperava o conteudo da linha ", iLine);                  break;
            default : Print("Existe um erro no acesso de um dos arquivos indicados...");
         }
                                              
         return (cError == 0 ? !_StopFlag : false);
#undef macroFileName
      }

Las partes borradas en el código anterior han sido eliminadas de él. Esto se debe a que ya no las necesitamos, se han colocado en otro lugar. Pero antes de ver dónde se encuentran, debes prestar mucha atención a cómo está funcionando el código en esta etapa.

Si lo observas, notarás que hay código bastante repetitivo aquí, a pesar de que siempre estaremos haciendo referencia a diferentes punteros.

Este código sirve para que el operador "new" cree un área de memoria para que la clase pueda existir. Al mismo tiempo, este operador inicializa la clase. Como aquí en realidad no hay un constructor de inicialización, la clase se crea y se inicializa con un valor predeterminado en todos los casos.

Una vez que la clase ha sido inicializada, el valor del puntero ya no será NULL, por lo que el puntero se referirá solo y exclusivamente al primer elemento. Sería como una lista, pero aquí estamos hablando de matrices (Arrays). Ahora que tenemos el puntero apuntando a una posición válida en la memoria, podemos usar uno de los métodos de la clase para agregar una cadena de caracteres a ella.

Observa que esto se hace de manera prácticamente transparente. No necesitamos saber dónde y de qué manera la clase está organizando la información que estamos agregando. Eso no nos importa. Lo que realmente importa es que cada vez que llamamos o invocamos el método, la clase almacena los datos para nosotros.

Fíjate que en ningún momento hicimos referencia a ninguno de los archivos que se indicarán en el archivo de configuración. Lo único que se hace en tiempo real de lectura es configurar la información básica de funcionamiento, pero la lectura de los datos, ya sea de barras o de ticks, se deja para después.

Después de que el sistema haya terminado de leer el archivo de configuración, pasamos a la siguiente etapa. Ahora viene la gran pregunta, sobre la cual tú y solo tú debes pensar al agregar o utilizar este sistema. Así que presta mucha atención a lo que voy a explicar, ya que esto puede marcar la diferencia en tu sistema en particular. Pero para nosotros, eso no importa.

Aquí tenemos 4 procedimientos, y el orden en que aparecen influye en el resultado final. Por ahora, no te preocupes por el código de cada uno de ellos. Lo que debes tener en cuenta aquí es el orden en que aparecen en el código. Porque dependiendo del orden en que aparecen, el sistema mostrará el gráfico de una manera u otra. Puede que esto te parezca confuso, pero intentemos entender cómo se desarrollará la situación y así sabremos cuál es el orden más adecuado según el tipo de base de datos que vayas a utilizar.

En el orden que se utiliza en el código, el sistema hará lo siguiente: primero leerá los ticks de datos reales. Luego convertirá las barras en ticks mediante un proceso de simulación. A continuación, creará las barras previas, siguiendo inicialmente la transformación de ticks en barras, y finalmente cargará las barras de 1 minuto.

La cuestión es que los datos se leerán en este orden que se describió anteriormente. Si deseas cambiar este orden, tendrás que hacerlo modificando el orden en que se llaman estos procedimientos. Sin embargo, no debes realizar la lectura de las barras previas antes de leer los ticks que serán simulados. Esto se debe a que la lectura de los ticks que se usarán en la repetición, o la lectura de las barras que se usarán en el simulador, provoca la eliminación de cualquier cosa que esté en el gráfico. Esto se debe a los detalles vistos en el artículo anterior. Pero el hecho de que el orden de uso o lectura se defina precisamente en el sistema y no en el archivo de configuración permite que declares las barras previas antes que los ticks, y aún así, el sistema podrá manejar posibles problemas.

Sin embargo, no todo es perfecto. Al hacer esto, permitimos que el sistema determine el orden en que ocurrirán las cosas, o más precisamente, en que serán leídas. Pero, sobre todo, la forma en que se hace esto, donde leemos todos los datos en la memoria y luego los interpretamos, hace que el sistema tenga dificultades para informar la línea exacta en caso de que la lectura falle. Sin embargo, aún tendremos la indicación del nombre del archivo del cual no se pudo acceder a los datos.

Para resolver esta molestia de que el sistema no pueda informar la línea exacta donde se encuentra el error, tendremos que hacer una pequeña adición al código, pero es algo bastante sencillo de hacer. Recuerda: No queremos reducir la funcionalidad de nuestro sistema, queremos aumentarla y expandirla para poder cubrir la mayor cantidad posible de casos.

Bueno, antes de implementar realmente la solución, quiero que entiendas por qué estamos utilizando esta solución específica.

Para resolver el problema de saber en qué línea ocurrió el error, durante la fase de carga, debemos almacenar tanto el nombre del archivo como la línea en la que se declaró. En realidad, esta es una solución bastante simple de implementar. La razón es que todo lo que necesitaremos hacer es declarar otro conjunto de variables que utilicen la clase CArrayInt. Esta clase almacenaría las líneas correspondientes a cada uno de los nombres de archivo.

Aunque esta solución parece bastante agradable a primera vista, ya que al hacerlo utilizaríamos la biblioteca estándar, es un poco costosa, ya que nos obligaría a agregar un número mucho mayor de variables de las que serían necesarias si desarrolláramos nuestra propia solución. Usaríamos el mismo principio que las clases utilizadas en la biblioteca estándar, pero con la capacidad de trabajar con una mayor cantidad de datos al mismo tiempo.

Puedes pensar que al hacer esto, estaríamos complicando nuestro proyecto en lugar de utilizar una solución ya probada e implementada que está disponible en la biblioteca estándar. Personalmente, estaría de acuerdo con esta idea y afirmación en muchos casos. Pero aquí no tiene mucho sentido, ya que no necesitamos todos esos métodos disponibles en la biblioteca estándar. Solo necesitamos dos de ellos. Por lo tanto, el costo de la implementación compensa el trabajo adicional. Así que nace una nueva clase en nuestro proyecto, la clase C_Array.

El código de la clase se puede ver íntegramente a continuación:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
class C_Array
{
    private :
//+------------------------------------------------------------------+
        string  m_Info[];
        int     m_nLine[];
        int     m_maxIndex;
//+------------------------------------------------------------------+
    public  :
        C_Array()
            :m_maxIndex(0)
        {};
//+------------------------------------------------------------------+
        ~C_Array()
        {
            if (m_maxIndex > 0)
            {
                ArrayResize(m_nLine, 0);
                ArrayResize(m_Info, 0);
            }
        };
//+------------------------------------------------------------------+
        bool Add(const string Info, const int nLine)
        {
            m_maxIndex++;
            ArrayResize(m_Info, m_maxIndex);
            ArrayResize(m_nLine, m_maxIndex);
            m_Info[m_maxIndex - 1] = Info;
            m_nLine[m_maxIndex - 1] = nLine;

            return true;
        }
//+------------------------------------------------------------------+
        string At(const int Index, int &nLine) const
        {
            if (Index >= m_maxIndex)
            {
                nLine = -1;
                return "";
            }
            nLine = m_nLine[Index];
            return m_Info[Index];
        }
//+------------------------------------------------------------------+
};
//+------------------------------------------------------------------+

Observa lo simple y compacta que es, y sin embargo, encaja perfectamente con el propósito que necesitamos. Usando esta clase, podemos almacenar al mismo tiempo tanto la línea como el nombre del archivo que contiene la información necesaria para que la repetición o simulación puedan ocurrir. Todos estos datos se declaran en el archivo de configuración. Entonces, al agregar esta clase a nuestro proyecto, mantenemos el mismo nivel de funcionalidad que antes, o mejor dicho, aumentamos el nivel de funcionalidad, ya que ahora también podemos hacer una repetición de datos provenientes de un mercado similar al de forex.

Pero con esto, algunos cambios necesitan hacerse en el código de la clase responsable de la configuración de la repetición/simulación, y estos cambios comienzan en el siguiente código:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_FileBars.mqh"
#include "C_FileTicks.mqh"
#include "C_Array.mqh"
//+------------------------------------------------------------------+
class C_ConfigService : protected C_FileTicks
{
        protected:
//+------------------------------------------------------------------+
                datetime m_dtPrevLoading;
                int      m_ReplayCount;
//+------------------------------------------------------------------+
inline void FirstBarNULL(void)
                {
                        MqlRates rate[1];

                        for(int c0 = 0; m_Ticks.Info[c0].volume_real == 0; c0++)
                                rate[0].close = m_Ticks.Info[c0].last;
                        rate[0].open = rate[0].high = rate[0].low = rate[0].close;
                        rate[0].tick_volume = 0;
                        rate[0].real_volume = 0;
                        rate[0].time = m_Ticks.Info[0].time - 60;
                        CustomRatesUpdate(def_SymbolReplay, rate);
                        m_ReplayCount = 0;
                }
//+------------------------------------------------------------------+
        private :
//+------------------------------------------------------------------+
                enum eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE};
                string m_szPath;
                struct st001
                {
                        C_Array *pTicksToReplay, *pBarsToTicks, *pTicksToBars, *pBarsToPrev;
                        int     Line;
                }m_GlPrivate;

Fue necesario agregar solo estos puntos al archivo, pero al mismo tiempo necesitamos ajustar algunas cosas en la rutina de configuración. Entonces, la nueva rutina quedará como se muestra a continuación:

bool SetSymbolReplay(const string szFileConfig)
    {
#define macroFileName ((m_szPath != NULL ? m_szPath + "\\" : "") + szInfo)
        int     file;
        char    cError,
                cStage;
        string  szInfo;
        bool    bBarsPrev;

        if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
        {
            Print("Falha na abertura do arquivo de configuração [", szFileConfig, "]. Serviço sendo encerrado...");
            return false;
        }
        Print("Carregando dados para replay. Aguarde....");
        ArrayResize(m_Ticks.Rate, def_BarsDiary);
        m_Ticks.nRate = -1;
        m_Ticks.Rate[0].time = 0;
        cError = cStage = 0;
        bBarsPrev = false;
        m_GlPrivate.Line = 1;
        m_GlPrivate.pTicksToReplay = m_GlPrivate.pTicksToBars = m_GlPrivate.pBarsToTicks = m_GlPrivate.pBarsToPrev = NULL;
        while ((!FileIsEnding(file)) && (!_StopFlag) && (cError == 0))
        {
            switch (GetDefinition(FileReadString(file), szInfo))
            {
                case Transcription_DEFINE:
                    cError = (WhatDefine(szInfo, cStage) ? 0 : 1);
                    break;
                case Transcription_INFO:
                if (szInfo != "") switch (cStage)
                {
                    case 0:
                        cError = 2;
                        break;
                    case 1:
                        if (m_GlPrivate.pBarsToPrev == NULL) m_GlPrivate.pBarsToPrev = new C_Array();
                        (*m_GlPrivate.pBarsToPrev).Add(macroFileName, m_GlPrivate.Line);
                        break;
                    case 2:
                        if (m_GlPrivate.pTicksToReplay == NULL) m_GlPrivate.pTicksToReplay = new C_Array();
                        (*m_GlPrivate.pTicksToReplay).Add(macroFileName, m_GlPrivate.Line);
                        break;
                    case 3:
                        if (m_GlPrivate.pTicksToBars == NULL) m_GlPrivate.pTicksToBars = new C_Array();
                        (*m_GlPrivate.pTicksToBars).Add(macroFileName, m_GlPrivate.Line);
                        break;
                    case 4:
                        if (m_GlPrivate.pBarsToTicks == NULL) m_GlPrivate.pBarsToTicks = new C_Array();
                        (*m_GlPrivate.pBarsToTicks).Add(macroFileName, m_GlPrivate.Line);
                        break;
                    case 5:
                        if (!Configs(szInfo)) cError = 7;
                        break;
                }
                break;
            };
            m_GlPrivate.Line += (cError > 0 ? 0 : 1);
        }
        FileClose(file);
        Cmd_TicksToReplay(cError);
        Cmd_BarsToTicks(cError);
        bBarsPrev = (Cmd_TicksToBars(cError) ? true : bBarsPrev);
        bBarsPrev = (Cmd_BarsToPrev(cError) ? true : bBarsPrev);
        switch(cError)
        {
            case 0:
                if (m_Ticks.nTicks <= 0)
                {
                    Print("Não existem ticks para serem usados. Serviço esta sendo encerrado...");
                    cError = -1;
                }else if (!bBarsPrev) FirstBarNULL();
                break;
            case 1  : Print("O comando na linha ", m_GlPrivate.Line, " não é reconhecido pelo sistema..."); break;
            case 2  : Print("O sistema não esperava o conteudo da linha: ", m_GlPrivate.Line);              break;
            default : Print("Erro no acesso no arquivo indicado na linha: ", m_GlPrivate.Line);
        }
        
        return (cError == 0 ? !_StopFlag : false);
#undef macroFileName

    }

Ahora puedes ver que ya podemos informar al usuario si ocurrió un error y en qué línea sucedió. Pero el costo de hacerlo es casi nulo, porque como puedes notar, las rutinas prácticamente se mantuvieron iguales que antes, solo sufrieron la adición de un nuevo elemento. Lo cual es muy bueno dada la cantidad de cosas que tendrían que hacerse si usáramos la biblioteca estándar. No me malinterpretes, no estoy diciendo que no debes usar la biblioteca estándar, solo estoy mostrando que existen momentos en los que necesitamos crear nuestra propia solución, ya que en esos momentos los costos compensan el trabajo.

Muy bien, ahora ya podemos ver las rutinas que complementan este sistema, que son las cuatro rutinas mostradas arriba, todas son rutinas bastante simples en general, y sus códigos se muestran a continuación, y dado que todas siguen casi el mismo principio, explicaré todas a la vez para no hacer que todo esto sea demasiado tedioso.

inline void Cmd_TicksToReplay(char &cError)
    {
        string szInfo;

        if (m_GlPrivate.pTicksToReplay != NULL)
        {
            for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++)
            {
                if ((szInfo = (*m_GlPrivate.pTicksToReplay).At(c0, m_GlPrivate.Line)) == "") break;
                if (LoadTicks(szInfo) == 0) cError = 4;                                         
            }
            delete m_GlPrivate.pTicksToReplay;
        }
    }
//+------------------------------------------------------------------+
inline void Cmd_BarsToTicks(char &cError)
    {
        string szInfo;

        if (m_GlPrivate.pBarsToTicks != NULL)
        {
            for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++)
            {
                if ((szInfo = (*m_GlPrivate.pBarsToTicks).At(c0, m_GlPrivate.Line)) == "") break;
                if (!BarsToTicks(szInfo)) cError = 6;
            }
            delete m_GlPrivate.pBarsToTicks;
        }
    }
//+------------------------------------------------------------------+
inline bool Cmd_TicksToBars(char &cError)
    {
        bool bBarsPrev = false;
        string szInfo;

        if (m_GlPrivate.pTicksToBars != NULL)
        {
            for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++)
            {
                if ((szInfo = (*m_GlPrivate.pTicksToBars).At(c0, m_GlPrivate.Line)) == "") break;
                if ((m_dtPrevLoading = LoadTicks(szInfo, false)) == 0) cError = 5; else bBarsPrev = true;
            }
            delete m_GlPrivate.pTicksToBars;
        }
        return bBarsPrev;
    }
//+------------------------------------------------------------------+
inline bool Cmd_BarsToPrev(char &cError)
    {
        bool bBarsPrev = false;
        string szInfo;
        C_FileBars      *pFileBars;

        if (m_GlPrivate.pBarsToPrev != NULL)
        {
            for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++)
            {
                if ((szInfo = (*m_GlPrivate.pBarsToPrev).At(c0, m_GlPrivate.Line)) == "") break;
                pFileBars = new C_FileBars(szInfo);
                if ((m_dtPrevLoading = (*pFileBars).LoadPreView()) == 0) cError = 3; else bBarsPrev = true;
                delete pFileBars;
            }
            delete m_GlPrivate.pBarsToPrev;
        }

        return bBarsPrev;
    }
//+------------------------------------------------------------------+

Pero esperen un momento... observen con atención todas estas cuatro rutinas anteriores... noten que tienen mucho código que se está repitiendo... ¿y qué hacemos cuando tenemos mucho código repetido? Intentamos reducir la rutina a un nivel más básico de modo que la reutilización de código se convierta en algo más común... ...sé que puede parecer tonto o incluso falta de atención de mi parte, pero en esta serie de artículos no solo te estoy mostrando cómo se construyen los programas desde cero, sino también cómo los buenos programadores hacen que sus programas sean mejores, reduciendo el trabajo de mantenimiento al mismo tiempo que los producen de manera extremadamente rápida...

No es el hecho de tener más o menos años de experiencia en programación lo que cuenta, sino el hecho de que tú, incluso si estás empezando en el mundo de la programación, puedas darte cuenta de que muchas veces las rutinas pueden mejorarse y los códigos reducirse, facilitando así el trabajo de desarrollo y acelerando la construcción de cosas. Aunque al principio pueda parecer una pérdida de tiempo optimizar las cosas para tener un mayor nivel de reutilización de código, a lo largo del desarrollo te ayudará, ya que cuando necesites mejorar algo, tendrás menos cosas que replicar en más de un lugar... Piensa en esto mientras estás programando: ¿No se podría reducir la cantidad de código que estoy usando para realizar esta tarea?

Esto es lo que hace la rutina a continuación, reemplaza a las 4 rutinas anteriores, por lo que si necesitamos mejorar el sistema, solo tendremos que modificar una rutina y no cuatro.

inline bool CMD_Array(char &cError, eWhatExec e1)
    {
        bool        bBarsPrev = false;
        string      szInfo;
        C_FileBars    *pFileBars;
        C_Array     *ptr = NULL;

        switch (e1)
        {
            case eTickReplay: ptr = m_GlPrivate.pTicksToReplay; break;
            case eTickToBar : ptr = m_GlPrivate.pTicksToBars;   break;
            case eBarToTick : ptr = m_GlPrivate.pBarsToTicks;   break;
            case eBarPrev   : ptr = m_GlPrivate.pBarsToPrev;    break;
        }                               
        if (ptr != NULL)
        {
            for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++)
            {
                if ((szInfo = ptr.At(c0, m_GlPrivate.Line)) == "") break;
                switch (e1)
                {
                    case eTickReplay:
                        if (LoadTicks(szInfo) == 0) cError = 4;
                        break;
                    case eTickToBar :
                        if ((m_dtPrevLoading = LoadTicks(szInfo, false)) == 0) cError = 5; else bBarsPrev = true;
                        break;
                    case eBarToTick :
                        if (!BarsToTicks(szInfo)) cError = 6;
                        break;
                    case eBarPrev   :
                        pFileBars = new C_FileBars(szInfo);
                        if ((m_dtPrevLoading = (*pFileBars).LoadPreView()) == 0) cError = 3; else bBarsPrev = true;
                        delete pFileBars;
                        break;
                }
            }
            delete ptr;
        }

        return bBarsPrev;
    }

Es muy importante que notes que esta rutina en realidad está dividida en dos segmentos, aunque esté dentro de un solo procedimiento. El primer segmento es donde inicializamos el puntero que se usará para acceder a los datos que se almacenaron durante la lectura del archivo de configuración. Este segmento es bastante simple, creo que nadie tendrá dificultades para entenderlo. Después de eso, pasaremos al segundo segmento donde leeremos el contenido de los archivos señalados por el usuario. Aquí, cada uno de los punteros leerá el contenido almacenado en la memoria y realizará su trabajo. Al final, el puntero se destruirá, liberando así la memoria utilizada.

Pero antes de volver a la rutina de configuración, quiero mostrar que podemos hacer aún más en este sistema. Al principio de este tema, mencioné que debías compilar el programa pensando en cómo se realizaría la lectura, pero durante el artículo, pensé mejor y decidí que podemos ampliar las cosas a un nivel un poco más amplio, para que no sea necesario volver a compilar el programa todo el tiempo.

La idea aquí es cargar primero los datos que se simularán y luego, si se indica, cargar las barras previas. Pero tenemos un problema: tanto los ticks que se utilizarán en la repetición/simulación como las barras previas pueden provenir de archivos de tipo ticks o de tipo barras, pero necesitamos alguna forma de especificarlo. Entonces, mi propuesta es usar una variable que el usuario pueda definir, pero que no seguirá ningún patrón. Vamos a ser muy específicos para que las cosas no se compliquen y sean viables a largo plazo. Para hacer esto, utilizaremos la siguiente tabla:

Valor Modo de lectura
1 Modo Tick - Tick
2 Modo Tick - Bar
3 Modo Bar - Tick 
4 Modo Bar - Bar 

Tabla 01 - Datos para el modelo de lectura interna

Lo que dice esta tabla de arriba es cómo se realizará la lectura. Puede parecer complicada, pero vamos a entender la propuesta con calma: Siempre comenzaremos leyendo los datos que se usarán en la repetición o simulación y luego leeremos el valor a usar como datos previos en el gráfico. Entonces, supongamos que el usuario informa el valor 1, el sistema leerá de la siguiente manera: Archivo de ticks reales - Archivo de barras convertido en ticks - Archivo de ticks convertido en datos previos - Archivo de barras previas... Este sería el modo 1, pero supongamos que deseas modificar el sistema por cualquier motivo, de modo que use la misma base de datos pero realice otro tipo de análisis. En ese caso, podrías informar el valor 4, por ejemplo, y el sistema utilizará la misma base de datos, pero el resultado sería ligeramente diferente porque la lectura se realizaría en el siguiente orden: Archivo de barras convertido en ticks - Archivo de ticks reales - Archivo de barras previas - Archivo de ticks convertido en datos previos...Si lo intentas, verás que los indicadores, ya sean promedios u otros, generarán pequeños cambios entre un modo y otro, incluso utilizando la misma base de datos.

Implementar esto tiene un costo de programación mínimo, por lo que creo que vale la pena ofrecer este tipo de recurso al usuario.

Entonces, veamos cómo implementar este tipo de sistema. Comenzaremos agregando una variable global, pero privada a la clase, para poder trabajar con más tranquilidad.

class C_ConfigService : protected C_FileTicks
{

       protected:
//+------------------------------------------------------------------+

          datetime m_dtPrevLoading;

          int      m_ReplayCount,
               m_ModelLoading;

Esta variable almacenará el modelo para nosotros, pero incluso si el usuario no la define en el archivo de configuración, estableceremos un valor inicial para ella.

C_ConfigService()
   :m_szPath(NULL), m_ModelLoading(1)
   {
   }

En otras palabras, siempre comenzaremos y, por defecto, utilizaremos el modo Tick - Tick... Después de hacer esto, necesitamos permitir que el usuario indique el valor que debe usarse. Y esto también es igualmente sencillo de hacer, como se ve en el fragmento a continuación:

inline bool Configs(const string szInfo)
    {
        const string szList[] = 
        {
            "PATH",
            "POINTSPERTICK",
            "VALUEPERPOINTS",
            "VOLUMEMINIMAL",
            "LOADMODEL"
        };
        string  szRet[];
        char    cWho;

        if (StringSplit(szInfo, '=', szRet) == 2)
        {
            StringTrimRight(szRet[0]);
            StringTrimLeft(szRet[1]);
            for (cWho = 0; cWho < ArraySize(szList); cWho++) if (szList[cWho] == szRet[0]) break;
            switch (cWho)
            {
                case 0:
                    m_szPath = szRet[1];
                    return true;
                case 1:
                    CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, StringToDouble(szRet[1]));
                    return true;
                case 2:
                    CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, StringToDouble(szRet[1]));
                    return true;
                case 3:
                    CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, StringToDouble(szRet[1]));
                    return true;
                case 4:
                    m_ModelLoading = StringInit(szRet[1]);
                    m_ModelLoading = ((m_ModelLoading < 1) && (m_ModelLoading > 4) ? 1 : m_ModelLoading);
                    return true;                            
            }
            Print("Variável >>", szRet[0], "<< não definida.");
        }else
        Print("Definição de configuração >>", szInfo, "<< invalida.");

        return false;
    }

Aquí establecemos el nombre que el usuario usará al manipular la variable, y también permitimos que el usuario defina un valor a utilizar según se ve en la tabla 01. Aquí hay un pequeño detalle: debemos asegurarnos de que el valor esté dentro de los límites esperados por el sistema. Por lo tanto, realizamos esta pequeña prueba.Si el usuario define un valor diferente al esperado, no se generará ningún tipo de error, pero el sistema comenzará a utilizar el valor predeterminado.

Con todo esto hecho, finalmente podemos ver cómo quedó la rutina de carga y configuración al final de todo este trabajo. Se puede ver a continuación:

bool SetSymbolReplay(const string szFileConfig)
    {
#define macroFileName ((m_szPath != NULL ? m_szPath + "\\" : "") + szInfo)
        int     file;
        char    cError,
                cStage;
        string  szInfo;
        bool    bBarsPrev;

        if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
        {
            Print("Falha na abertura do arquivo de configuração [", szFileConfig, "]. Serviço sendo encerrado...");
            return false;
        }
        Print("Carregando dados para replay. Aguarde....");
        ArrayResize(m_Ticks.Rate, def_BarsDiary);
        m_Ticks.nRate = -1;
        m_Ticks.Rate[0].time = 0;
        cError = cStage = 0;
        bBarsPrev = false;
        m_GlPrivate.Line = 1;
        m_GlPrivate.pTicksToReplay = m_GlPrivate.pTicksToBars = m_GlPrivate.pBarsToTicks = m_GlPrivate.pBarsToPrev = NULL;
        while ((!FileIsEnding(file)) && (!_StopFlag) && (cError == 0))
        {
            switch (GetDefinition(FileReadString(file), szInfo))
            {
                case Transcription_DEFINE:
                    cError = (WhatDefine(szInfo, cStage) ? 0 : 1);
                    break;
                case Transcription_INFO:
                    if (szInfo != "") switch (cStage)
                    {
                        case 0:
                            cError = 2;
                            break;
                        case 1:
                            if (m_GlPrivate.pBarsToPrev == NULL) m_GlPrivate.pBarsToPrev = new C_Array();
                            (*m_GlPrivate.pBarsToPrev).Add(macroFileName, m_GlPrivate.Line);
                            break;
                        case 2:
                            if (m_GlPrivate.pTicksToReplay == NULL) m_GlPrivate.pTicksToReplay = new C_Array();
                            (*m_GlPrivate.pTicksToReplay).Add(macroFileName, m_GlPrivate.Line);
                            break;
                        case 3:
                            if (m_GlPrivate.pTicksToBars == NULL) m_GlPrivate.pTicksToBars = new C_Array();
                            (*m_GlPrivate.pTicksToBars).Add(macroFileName, m_GlPrivate.Line);
                            break;
                        case 4:
                            if (m_GlPrivate.pBarsToTicks == NULL) m_GlPrivate.pBarsToTicks = new C_Array();
                            (*m_GlPrivate.pBarsToTicks).Add(macroFileName, m_GlPrivate.Line);
                            break;
                        case 5:
                            if (!Configs(szInfo)) cError = 7;
                            break;
                    }
                break;
            };
            m_GlPrivate.Line += (cError > 0 ? 0 : 1);
        }
        FileClose(file);
        CMD_Array(cError, (m_ModelLoading <= 2 ? eTickReplay : eBarToTick));
        CMD_Array(cError, (m_ModelLoading <= 2 ? eBarToTick : eTickReplay));
        bBarsPrev = (CMD_Array(cError, ((m_ModelLoading & 1) == 1 ? eTickToBar : eBarPrev)) ? true : bBarsPrev);
        bBarsPrev = (CMD_Array(cError, ((m_ModelLoading & 1) == 1 ? eBarPrev : eTickToBar)) ? true : bBarsPrev);
        switch(cError)
        {
            case 0:
                if (m_Ticks.nTicks <= 0)
                {
                    Print("Não existem ticks para serem usados. Serviço esta sendo encerrado...");
                    cError = -1;
                }else if (!bBarsPrev) FirstBarNULL();
                break;
            case 1  : Print("O comando na linha ", m_GlPrivate.Line, " não é reconhecido pelo sistema..."); break;
            case 2  : Print("O sistema não esperava o conteudo da linha: ", m_GlPrivate.Line);              break;
            default : Print("Erro no acesso no arquivo indicado na linha: ", m_GlPrivate.Line);
        }

        return (cError == 0 ? !_StopFlag : false);
#undef macroFileName
    }

Lo que tenemos aquí es un pequeño sistema que nos permite, o mejor dicho, permite al usuario seleccionar cuál será el modelo a utilizar en la carga. Pero ten en cuenta que siempre comenzaremos cargando primero las cosas que se utilizarán en la repetición o simulación y luego cargaremos los datos que se utilizarán como barras previas.


Consideraciones finales

Con esto concluimos esta fase de trabajo en el archivo de configuración. Ahora, al menos por ahora, el usuario podrá indicar todas las cosas que necesitamos en este primer momento.

Bueno, este artículo consumió bastante tiempo, pero en mi opinión, valió la pena, ya que ahora tenemos la posibilidad de simplemente no preocuparnos por un tiempo con los asuntos relacionados con el archivo de configuración, ya que ahora siempre se ejecutará de la forma esperada, independientemente de cómo se haya construido.

En el próximo artículo continuaremos adaptando el sistema de repetición/simulación para cubrir aún más el mercado de divisas y sus similares, ya que el mercado de valores ya se encuentra en una etapa mucho más avanzada. Me centraré en el mercado de divisas para que el sistema pueda manejarlo de la misma manera que lo hace con el mercado de valores.

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

Archivos adjuntos |
Market_Replay_ev221.zip (14387.15 KB)
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 22): FOREX (III) Desarrollo de un sistema de repetición — Simulación de mercado (Parte 22): FOREX (III)
Para aquellos que aún no han comprendido la diferencia entre el mercado de acciones y el mercado de divisas (forex), a pesar de que este ya es el tercer artículo en el que abordo esto, debo dejar claro que la gran diferencia es el hecho de que en forex no existe, o mejor dicho, no se nos informa acerca de algunas cosas que realmente ocurrieron en la negociación.
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 20): FOREX (I) Desarrollo de un sistema de repetición — Simulación de mercado (Parte 20): FOREX (I)
La intención inicial de este artículo no será cubrir todas las características de FOREX, sino más bien adaptar el sistema de manera que puedas realizar al menos una repetición del mercado. La simulación quedará para otro momento. Sin embargo, en caso de que no tengas los ticks y solo tengas las barras, con un poco de trabajo, puedes simular posibles transacciones que podrían haber ocurrido en FOREX. Esto será hasta que te muestre cómo adaptar el simulador. El hecho de intentar trabajar con datos provenientes de FOREX dentro del sistema sin modificarlo conlleva errores de rango.
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 23): FOREX (IV) Desarrollo de un sistema de repetición — Simulación de mercado (Parte 23): FOREX (IV)
La creación ahora se realiza en el mismo punto en el que convertimos los ticks en barras. Así, si algo va mal durante la conversión, nos daremos cuenta del error enseguida. Esto se debe a que el mismo código que coloca las barras de 1 minuto en el gráfico cuando avanzamos rápidamente también se utiliza para el sistema de posicionamiento y para colocar las barras durante el avance normal. En otras palabras, el código responsable de esta tarea ya no se duplica en ningún lugar. De esta manera, tenemos un sistema mucho más adecuado tanto para el mantenimiento como para las mejoras.
Desarrollando un cliente MQTT para MetaTrader 5: metodología de TDD Desarrollando un cliente MQTT para MetaTrader 5: metodología de TDD
El presente artículo representa el primer intento de desarrollar un cliente MQTT nativo para MQL5. El MQTT es un protocolo de comunicación "publicación-suscripción". Es ligero, abierto, simple y está diseñado para implementarse con facilidad, lo cual permite su uso en muchas situaciones.