Desarrollo de un sistema de repetición — Simulación de mercado (Parte 16): Un nuevo sistema de clases
Introdução
En el artículo anterior, "Desarrollo de un sistema de repetición — Simulación de mercado (Parte 15): Nacimiento del SIMULADOR (V) - RANDOM WALK", desarrollamos una forma de promover la aleatorización de los datos Esto se hizo de manera que el resultado quedara bastante adecuado. Pero a pesar de que el sistema es realmente capaz de simular las órdenes de manera plausible, todavía carece de cierta información, no solo el simulador en sí, sino también el propio sistema de repetición. La verdad es que algunas de las cosas que necesitamos implementar son de las más complicadas de hacer, especialmente en la forma en que se está construyendo el sistema. Necesitamos hacer algunos cambios para al menos tener un mejor modelo y evitar perdernos en las próximas etapas.
Créeme, si encontraste complicado el RANDOM WALK, es porque aún no has visto o no tienes idea de lo que será necesario desarrollar para que la experiencia de simulación/repetición sea la más adecuada. Uno de los aspectos más desafiantes es que debes conocer al menos lo básico sobre el activo que se va a simular o del cual se va a hacer la repetición. En este momento de desarrollo, no estoy preocupado por resolver algunas cuestiones de manera automática. No es que esto sea difícil de hacer, sino que existen formas más prácticas de hacerlo.
Si observaste bien el artículo anterior, habrás notado que es posible utilizar programas como EXCEL para generar posibles escenarios de mercado. Pero es posible que también utilices información de otros activos y los combines para generar una simulación más compleja. El hecho de hacer esto complica la creación de un sistema totalmente automático que podría buscar la información faltante del propio activo. Pero también tenemos otro problema de menor importancia. Por eso, aquí recortaré y distribuiré el contenido del archivo de encabezado C_Replay.mqh de tal manera que el mantenimiento y las mejoras se hagan de manera más sencilla. Trabajar con una clase muy grande, además de ser incómodo, no es algo que me guste. La razón es que muchas veces el código puede no estar completamente optimizado. Por lo tanto, para facilitar las cosas, realizaremos ambos cambios: distribuir el contenido de la clase C_Replay en varias clases y, al mismo tiempo, implementaremos las partes más urgentes.
Implementación del servicio con un nuevo sistema de clases
A pesar de que el código visto en artículos anteriores ha experimentado muy pocos cambios en general, esta nueva implementación trae consigo un nuevo modelo. Este modelo es al mismo tiempo más sencillo de expandir y también de comprender para aquellos que no tienen un gran conocimiento en programación. Esto se debe a que las cosas están relacionadas de una manera más simple. No tendremos rutinas largas y tediosas de leer. Hay algunos detalles que para algunos pueden parecer un poco extraños. Pero por más extraño que pueda parecer, hacen que el código sea más seguro, estable y sostenible en términos de estructura. Uno de estos detalles es el uso de punteros, no de la misma manera que los usamos en los lenguajes C++ o C heredados, pero con un comportamiento muy similar a esos lenguajes.
El uso de punteros nos permite hacer cosas que de otra manera no serían posibles. Aunque muchas de las posibilidades presentes en C++ no sean posibles en MQL5, el simple hecho de poder usar punteros en su forma más básica nos permite modelar las cosas de una manera más flexible y agradable, al menos en lo que respecta a la programación y la sintaxis.
Por eso, el nuevo archivo del servicio en la versión actual, que se encuentra en el anexo, ahora se ve de la siguiente manera:
#property service #property icon "\\Images\\Market Replay\\Icon.ico" #property copyright "Daniel Jose" #property version "1.16" #property description "Serviço de Replay-Simulador para plataforma MT5." #property description "Este é dependente do indicador Market Replay." #property description "Para mais detalhes sobre esta versão veja o artigo:" #property link "https://www.mql5.com/pt/articles/11095" //+------------------------------------------------------------------+ #define def_Dependence "\\Indicators\\Market Replay.ex5" #resource def_Dependence //+------------------------------------------------------------------+ #include <Market Replay\C_Replay.mqh> //+------------------------------------------------------------------+ input string user00 = "Config.txt"; //Arquivo de configuração do Replay. input ENUM_TIMEFRAMES user01 = PERIOD_M1; //Tempo gráfico inicial. input bool user02 = true; //Visualizar a construção das barras. input bool user03 = true; //Visualizar métricas de criação. //+------------------------------------------------------------------+ void OnStart() { C_Replay *pReplay; pReplay = new C_Replay(user00); if (pReplay.ViewReplay(user01)) { Print("Permissão concedida. Serviço de replay já pode ser utilizado..."); while (pReplay.LoopEventOnTime(user02, user03)); } delete pReplay; } //+------------------------------------------------------------------+
Puede parecer que no ha experimentado cambios importantes, pero en realidad está bastante diferente. Aunque para el usuario final seguirá siendo el mismo y tendrá un comportamiento idéntico al de las versiones anteriores. Pero para la plataforma y, sobre todo, para el sistema operativo, el archivo ejecutable generado tendrá un comportamiento y se verá de manera diferente. La razón es que estamos utilizando la clase no como una variable, sino como un puntero de memoria. Y es precisamente por eso que todo el sistema tiene un comportamiento muy diferente. Si la programación se realiza con cuidado y atención, las clases serán aún más seguras y estables que las clases utilizadas solo como variables. Aunque tengan el mismo comportamiento, serán diferentes en cuanto al uso de memoria.
Ahora, debido a que estamos utilizando las clases como punteros, renunciaremos a algunas cosas y comenzaremos a usar otras. La primera de ellas es que ahora la clase siempre se inicializará y siempre se cerrará. Ya no de forma implícita, sino de forma explícita. Esto se hace utilizando los operadores new e delete. Cuando utilizamos el operador "new" para crear una clase, siempre debemos llamar a un constructor de clase. Estos nunca devuelven un valor, por lo que no se puede probar un valor de retorno directamente. Deberemos hacer esto en otro momento. Lo mismo ocurre al utilizar el operador "delete", este llamará al destructor de la clase. Al igual que el constructor de la clase, el destructor nunca devuelve un valor. Pero a diferencia del constructor, el destructor no recibe ningún tipo de argumento.
Siempre tendrás que hacerlo de esta manera: crear la clase utilizando el operador "new" y destruir la clase utilizando el operador "delete". Este será el único trabajo que realmente tendrás que hacer. Todo lo demás lo hace el sistema operativo, que asignará suficiente memoria para que el programa se ejecute en una región de memoria, lo que lo hace lo más seguro posible durante toda su vida útil. Pero aquí yace un peligro para quienes están acostumbrados a utilizar punteros en C++/C, que es la notación. En los lenguajes C++ y C, cada vez que nos referimos a punteros, tenemos una notación muy específica. Normalmente usamos una flecha (->). Para un programador de C++/C, esto significa que estamos utilizando un puntero. Pero también podemos usar una notación diferente, que se verá en mis códigos cuando se refiera a un puntero.
Además, por supuesto, del nombre de la variable, que normalmente comienza con la letra "p" o se utiliza una combinación como "ptr" (a pesar de que esto no es una regla estricta, así que no te aferres a ella). Aunque MQL5 acepta tanto la notación que se muestra en el código anterior como la que se verá a continuación, personalmente creo que es más sencillo leer un código que utiliza punteros cuando realmente utiliza la declaración adecuada. Por eso, en mis códigos, la notación será la que se muestra a continuación debido a mi familiaridad con el lenguaje C++/C:
void OnStart() { C_Replay *pReplay; pReplay = new C_Replay(user00); if ((*pReplay).ViewReplay(user01)) { Print("Permissão concedida. Serviço de replay já pode ser utilizado..."); while ((*pReplay).LoopEventOnTime(user02, user03)); } delete pReplay; }
De hecho, hay un trabajo adicional en la parte de escribir el código. Pero para mí, que he estado programando en C++/C durante años, es más fácil entender que me estoy refiriendo a un puntero al ver un código como el mostrado anteriormente. Y dado que MQL5 entiende esto de la misma manera que lo haría C++/C, no veo ningún problema en usar esta notación. Cada vez que veas un código con una notación igual a la mostrada anteriormente, no te preocupes, se trata simplemente de un puntero.
Dicho esto, podemos seguir explorando el nuevo sistema de clases. Si piensas que los cambios solo se han producido hasta este punto, eres bastante optimista. El simple hecho de haber realizado estos cambios, donde garantizaremos de manera explícita que una clase se construirá y destruirá en momentos muy específicos, nos llevará a hacer varios otros cambios en el código. Un constructor y un destructor no devuelven ningún valor. Entonces, necesitamos hacer algo para saber si la clase se ha construido correctamente o no.
Para comprender cómo hacer esto, echemos un vistazo dentro de la caja negra de la clase C_Replay. Esta se encuentra en el archivo de encabezado C_Replay.mqh. La estructura interna se muestra en la imagen a continuación:
Figura 01 - Sistema de conexión de las clases de Repetición
La figura 01 muestra cómo las clases están conectadas entre sí para lograr el funcionamiento deseado y previsto por el servicio de repetición/simulación. La flecha verde indica que la clase se importa de manera que algunas de las cosas internas de la clase serán públicas. La flecha roja indica que los datos se importarán, pero ya no serán visibles en clases superiores. La clase C_FilesBars, en realidad, es una clase independiente. No será heredada por ninguna otra, pero sus métodos serán utilizados por las demás clases.
Para comprender realmente cómo se establecen estas conexiones, será necesario ver lo que está sucediendo bajo la superficie. Para hacerlo, tendremos que ver cómo se creó cada una de las clases y cómo están dentro de sus respectivos archivos. Estos archivos siempre reciben el mismo nombre que la clase. Esto no es obligatorio, pero es una buena práctica. Dado que el código ha experimentado algunos cambios, y muchos de ellos solo para hacer las cosas más agradables y organizadas. No entraré en muchos detalles sobre esto en realidad, pero en algunos momentos explicaré los cambios que han ocurrido y por qué. Puede ser bastante valioso para aquellos que están comenzando a aprender a programar, ya que es mucho más fácil aprender cuando se tiene un código funcional y se va modificando para generar algo diferente, pero al mismo tiempo algo que deseamos, manteniendo siempre el código funcionando.
Entonces, comencemos a comprender cómo se realizó la construcción.
Una clase independiente: C_FileBars
Esta clase es bastante simple y contiene todo lo que necesitas para leer las barras presentes en un archivo. No es una clase muy grande. De esta manera, todo su código se puede ver íntegramente a continuación:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #include "Interprocess.mqh" //+------------------------------------------------------------------+ #define def_BarsDiary 1440 //+------------------------------------------------------------------+ class C_FileBars { private : int m_file; string m_szFileName; //+------------------------------------------------------------------+ inline void CheckFileIsBar(void) { string szInfo = ""; for (int c0 = 0; (c0 < 9) && (!FileIsEnding(m_file)); c0++) szInfo += FileReadString(m_file); if (szInfo != "<DATE><TIME><OPEN><HIGH><LOW><CLOSE><TICKVOL><VOL><SPREAD>") { Print("Arquivo ", m_szFileName, ".csv não é um arquivo de barras."); FileClose(m_file); m_file = INVALID_HANDLE; } } //+------------------------------------------------------------------+ public : //+------------------------------------------------------------------+ C_FileBars(const string szFileNameCSV) :m_szFileName(szFileNameCSV) { if ((m_file = FileOpen("Market Replay\\Bars\\" + m_szFileName + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) Print("Falha ao acessar ", m_szFileName, ".csv de barras."); else CheckFileIsBar(); } //+------------------------------------------------------------------+ ~C_FileBars() { if (m_file != INVALID_HANDLE) FileClose(m_file); } //+------------------------------------------------------------------+ bool ReadBar(MqlRates &rate[]) { if (m_file == INVALID_HANDLE) return false; if (FileIsEnding(m_file)) return false; rate[0].time = StringToTime(FileReadString(m_file) + " " + FileReadString(m_file)); rate[0].open = StringToDouble(FileReadString(m_file)); rate[0].high = StringToDouble(FileReadString(m_file)); rate[0].low = StringToDouble(FileReadString(m_file)); rate[0].close = StringToDouble(FileReadString(m_file)); rate[0].tick_volume = StringToInteger(FileReadString(m_file)); rate[0].real_volume = StringToInteger(FileReadString(m_file)); rate[0].spread = (int) StringToInteger(FileReadString(m_file)); return true; } //+------------------------------------------------------------------+ datetime LoadPreView(const string szFileNameCSV) { int iAdjust = 0; datetime dt = 0; MqlRates Rate[1]; Print("Carregando barras previas para Replay. Aguarde ...."); while (ReadBar(Rate) && (!_StopFlag)) { iAdjust = ((dt != 0) && (iAdjust == 0) ? (int)(Rate[0].time - dt) : iAdjust); dt = (dt == 0 ? Rate[0].time : dt); CustomRatesUpdate(def_SymbolReplay, Rate, 1); } return ((_StopFlag) || (m_file == INVALID_HANDLE) ? 0 : Rate[0].time + iAdjust); } //+------------------------------------------------------------------+ };
Puede que estés pensando que esta clase no tiene mucho sentido, pero contiene todo lo que necesitas para abrir, leer, ensamblar el activo personalizado y cerrar el archivo de barras. Todo esto se hace en pasos muy específicos, de manera que no importa lo que cambiemos aquí adentro. Cualquiera que quiera leer un archivo de barras podrá hacerlo aquí.
Esta clase fue diseñada y desarrollada para siempre utilizar los operadores NEW y DELETE. De esta manera, permanecerá en la memoria solo el tiempo suficiente para realizar su trabajo. Debes evitar al máximo usar esta clase sin utilizar los operadores mencionados anteriormente. De lo contrario, podrías tener problemas de estabilidad. No es que esto realmente sucederá, pero el hecho de que haya sido diseñada para utilizar los operadores no la hace adecuada para ser utilizada de otra manera.
Vamos a entender lo siguiente: En esta clase, tenemos 2 variables globales y privadas de ella misma. Se inicializan exactamente en el constructor de la clase. Donde intentaremos abrir y comprobar si el archivo indicado es o no un archivo de barras. Pero recuerda el siguiente hecho: Un constructor no devuelve ningún tipo de valor. No tenemos forma de devolver absolutamente nada aquí. Pero podemos indicar que el archivo no cumple con lo esperado marcándolo como un archivo no válido. Esto, por supuesto, después de haberlo cerrado. De esta manera, cualquier intento de lectura resultará en un error que se informará adecuadamente. Así, puede ser manejado por el autor de llamada. Por lo tanto, la clase funcionará como si fuera en realidad un archivo que ya contiene las barras. O un gran objeto con el contenido del archivo ya presente en ella. Todo lo que los autores de llamada estarán haciendo se verá como una lectura del contenido de este gran objeto. Pero una vez que se llame al destructor, se cerrará el archivo. Y la clase habrá sido destruida por el proceso autor de llamada.
Puede que no parezca tan seguro o estable este tipo de modelado, pero créanme, es considerablemente mucho más seguro y estable de lo que parece. Aunque si fuera posible tener acceso a algunos otros operadores presentes en C++, la cosa sería aún más interesante. Esto, por supuesto, si la programación se hace correctamente; de lo contrario, sería un desastre total. Pero como MQL5 no es C++, está bien, exploremos y saquemos el máximo provecho de lo que este lenguaje nos permite hacer. De esta manera, tendremos un sistema que estará utilizando algo muy cercano a los límites que el lenguaje nos permite alcanzar.
Uma classe profunda : C_FileTicks
La próxima clase que vamos a ver es la clase C_FileTicks. Esta clase es bastante más compleja que la clase C_FileBars. Esto se debe al hecho de que tenemos cosas que son públicas, cosas que son privadas y cosas que están en un punto intermedio. Y reciben una denominación especial: PROTECTED. Este término "protected" tiene un nivel especial cuando se trata de herencia entre clases. En el caso de C++, la cosa es bastante complicada, al menos al principio del aprendizaje. Esto se debe a algunos operadores presentes en C++. Pero afortunadamente, MQL5 maneja las cosas de una manera mucho más simple. Por lo tanto, será mucho más fácil entender cómo las cosas declaradas como protegidas se heredan y si se pueden acceder o no, dependiendo, por supuesto, de cómo ocurra la herencia. Para esto, consulta la tabla a continuación:
Definición en la clase Base | Tipo de herencia de la clase Base | Acceso dentro de la clase Derivada | Acceso mediante llamada a la clase derivada |
---|---|---|---|
private | public | Acceso denegado | No se puede acceder a datos o procedimientos de la clase base |
public | public | Acceso permitido | Acceso permitido a los datos o procedimientos de la clase base |
protected | public | Acceso permitido | No se puede acceder a datos o procedimientos de la clase base |
private | private | Acceso denegado | No se puede acceder a datos o procedimientos de la clase base |
public | private | Acceso permitido | No se puede acceder a datos o procedimientos de la clase base |
protected | private | Acceso permitido | No se puede acceder a datos o procedimientos de la clase base |
private | protected | Acceso denegado | No se puede acceder a datos o procedimientos de la clase base |
public | protected | Acceso permitido | No se puede acceder a datos o procedimientos de la clase base |
protected | protected | Acceso permitido | No se puede acceder a datos o procedimientos de la clase base |
Tabla de niveles de acceso a elementos y procedimientos de las clases
Notarás que, en un solo caso, realmente podemos acceder a los datos o procedimientos dentro de una clase al utilizar el sistema de herencia o las definiciones de acceso. Y es precisamente cuando todo se declara como público. En todos los demás casos, es más o menos posible a nivel de herencia de la clase. Pero no es posible acceder a ningún procedimiento o dato que esté dentro de la clase base. Esto es independiente de la cláusula de acceso.
Ahora, un detalle: si declaras algo como "protected" y tratas de acceder directamente a estos datos o procedimientos sin que sea a través de la herencia de clase, no podrás acceder a tales datos o procedimientos. Esto se debe a que, sin el uso de la herencia, estos datos o procedimientos declarados como protegidos se tratan como privados. Por lo tanto, el acceso a ellos se denegará.
Parece ser bastante complicado, ¿verdad? Pero no es necesario entrar en pánico y arrancarse los cabellos. En la práctica, las cosas son mucho más simples. Aunque tendrás que experimentar este mecanismo en funcionamiento algunas veces para comprender realmente cómo funciona. Pero créeme, esto en MQL5 es mucho, pero mucho más simple y sin lugar a dudas, que en C++. Allí, la cosa es extremadamente más complicada. Y la razón es que tenemos formas de cambiar el nivel de acceso a datos o procedimientos que se declaran como protegidos. En algunos casos, incluso los privados, durante el proceso de herencia de la clase. Allí, es una locura total. Pero aquí en MQL5, es suave.
Con base en esto, finalmente podemos ver la clase C_FileTicks, que a pesar de ser más complicada en teoría, tiene un código relativamente simple. Comencemos viendo las primeras cosas dentro de la clase, a partir de su declaración:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #include "C_FileBars.mqh" //+------------------------------------------------------------------+ #define def_MaxSizeArray 16777216 // 16 Mbytes de posições //+------------------------------------------------------------------+ #define macroRemoveSec(A) (A - (A % 60)) //+------------------------------------------------------------------+ class C_FileTicks { protected: struct st00 { MqlTick Info[]; MqlRates Rate[]; int nTicks, nRate; }m_Ticks; double m_PointsPerTick;
Observa que es algo simple, pero ten cuidado. Dado que esta estructura y esta variable pueden ser accedidas siguiendo la tabla de nivel de acceso. Hecho esto, tenemos una serie de procedimientos privados de la clase. Estos no pueden ser accedidos fuera de la clase y sirven para apoyar a los procedimientos que son públicos, que son dos y pueden verse a continuación:
public : //+------------------------------------------------------------------+ bool BarsToTicks(const string szFileNameCSV) { C_FileBars *pFileBars; int iMem = m_Ticks.nTicks; MqlRates rate[1]; MqlTick local[]; pFileBars = new C_FileBars(szFileNameCSV); ArrayResize(local, def_MaxSizeArray); Print("Convertendo barras em ticks. Aguarde..."); while ((*pFileBars).ReadBar(rate) && (!_StopFlag)) Simulation(rate[0], local); ArrayFree(local); delete pFileBars; return ((!_StopFlag) && (iMem != m_Ticks.nTicks)); } //+------------------------------------------------------------------+ datetime LoadTicks(const string szFileNameCSV, const bool ToReplay = true) { int MemNRates, MemNTicks; datetime dtRet = TimeCurrent(); MqlRates RatesLocal[]; MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate); MemNTicks = m_Ticks.nTicks; if (!Open(szFileNameCSV)) return 0; if (!ReadAllsTicks()) return 0; if (!ToReplay) { ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates)); ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0); CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates)); dtRet = m_Ticks.Rate[m_Ticks.nRate].time; m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates); m_Ticks.nTicks = MemNTicks; ArrayFree(RatesLocal); } return dtRet; }; //+------------------------------------------------------------------+
Todos los procedimientos privados son procedimientos que ya se han explicado y analizado en artículos anteriores de esta serie. De hecho, no entraré en detalles sobre ellos aquí, ya que no han sufrido ningún cambio, o casi ninguno, con respecto a lo que se explicó. Sin embargo, en relación a estos dos procedimientos públicos, hay algo que merece una atención más especial. El detalle se encuentra en la rutina BarsToTicks. Así que presta atención a los siguientes puntos: En el tema anterior, mencioné que la clase C_FileBars es una clase independiente que no se hereda en realidad. Esto es cierto hasta el momento presente. Pero también mencioné que se usaría en puntos específicos. Cuando esto sucediera, debía ser accedida de una manera bastante específica.
Bien, aquí tienes uno de esos puntos. Primero declaramos la clase de una manera muy específica. Ahora llamamos al constructor de la clase con el nombre del archivo del cual queremos obtener los valores de las barras. Recuerda: esta llamada no devolverá ningún valor. Lo que estamos haciendo es utilizar el operador NEW para que la clase reciba un espacio en la memoria reservado solo para ella. En este espacio se contendrá, MetaTrader 5 no tiene realmente control sobre dónde puede estar la clase. Solo el sistema operativo tiene esta información.
Pero de manera más conveniente, recibimos un valor del operador NEW, y este valor es un "puntero" que podemos usar para referenciar directamente a nuestra clase (NOTA: La palabra "puntero" está entre comillas porque no es en realidad un puntero. Es simplemente una variable que puede referenciar una clase, como si esta clase fuera otra variable o constante. En otro momento en el futuro, mostraré cómo puedes usar este recurso para crear una clase constante en la que solo podemos acceder a datos pero no realizar cálculos. Dado que tiene una aplicación muy específica, la dejaremos para otra oportunidad). Una vez que tenemos este "puntero", podemos realizar el trabajo en nuestra clase. Pero nuevamente, aún no tenemos la certeza de que el archivo esté abierto y pueda ser leído. Por eso, tendríamos que realizar algún tipo de prueba antes de intentar leer y usar cualquier dato. Afortunadamente, esto no es necesario, ya que durante un intento de lectura, podemos probar si la llamada se ejecutó correctamente o no. Es decir, si realmente se realizó una lectura de datos o si no ocurrió. Y esto se hace en este punto.
Si la lectura falla por cualquier motivo, el bucle WHILE simplemente se cerrará. Por lo tanto, no es necesario analizar las cosas. El propio intento de leer datos servirá como una prueba para indicar si la lectura fue exitosa o no. De esta manera, podremos controlar las cosas fuera de la clase C_FileBars. Pero debemos finalizar la clase de manera explícita para que la memoria en la que se alojó sea devuelta al sistema operativo. Esto se hace al llamar al destructor a través del operador DELETE. De esta manera, nos aseguramos de que la clase se haya eliminado correctamente y que no haya más referencias a ella.
El hecho de que no hagas esto de la manera que se muestra puede generar datos inconsistentes e incluso el uso de "basura" en tus programas. Pero siguiendo el procedimiento descrito anteriormente, sabrás exactamente cuándo, dónde y cómo se está utilizando una clase. Esto puede ayudarte en varios escenarios donde la modelización puede ser bastante compleja.
Una clase, varias funciones: C_ConfigService
Esta clase es bastante curiosa. A pesar de ser una clase que actúa como un puente entre la clase C_FileTicks y la clase C_Replay, de alguna manera garantiza que las cosas se mantengan según lo esperado. Y que las mejoras o cambios en el sistema de configuración solo se reflejen en los lugares donde realmente deben ser visibles. No es una clase muy extensa ni complicada en absoluto. Es simplemente una clase intermedia con un código bastante simple, por cierto. La idea es poner en ella cualquier cosa relacionada con la configuración del servicio de repetición/simulación. Básicamente, su trabajo consiste en leer el archivo de configuración y aplicar lo que está en él al servicio, para que funcione según lo configurado por el usuario.
El trabajo de esta clase se reduce a: leer y crear ticks para la simulación, aplicar las barras al activo de repetición y, en algunos casos, ajustar las variables del activo de repetición. De esta manera, el activo tendrá un comportamiento lo más parecido posible al del activo real. Su código completo, en la etapa actual de desarrollo, se puede ver a continuación:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #include "C_FileBars.mqh" #include "C_FileTicks.mqh" //+------------------------------------------------------------------+ class C_ConfigService : protected C_FileTicks { protected: //+------------------------------------------------------------------+ datetime m_dtPrevLoading; //+------------------------------------------------------------------+ private : //+------------------------------------------------------------------+ enum eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE}; //+------------------------------------------------------------------+ inline eTranscriptionDefine GetDefinition(const string &In, string &Out) { string szInfo; szInfo = In; Out = ""; StringToUpper(szInfo); StringTrimLeft(szInfo); StringTrimRight(szInfo); if (StringSubstr(szInfo, 0, 1) == "#") return Transcription_INFO; if (StringSubstr(szInfo, 0, 1) != "[") { Out = szInfo; return Transcription_INFO; } for (int c0 = 0; c0 < StringLen(szInfo); c0++) if (StringGetCharacter(szInfo, c0) > ' ') StringAdd(Out, StringSubstr(szInfo, c0, 1)); return Transcription_DEFINE; } //+------------------------------------------------------------------+ inline bool Configs(const string szInfo) { const string szList[] = { "POINTSPERTICK" }; 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_PointsPerTick = StringToDouble(szRet[1]); return true; } Print("Variável >>", szRet[0], "<< não definida."); }else Print("Definição de configuração >>", szInfo, "<< invalida."); return false; } //+------------------------------------------------------------------+ inline void FirstBarNULL(void) { MqlRates rate[1]; rate[0].close = rate[0].open = rate[0].high = rate[0].low = m_Ticks.Info[0].last; rate[0].tick_volume = 0; rate[0].real_volume = 0; rate[0].time = m_Ticks.Info[0].time - 60; CustomRatesUpdate(def_SymbolReplay, rate, 1); } //+------------------------------------------------------------------+ inline bool WhatDefine(const string szArg, char &cStage) { const string szList[] = { "[BARS]", "[TICKS]", "[TICKS->BARS]", "[BARS->TICKS]", "[CONFIG]" }; cStage = 1; for (char c0 = 0; c0 < ArraySize(szList); c0++, cStage++) if (szList[c0] == szArg) return true; return false; } //+------------------------------------------------------------------+ public : //+------------------------------------------------------------------+ bool SetSymbolReplay(const string szFileConfig) { int file, iLine; char cError, cStage; string szInfo; bool bBarPrev; C_FileBars *pFileBars; if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { MessageBox("Falha na abertura do\narquivo de configuração.", "Market Replay", MB_OK); return false; } Print("Carregando dados para replay. Aguarde...."); ArrayResize(m_Ticks.Rate, def_BarsDiary); m_Ticks.nRate = -1; m_Ticks.Rate[0].time = 0; bBarPrev = false; iLine = 1; cError = cStage = 0; 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: pFileBars = new C_FileBars(szInfo); if ((m_dtPrevLoading = (*pFileBars).LoadPreView(szInfo)) == 0) cError = 3; else bBarPrev = true; delete pFileBars; break; case 2: if (LoadTicks(szInfo) == 0) cError = 4; break; case 3: if ((m_dtPrevLoading = LoadTicks(szInfo, false)) == 0) cError = 5; else bBarPrev = true; break; case 4: if (!BarsToTicks(szInfo)) cError = 6; break; case 5: if (!Configs(szInfo)) cError = 7; break; } break; }; iLine += (cError > 0 ? 0 : 1); } FileClose(file); 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 (!bBarPrev) 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 na linha ", iLine); } return (cError == 0 ? !_StopFlag : false); } //+------------------------------------------------------------------+ };
Aquí, y específicamente aquí, comenzamos a utilizar esa tabla de herencias. Todos estos puntos son heredados de la clase C_FileTicks. De esta manera, estamos realmente ampliando las funcionalidades de la propia clase C_ConfigService. Pero no es solo eso, porque si observas con cuidado, hay una situación muy específica en la que necesitamos cargar datos de barras previas. Para hacer esto, necesitamos utilizar la clase C_FileBars. Entonces, utilizamos la misma forma que hicimos en la clase C_FileTicks cuando fue necesario cargar datos de un archivo de barras para convertirlos en ticks. La misma explicación que se dio allí también se aplica aquí.
En cierto modo, esta clase será responsable de traducir los datos contenidos en el archivo de configuración. Entonces, todo lo que necesitamos hacer es definir las cosas en los puntos adecuados para que las cosas indiquen o, mejor dicho, llamen al estado correcto. Esto es para que los valores se rellenen correctamente, o los datos se carguen correctamente. Esto se hace en dos lugares.
El primer lugar es donde indicamos en qué estado o, mejor dicho, cuál es la clave que estamos capturando o ajustando. A pesar de que no es del todo complejo, aquí tenemos simplemente lo siguiente: una lista de cosas que servirán como clave para indicar en qué estaremos trabajando en las próximas líneas en el archivo de configuración. La única atención que debes tener aquí es que esta lista debe seguir un cierto orden lógico. De lo contrario, tendremos problemas durante la traducción de los valores. Para saber en qué posición debe estar un elemento de datos, simplemente mira en la rutina SetSymbolReplay y observa lo que hacen cada uno de los valores exactamente aquí.
El segundo lugar es responsable de transcribir los valores contenidos en el archivo de configuración de la repetición/simulación en constantes que se utilizarán dentro del servicio. Aquí haremos casi lo mismo que se hizo antes. Pero esta vez, cada uno de los valores contenidos en la matriz indicará el nombre de una variable dentro de la clase. Entonces, todo lo que necesitas hacer es agregar el nombre que tendrá la variable en la lista del archivo de configuración. Luego, agregar la posición que ocupa en la lista a una llamada. De esta manera, modificarás el valor de la variable deseada. Si no has entendido bien lo que acabo de decir, no te preocupes. Pronto mostraré un ejemplo real de cómo agregar nuevas variables, ya que necesitamos definir algunas cosas adicionales aquí, en este punto específico.
A pesar de que todo parece muy bonito y genial, aún nos queda una última clase por ver.
Clase C_Replay - No estoy entendiendo nada... ¡¿Dónde están las cosas?!
Esta es la única clase con la que el servicio de repetición/simulación realmente tendrá contacto. Piensa en esta clase como una biblioteca. Pero una biblioteca cuya única función es promover un comportamiento muy similar a lo que sucedería si, en lugar de hacer una repetición o simulación, estuviéramos realmente en contacto con el mercado físico o una cuenta demo. Es decir, todo lo que necesitaremos y tendremos que hacer de hecho es implementar las cosas dentro, y solo dentro, de esta clase para que la plataforma MetaTrader 5 pueda realizar toda la simulación de la repetición como si viniera de un servidor real.
Pero si miras el código de la clase con atención y comienzas a buscar las cosas, te preguntarás: ¿Dónde están las variables, las estructuras y las rutinas que se están llamando? ¡No las encuentro en ninguna parte! Bueno, este tipo de pensamiento realmente forma parte del principio. Esto se debe a que aún no estás muy familiarizado con la herencia entre las clases. No te preocupes, estudia el código con calma y pronto comenzarás a entender cómo funciona esta herencia. Es bueno que empieces ahora, porque pronto mostraré algo que es aún más complicado y que podría confundirte mucho. Una de esas cosas es el POLIMORFISMO. Algo extremadamente útil pero que genera una gran confusión para quienes no han entendido las cuestiones involucradas en cómo funciona la herencia. Entonces estudia bien este código aquí.
Bueno, por ahora, dejemos este tema del polimorfismo para otro momento. Centrémonos en el código actual, que puedes ver a continuación:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #include "C_ConfigService.mqh" //+------------------------------------------------------------------+ class C_Replay : private C_ConfigService { private : int m_ReplayCount; long m_IdReplay; struct st01 { MqlRates Rate[1]; bool bNew; datetime memDT; int delay; }m_MountBar; //+------------------------------------------------------------------+ void AdjustPositionToReplay(const bool bViewBuider) { u_Interprocess Info; MqlRates Rate[def_BarsDiary]; int iPos, nCount; Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); if (Info.s_Infos.iPosShift == (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks)) return; iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1))); Rate[0].time = macroRemoveSec(m_Ticks.Info[iPos].time); if (iPos < m_ReplayCount) { CustomRatesDelete(def_SymbolReplay, Rate[0].time, LONG_MAX); if ((m_dtPrevLoading == 0) && (iPos == 0)) { m_ReplayCount = 0; Rate[m_ReplayCount].close = Rate[m_ReplayCount].open = Rate[m_ReplayCount].high = Rate[m_ReplayCount].low = m_Ticks.Info[iPos].last; Rate[m_ReplayCount].tick_volume = Rate[m_ReplayCount].real_volume = 0; CustomRatesUpdate(def_SymbolReplay, Rate, 1); }else { for(Rate[0].time -= 60; (m_ReplayCount > 0) && (Rate[0].time <= macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)); m_ReplayCount--); m_ReplayCount++; } }else if (iPos > m_ReplayCount) { if (bViewBuider) { Info.s_Infos.isWait = true; GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); }else { for(; Rate[0].time > m_Ticks.Info[m_ReplayCount].time; m_ReplayCount++); for (nCount = 0; m_Ticks.Rate[nCount].time < macroRemoveSec(m_Ticks.Info[iPos].time); nCount++); CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, nCount); } } for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag);) CreateBarInReplay(); Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); Info.s_Infos.isWait = false; GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); } //+------------------------------------------------------------------+ inline void CreateBarInReplay(const bool bViewMetrics = false) { #define def_Rate m_MountBar.Rate[0] static ulong _mdt = 0; int i; if (m_MountBar.bNew = (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time))) { if (bViewMetrics) { _mdt = (_mdt > 0 ? GetTickCount64() - _mdt : _mdt); i = (int) (_mdt / 1000); Print(TimeToString(m_Ticks.Info[m_ReplayCount].time, TIME_SECONDS), " - Metrica: ", i / 60, ":", i % 60, ".", (_mdt % 1000)); _mdt = GetTickCount64(); } m_MountBar.memDT = macroRemoveSec(m_Ticks.Info[m_ReplayCount].time); def_Rate.real_volume = 0; def_Rate.tick_volume = 0; } def_Rate.close = m_Ticks.Info[m_ReplayCount].last; def_Rate.open = (m_MountBar.bNew ? def_Rate.close : def_Rate.open); def_Rate.high = (m_MountBar.bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high); def_Rate.low = (m_MountBar.bNew || (def_Rate.close < def_Rate.low) ? def_Rate.close : def_Rate.low); def_Rate.real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real; def_Rate.tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > 0 ? 1 : 0); def_Rate.time = m_MountBar.memDT; m_MountBar.bNew = false; CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate, 1); m_ReplayCount++; #undef def_Rate } //+------------------------------------------------------------------+ public : //+------------------------------------------------------------------+ C_Replay(const string szFileConfig) { m_ReplayCount = 0; m_dtPrevLoading = 0; m_Ticks.nTicks = 0; m_PointsPerTick = 0; Print("************** Serviço Market Replay **************"); srand(GetTickCount()); GlobalVariableDel(def_GlobalVariableReplay); SymbolSelect(def_SymbolReplay, false); CustomSymbolDelete(def_SymbolReplay); CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay), _Symbol); CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX); CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX); SymbolSelect(def_SymbolReplay, true); m_IdReplay = (SetSymbolReplay(szFileConfig) ? 0 : -1); } //+------------------------------------------------------------------+ ~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); Print("Serviço de replay finalizado..."); } //+------------------------------------------------------------------+ bool ViewReplay(ENUM_TIMEFRAMES arg1) { u_Interprocess info; 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("Aguardando permissão do indicador [Market Replay] para iniciar replay ..."); info.u_Value.IdGraphic = m_IdReplay = ChartOpen(def_SymbolReplay, arg1); ChartApplyTemplate(m_IdReplay, "Market Replay.tpl"); ChartRedraw(m_IdReplay); GlobalVariableDel(def_GlobalVariableIdGraphics); GlobalVariableTemp(def_GlobalVariableIdGraphics); GlobalVariableSet(def_GlobalVariableIdGraphics, info.u_Value.df_Value); while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(750); return ((!_StopFlag) && (ChartSymbol(m_IdReplay) != "")); } //+------------------------------------------------------------------+ bool LoopEventOnTime(const bool bViewBuider, const bool bViewMetrics) { u_Interprocess Info; int iPos, iTest; 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); m_MountBar.delay = 0; while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag)) { CreateBarInReplay(bViewMetrics); iPos = (int)(m_ReplayCount < m_Ticks.nTicks ? m_Ticks.Info[m_ReplayCount].time_msc - m_Ticks.Info[m_ReplayCount - 1].time_msc : 0); m_MountBar.delay += (iPos < 0 ? iPos + 1000 : iPos); if (m_MountBar.delay > 400) { if (ChartSymbol(m_IdReplay) == "") break; 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(m_MountBar.delay - 20); m_MountBar.delay = 0; } } return (m_ReplayCount == m_Ticks.nTicks); } //+------------------------------------------------------------------+ }; //+------------------------------------------------------------------+ #undef macroRemoveSec #undef def_SymbolReplay //+------------------------------------------------------------------+
Como puedes notar, no es nada del otro mundo. Sin embargo, este código es mucho más amplio que el de la versión anterior. Aún así, logramos hacer todo lo que hacíamos antes. Sin embargo, quiero destacar que todos los puntos marcados no forman realmente parte de la clase C_Replay. Estos puntos están siendo heredados. Esto se debe a que no quiero que sean accesibles fuera de la clase C_Replay. Para lograrlo, estamos heredando las cosas de forma privada. Así garantizamos la integridad de la información heredada. Y esta clase cuenta con solo dos funciones que realmente pueden ser accesibles desde el exterior. Dado que el constructor y el destructor no cuentan.
Pero antes de hablar sobre el constructor y el destructor de la clase, primero echemos un vistazo a las dos funciones que pueden ser accesibles desde fuera de la clase. En un momento dado, consideré dejar solo una función. Pero por razones prácticas, decidí dejar ambas. Ya que de esta manera, sería más sencillo hacer las cosas. La función LoopEventOnTime ya se ha explorado bastante en artículos anteriores. Y dado que aquí no ha sufrido ningún tipo de modificación, no tiene sentido hablar, o mejor dicho, dar una explicación adicional. Podemos omitirla y centrarnos en la que ha experimentado cambios: la función ViewReplay.
La función ViewReplay solo ha experimentado un único cambio, que es precisamente esta prueba aquí. Esto verificará si el constructor de la clase logró inicializar la clase con éxito. Si falló, la función devolverá un valor que debería forzar la finalización del servicio de repetición. Esta es la única modificación en el código en comparación con lo que se encontró en los artículos anteriores.
Consideraciones finales
Con todos estos cambios, te sugiero que estudies y practiques lo que se ha visto aquí. Compara este código adjunto con los demás códigos vistos en los artículos anteriores. En el próximo artículo, comenzaremos a abordar un tema completamente diferente, pero bastante curioso, por decir lo menos sobre el asunto.
Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/11095
- Aplicaciones de trading gratuitas
- VPS fórex gratuito por 24 horas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso