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

Desarrollo de un sistema de repetición — Simulación de mercado (Parte 01): Primeros experimentos (I)

MetaTrader 5Probador | 7 junio 2023, 09:42
447 0
Daniel Jose
Daniel Jose

Introducción

Durante la serie de artículos "Desarrollo de un EA desde cero", me di cuenta en varios momentos de que era posible hacer mucho más de lo que se estaba haciendo programando en MQL5. Un momento en particular me hizo pensar: cuando desarrollé el sistema de gráficos Times & Trade. En ese artículo, empecé a preguntarme si era posible ir más allá de lo que se estaba construyendo.

Pues bien, una de las quejas habituales de muchos principiantes es la falta de funciones específicas en la plataforma MetaTrader 5. Entre esas funciones, una que tiene mucho sentido, en mi opinión, es un sistema de simulación o repetición del mercado. Sería conveniente que los nuevos participantes en el mercado dispusieran de alguna herramienta o mecanismo que les permitiera probar, verificar o estudiar activos, incluso activos específicos. Entre esas herramientas está precisamente el sistema de repetición y simulación.

MetaTrader 5 no incorpora esta característica en su instalación por defecto, dejando a criterio de cada usuario la forma de realizar estos estudios. Sin embargo, MetaTrader 5 es una plataforma muy adecuada para muchas tareas. Y no me refiero necesariamente a saber programar en lenguaje MQL5, sino a la programación en general.

Si no tienes mucha experiencia en este campo, estarás limitado a lo básico. Estarás totalmente desprovisto de medios más adecuados o mejores para desarrollarte realmente en el mercado, especialmente si quieres convertirte en un tráder excepcional. A menos que tengas un buen conocimiento de programación, no serás capaz de utilizar todas las características que MetaTrader 5 ofrece. Incluso programadores experimentados pueden no estar interesados en desarrollar ciertos tipos de programas o aplicaciones para MetaTrader 5.

El hecho es que pocas personas serán capaces de crear un sistema viable para los principiantes. Hay algunas propuestas gratuitas para crear un sistema de repetición de mercado, pero en mi opinión, no explotan plenamente las características que ofrece MQL5. A menudo requieren el uso de DLLs externas con código fuente cerrado, lo cual no es una buena idea porque no se conoce el origen ni el contenido de esas DLLs, lo que pone en riesgo todo el sistema.

En esta serie de artículos, todavía no sé cuántos serán necesarios para desarrollar una repetición que funcione. Te mostraré cómo puedes crear un código que te permita realizar esta repetición. Pero eso no es todo, paralelamente, desarrollaremos un sistema que te permita simular cualquier situación de mercado, por extraña o rara que sea.

Un hecho curioso es que muchas personas hablan de trade cuantitativo sin entender realmente de lo que están hablando, principalmente porque no hay una forma práctica de estudiar este tipo de cosas. Sin embargo, si entiendes los conceptos que presentaré en esta serie, serás capaz de transformar MetaTrader 5 en un sistema de análisis cuantitativo. Las posibilidades se extenderán mucho más allá de lo que voy a exponer aquí.

Para evitar repeticiones y hacer pesado el texto, me referiré al sistema sólo como repetición. Aunque lo correcto es llamarlo repetición/simulador, porque además de analizar movimientos pasados, también podemos desarrollar nuestros propios movimientos para estudiarlos. Por lo tanto, no veas este sistema sólo como una repetición de mercado, sino como un simulador de mercado o incluso como un "videojuego" de mercado, ya que implicará también mucha programación de juegos. Este tipo de programación utilizada en los juegos será necesaria en algunos puntos, pero lo veremos con calma a medida que se desarrolle el sistema.


Planificación

Lo primero que hay que hacer es entender a qué nos vamos a enfrentar. Puede sonar extraño decir esto, pero ¿sabes realmente lo que quieres hacer cuando utilizas un sistema de repetición/simulador?

Bueno, hay algunas cuestiones bastante complejas a la hora de crear una repetición de mercado. Una de ellas, quizá la principal, es la vida útil de los activos y la información sobre ellos. Si no lo entiendes, quiero que entiendas lo siguiente: el sistema comercial registra toda la información, tick a tick, de cada operación realizada para todos los activos, uno por uno. Ahora bien, ¿tienes idea de la cantidad de datos que esto representa? ¿Te has parado a pensar en el tiempo que llevaría organizar y catalogar cada uno de los activos?

Algunos activos típicos, en sus movimientos diarios, pueden contener alrededor de 80 Mb de datos, y pueden ser un poco más o un poco menos en algunos casos. Eso es sólo para un único activo en un solo día. Ahora, piensa en tener que almacenar ese mismo activo durante 1 mes... 1 año... 10 años... o tal vez para siempre. Piensa en la enorme cantidad de datos que habrá que almacenar y luego encontrar. Si te limitas a echarlos al disco, pronto no podrás encontrar nada más. Hay una frase que lo ilustra bien:

"Cuanto mayor es el espacio, mayor es el lío".

Para facilitar las cosas, al cabo de un tiempo, los datos se comprimen en barras de 1 minuto, que contienen sólo la información mínima necesaria para que podamos realizar algún tipo de estudio. Sin embargo, cuando se crea esa barra, los ticks que la componían desaparecen y dejan de ser accesibles. A partir de ese momento, ya no será posible hacer una repetición real del mercado. Tendremos entonces un simulador, ya que el movimiento real ya no es accesible, y tendremos que crear una forma de simular ese movimiento basándonos en un movimiento plausible del mercado.

Para entenderlo, observa las siguientes figuras:

                       

La secuencia de arriba muestra cómo se pierden los datos con el tiempo. La imagen de la izquierda muestra los valores reales de los ticks. Cuando se produce la compresión, tenemos la imagen del centro. Sin embargo, basándonos en ella, no podremos obtener los valores de la izquierda. Es IMPOSIBLE hacerlo. Pero podemos crear algo parecido a la imagen de la derecha, en la que estaremos simulando los movimientos del mercado basándonos en el conocimiento de cómo se comporta normalmente el mercado. Pero date cuenta de que esto no se parece en nada a la imagen original.

Es importante tener esto en cuenta cuando se trabaja con una repetición. Si no dispones de los datos originales, no podrás realizar un estudio real. Sólo podrás realizar un estudio estadístico, que puede acercarse o no a un posible movimiento real. Recuerda esto en todo momento. A lo largo de esta secuencia, exploraremos y explicaremos más sobre cómo hacer esto, pero se hará gradualmente.

Con esto, pasemos a la parte realmente difícil: implementar un sistema de repetición.


Implementación

Esta parte, aunque parece sencilla, es bastante complicada, porque hay problemas y limitaciones relacionados con el hardware, y otros problemas relacionados con el software. Así que tenemos que intentar crear algo, al menos lo más básico, que sea funcional y aceptable. No tiene sentido intentar hacer algo más complejo cuando los cimientos son débiles y se desmoronan constantemente.

Nuestro mayor y principal problema, por increíble que parezca, es el tiempo. Sí, el tiempo es un gran, por no decir enorme, reto a superar.

En el apéndice, siempre proporcionaré (en esta primera fase) al menos 2 series REALES de ticks de algún activo en cualquier periodo pasado. Estos datos ya no se pueden adquirir porque se han perdido y no se pueden descargar. Esto le servirá para estudiar todos los detalles. Sin embargo, también puedes crear tu propio banco de ticks REALES.


Creación de un banco de datos propio

Afortunadamente, MetaTrader 5 nos proporciona los medios para hacer esto. Es bastante sencillo de hacer, pero hay que hacerlo siempre, de lo contrario se pueden perder los valores y ya no será posible realizar esta tarea.

Para ello, abre la plataforma MetaTrader 5 y por defecto las teclas de acceso directo son CTRL + U. Aparecerá una pantalla delante de ti. En esta pantalla debe informar qué activo y las fechas de inicio y fin para la captura de datos, pulsa el botón para solicitar los datos y espera unos instantes. El servidor te devolverá todos los datos. Una vez hecho esto, simplemente exporta estos datos y guárdalos con cuidado, ya que son muy valiosos.

A continuación se muestra la pantalla que utilizarás para realizar la captura.

Aunque es posible crear un programa para hacer esto, creo que es mejor hacerlo manualmente. Hay cosas en las que no podemos confiar ciegamente, realmente necesitamos ver lo que se está haciendo, de lo contrario no tendremos la suficiente confianza en lo que se va a utilizar.

Esa es la parte fácil, créeme, es la parte más fácil de todo el sistema que te voy a enseñar a hacer. A partir de ahora, las cosas se van a poner mucho, mucho más complicadas.


Primera prueba de repetición

Algunos pueden pensar que esto será una tarea sencilla. Esa idea pronto será derribada. Otros pueden preguntar: ¿Por qué no utilizar el simulador de estrategias de MetaTrader 5 para hacer la repetición? La razón es que no nos permite realizar una repetición como si estuviéramos operando en el mercado. Hay muchas limitaciones y dificultades en hacer una repetición a través del simulador, y al final, no tendríamos una inmersión perfecta en la repetición como si realmente estuviéramos operando en el mercado.

Nos enfrentaremos a grandes retos, pero tenemos que dar los primeros pasos para iniciar este largo viaje. Así que vamos a empezar por hacer una implementación bastante simple. Para ello, necesitamos un evento OnTime que generará el flujo de datos para crear las barras (Candlesticks). Este evento está previsto para los Expert Advisors y los indicadores, pero no deberíamos utilizar indicadores en este caso, porque si se produce un fallo, comprometerá mucho más que el sistema de repetición. Con eso, vamos a empezar el código de la siguiente manera:

#property copyright "Daniel Jose"
#property icon "Resources\\App.ico"
#property description "Expert Advisor - Market Replay"
//+------------------------------------------------------------------+
int OnInit()
{
        EventSetTimer(60);
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
        EventKillTimer();
}
//+------------------------------------------------------------------+
void OnTick()
{
}
//+------------------------------------------------------------------+
void OnTimer()
{
}
//+------------------------------------------------------------------+

Bien, sin embargo, el código resaltado no es adecuado para nuestro propósito, porque en este caso, el período más pequeño que podemos utilizar es de 1 segundo, y eso es mucho tiempo, muy largo de hecho. Dado que los eventos de mercado ocurren en un intervalo de tiempo mucho más pequeño, necesitamos bajar a los milisegundos, y para ello, tendremos que utilizar otra función: EventSetMillisecondTimer. Sin embargo, aquí nos encontramos con un problema.


Limitaciones de la función EventSetMillisecondTimer

Veamos lo que dice la documentación:

"...  Cuando se trabaja en modo tiempo real, los eventos del temporizador no se generan más de una vez cada 10-16 milisegundos, debido a limitaciones de hardware..."

Esto podría no ser un problema, pero necesitamos hacer pruebas para verificar lo que realmente ocurre. Así que vamos a implementar un código sencillo para verificar los resultados.

Comencemos con el código del EA, que puede verse abajo en su totalidad:

#property copyright "Daniel Jose"
#property icon "Resources\\App.ico"
#property description "Expert Advisor - Market Replay"
//+------------------------------------------------------------------+
#include "Include\\C_Replay.mqh"
//+------------------------------------------------------------------+
input string user01 = "WINZ21_202110220900_202110221759";       //Arquivo de ticks
//+------------------------------------------------------------------+
C_Replay        Replay;
//+------------------------------------------------------------------+
int OnInit()
{
        Replay.CreateSymbolReplay(user01);
        EventSetMillisecondTimer(20);
        
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
        EventKillTimer();
}
//+------------------------------------------------------------------+
void OnTick() {}
//+------------------------------------------------------------------+
void OnTimer()
{
        Replay.Event_OnTime();
}
//+------------------------------------------------------------------+

Observa que tendremos un evento OnTime cada aproximadamente 20 milisegundos, indicado por la línea resaltada en el código EA. Puedes pensar que esto es demasiado rápido, pero ¿lo es realmente? Bueno, tenemos que comprobarlo. Recuerda que la documentación indica que no es posible bajar de 10 a 16 milisegundos. Así que no tiene sentido establecer un valor de 1 milisegundo, porque el evento no se generará en ese tiempo.

Observa que sólo tenemos dos referencias externas en el código del EA. Ahora vamos a conocer la clase en la que se implementan estos códigos.

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#define def_MaxSizeArray 134217727 // 128 Mbytes de posições
#define def_SymbolReplay "Replay"
//+------------------------------------------------------------------+
class C_Replay
{
};

Es importante notar que la clase tiene una definición de 128 MB, como se indica en el punto resaltado arriba. Esto significa que el archivo que contiene los datos de todos los ticks no debe tener un tamaño mayor que ese. Puedes aumentar este tamaño si lo deseas o necesitas, pero personalmente no tuve problemas con este valor.

La siguiente línea define el nombre del activo que se utilizará para la repetición. Bastante creativo de mi parte llamar al activo REPLAY, ¿no? 😂 Pero sigamos explorando la clase. El siguiente trozo de código que merece la pena destacar es el siguiente:

void CreateSymbolReplay(const string FileTicksCSV)
{
        SymbolSelect(def_SymbolReplay, false);
        CustomSymbolDelete(def_SymbolReplay);
        CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\Replay\\%s", def_SymbolReplay), _Symbol);
        CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX);
        CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX);
        SymbolSelect(def_SymbolReplay, true);
        m_IdReplay = ChartOpen(def_SymbolReplay, PERIOD_M1);
        LoadFile(FileTicksCSV);
        Print("Executando teste de velocidade.");
}

Las dos líneas resaltadas hacen cosas bastante curiosas. Para quien no lo sepa, la función CustomSymbolCreate nos crea un activo personalizado. En este caso, podemos ajustar varias cosas, pero como esto es sólo una prueba, no profundizaré demasiado por ahora. La función ChartOpen abrirá el gráfico de nuestro activo personalizado, que será la repetición. Todo muy bonito, pero necesitamos cargar nuestra repetición desde el archivo, y eso se hace con la siguiente función.

#define macroRemoveSec(A) (A - (A % 60))
                void LoadFile(const string szFileName)
                        {
                                int file;
                                string szInfo;
                                double last;
                                long    vol;
                                uchar flag;
                                
                                if ((file = FileOpen("Market Replay\\Ticks\\" + szFileName + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
                                {
                                        ArrayResize(m_ArrayInfoTicks, def_MaxSizeArray);
                                        m_ArrayCount = 0;
                                        last = 0;
                                        vol = 0;
                                        for (int c0 = 0; c0 < 7; c0++) FileReadString(file);
                                        Print("Carregando dados para Replay.\nAguarde ....");
                                        while ((!FileIsEnding(file)) && (m_ArrayCount < def_MaxSizeArray))
                                        {
                                                szInfo = FileReadString(file);
                                                szInfo += " " + FileReadString(file);                                           
                                                m_ArrayInfoTicks[m_ArrayCount].dt = macroRemoveSec(StringToTime(StringSubstr(szInfo, 0, 19)));
                                                m_ArrayInfoTicks[m_ArrayCount].milisec = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                                                m_ArrayInfoTicks[m_ArrayCount].Bid = StringToDouble(FileReadString(file));
                                                m_ArrayInfoTicks[m_ArrayCount].Ask = StringToDouble(FileReadString(file));
                                                m_ArrayInfoTicks[m_ArrayCount].Last = StringToDouble(FileReadString(file));
                                                m_ArrayInfoTicks[m_ArrayCount].Vol = StringToInteger(FileReadString(file));
                                                flag = m_ArrayInfoTicks[m_ArrayCount].flag = (uchar)StringToInteger(FileReadString(file));
                                                if (((flag & TICK_FLAG_ASK) == TICK_FLAG_ASK) || ((flag & TICK_FLAG_BID) == TICK_FLAG_BID)) continue;
                                                m_ArrayCount++;
                                        }
                                        FileClose(file);
                                        Print("Carregamento concluido.\nIniciando Replay.");
                                        return;
                                }
                                Print("Falha no acesso ao arquivo de dados de ticks.");
                                ExpertRemove();
                        };
#undef macroRemoveSec

Esta función cargará todos los datos de tick, línea por línea. En caso de que el archivo no exista o no se pueda acceder a él, ExpertRemove cerrará el Expert Advisor.

Todos los datos se almacenarán temporalmente en la memoria para acelerar el procesamiento posterior. Esto se debe a que puede estar utilizando una unidad de disco que seguramente será más lenta que la memoria del sistema. Así que es mejor asegurarse de que todos los datos están presentes desde el principio.

Pero hay algo bastante curioso en la rutina anterior: la función FileReadString. Leerá los datos hasta que encuentre algún tipo de delimitador. Es interesante observar que cuando analizamos los datos binarios de un archivo de ticks generado por MetaTrader 5 y guardado en formato CSV, tal y como se explica al principio de este artículo, obtenemos el siguiente resultado.


La región amarilla representa la cabecera del archivo y nos muestra la organización de la estructura interna que seguirá. La región verde es la primera línea de datos. Ahora observa los puntos azules, que son los delimitadores presentes en este formato. 0D y 0A indican una nueva línea, y 09 representa la tabulación (TECLA TAB). Cuando utilizo la función FileReadString, no necesito acumular los datos para probarla. La propia rutina lo hace por mí. Todo lo que necesito hacer es convertir los datos al tipo correcto. Esto lo hace muy fácil. Sin embargo, echa un vistazo al siguiente fragmento de código.

if (((flag & TICK_FLAG_ASK) == TICK_FLAG_ASK) || ((flag & TICK_FLAG_BID) == TICK_FLAG_BID)) continue;

Este fragmento evita que tengamos datos innecesarios en nuestra matriz de datos. ¿Pero por qué estoy filtrando estos valores? Porque, de hecho, no serán útiles en nuestra repetición. Pero si quieres usar esos valores, puedes permitir que pasen, pero eso requerirá filtrarlos más tarde cuando creemos las barras. Así que prefiero filtrarlos aquí.

La última rutina en nuestro sistema de pruebas se muestra a continuación:

#define macroGetMin(A)  (int)((A - (A - ((A % 3600) - (A % 60)))) / 60)
                void Event_OnTime(void)
                        {
                                bool isNew;
                                static datetime _dt = 0;
                                
                                if (m_ReplayCount >= m_ArrayCount) return;
                                if (m_dt == 0)
                                {
                                        m_Rate[0].close = m_Rate[0].open =  m_Rate[0].high = m_Rate[0].low = m_ArrayInfoTicks[m_ReplayCount].Last;
                                        m_Rate[0].tick_volume = 0;
                                        _dt = TimeLocal();
                                }
                                isNew = m_dt != m_ArrayInfoTicks[m_ReplayCount].dt;
                                m_dt = (isNew ? m_ArrayInfoTicks[m_ReplayCount].dt : m_dt);
                                m_Rate[0].close = m_ArrayInfoTicks[m_ReplayCount].Last;
                                m_Rate[0].open = (isNew ? m_Rate[0].close : m_Rate[0].open);
                                m_Rate[0].high = (isNew || (m_Rate[0].close > m_Rate[0].high) ? m_Rate[0].close : m_Rate[0].high);
                                m_Rate[0].low = (isNew || (m_Rate[0].close < m_Rate[0].low) ? m_Rate[0].close : m_Rate[0].low);
                                m_Rate[0].tick_volume = (isNew ? m_ArrayInfoTicks[m_ReplayCount].Vol : m_Rate[0].tick_volume + m_ArrayInfoTicks[m_ReplayCount].Vol);
                                m_Rate[0].time = m_dt;
                                CustomRatesUpdate(def_SymbolReplay, m_Rate, 1);
                                m_ReplayCount++;
                                if ((macroGetMin(m_dt) == 1) && (_dt > 0))
                                {
                                        Print(TimeToString(_dt, TIME_DATE | TIME_SECONDS), " ---- ", TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS));
                                        _dt = 0;
                                }
                        };
#undef macroGetMin

Este código creará las barras con un periodo de 1 minuto, que es el mínimo requerido por la plataforma para generar cualquier otro periodo del gráfico. Las partes resaltadas no forman parte del código en sí, pero son útiles para analizar la barra de 1 minuto. Nos permiten comprobar si se está creando dentro de ese marco temporal. Si tarda mucho más de 1 minuto en crearse, tendremos que hacer algo al respecto. Sin embargo, si se está creando en menos de 1 minuto, tal vez el sistema es viable desde el principio.

Ejecutando este sistema, tendremos el resultado que se muestra en el siguiente vídeo:



Mientras que algunos pueden pensar que el tiempo está muy lejos, todavía podemos hacer algunas mejoras en el código, y tal vez eso hará la diferencia.


Mejora del código

A pesar del enorme retraso, es posible que podamos mejorar un poco las cosas y ayudar a que el sistema tenga un rendimiento un poco más cercano al esperado. Sin embargo, no creo en milagros, porque conocemos las limitaciones de la función EventSetMillisecondTimer, y el problema no está relacionado con MQL5, es una limitación de hardware. Pero veamos si podemos ayudar un poco al sistema.

Observando los datos, veremos que hay varios momentos en los que el sistema simplemente no se mueve, el precio se queda quieto, o puede ocurrir que el libro absorba todas y cada una de las agresiones, y el precio simplemente no se mueva. Esto se puede observar en la imagen de abajo.

Observa que tenemos dos condiciones diferentes: una en la que el tiempo y el precio no han cambiado. Esto no indica que los datos sean incorrectos, sino que no ha habido tiempo suficiente para que la medición en milisegundos marque realmente la diferencia. Y tenemos otro tipo de evento, en el que el precio no se movió, pero el tiempo sí, pasando sólo 1 milisegundo. En ambos casos, cuando combinamos la información, puede haber una diferencia en el tiempo de creación de la barra de 1 minuto. Porque esto evitaría una llamada extra a las rutinas de creación, y cada nanosegundo, que se ahorra, puede suponer una gran diferencia al final. Y es que las cosas suman, poco a poco se consigue mucho.

Para comprobar si habrá o no alguna diferencia, tendremos que analizar la cantidad de información que se genera. Se trata de una cuestión estadística y no es exacta. Un pequeño error es aceptable. Sin embargo, tardar el tiempo que se muestra en el vídeo es totalmente inaceptable para una simulación cercana a la realidad.

Para comprobarlo, haremos una primera modificación en el código:

#define macroRemoveSec(A) (A - (A % 60))
                void LoadFile(const string szFileName)
                        {

// ... Código interno ...
                                        FileClose(file);
                                        Print("Carregamento concluido.\nFoi gerados ", m_ArrayCount, " posições de movimento.\nIniciando Replay.");
                                        return;
                                }
                                Print("Falha no acesso ao arquivo de dados de ticks.");
                                ExpertRemove();
                        };
#undef macroRemoveSec

Esta parte añadida lo hará por nosotros. Veamos ahora la primera ejecución y lo que ocurre, como se puede ver en la imagen inferior:

Ahora tenemos algún parámetro para comprobar si las modificaciones están ayudando o no. Si haces el cálculo, verás que tardó casi 3 minutos en generar 1 minuto de datos. En otras palabras, el sistema está muy lejos de lo aceptable.

Por lo tanto, vamos a implementar una pequeña mejora en el código, que se puede ver a continuación:

#define macroRemoveSec(A) (A - (A % 60))
                void LoadFile(const string szFileName)
                        {
                                int file;
                                string szInfo;
                                double last;
                                long    vol;
                                uchar flag;
                                
                                
                                if ((file = FileOpen("Market Replay\\Ticks\\" + szFileName + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
                                {
                                        ArrayResize(m_ArrayInfoTicks, def_MaxSizeArray);
                                        m_ArrayCount = 0;
                                        last = 0;
                                        vol = 0;
                                        for (int c0 = 0; c0 < 7; c0++) FileReadString(file);
                                        Print("Carregando dados para Replay.\nAguarde ....");
                                        while ((!FileIsEnding(file)) && (m_ArrayCount < def_MaxSizeArray))
                                        {
                                                szInfo = FileReadString(file);
                                                szInfo += " " + FileReadString(file);                                           
                                                m_ArrayInfoTicks[m_ArrayCount].dt = macroRemoveSec(StringToTime(StringSubstr(szInfo, 0, 19)));
                                                m_ArrayInfoTicks[m_ArrayCount].milisec = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                                                m_ArrayInfoTicks[m_ArrayCount].Bid = StringToDouble(FileReadString(file));
                                                m_ArrayInfoTicks[m_ArrayCount].Ask = StringToDouble(FileReadString(file));
                                                m_ArrayInfoTicks[m_ArrayCount].Last = StringToDouble(FileReadString(file));
                                                m_ArrayInfoTicks[m_ArrayCount].Vol = vol + StringToInteger(FileReadString(file));
                                                flag = m_ArrayInfoTicks[m_ArrayCount].flag = (uchar)StringToInteger(FileReadString(file));
                                                if (((flag & TICK_FLAG_ASK) == TICK_FLAG_ASK) || ((flag & TICK_FLAG_BID) == TICK_FLAG_BID)) continue;
                                                if (m_ArrayInfoTicks[m_ArrayCount].Last != last)
                                                {
                                                        last = m_ArrayInfoTicks[m_ArrayCount].Last;
                                                        vol = 0;
                                                        m_ArrayCount++;
                                                }else
                                                        vol += m_ArrayInfoTicks[m_ArrayCount].Vol;
                                        }
                                        FileClose(file);
                                        Print("Carregamento concluido.\nFoi gerados ", m_ArrayCount, " posições de movimento.\nIniciando Replay.");
                                        return;
                                }
                                Print("Falha no acesso ao arquivo de dados de ticks.");
                                ExpertRemove();
                        };
#undef macroRemoveSec

Añadir estas líneas resaltadas mejora significativamente los resultados, como puede verse en la imagen inferior:


Aquí, hemos mejorado el rendimiento del sistema. Puede parecer poco, pero demuestra que los primeros cambios han marcado la diferencia. Hemos conseguido un tiempo aproximado de 2 minutos y 29 segundos para generar la barra de 1 minuto. En otras palabras, se ha producido una mejora general del sistema. Sin embargo, aunque esto suene alentador, tenemos un problema que lo complica todo. No podemos reducir el tiempo entre eventos generado por la función EventSetMillisecondTimer, lo que nos hace pensar en un enfoque diferente.

Pero a pesar de todo, el sistema ha recibido una pequeña mejora, como se muestra a continuación:

                void Event_OnTime(void)
                        {
                                bool isNew;
                                static datetime _dt = 0;
                                
                                if (m_ReplayCount >= m_ArrayCount) return;
                                if (m_dt == 0)
                                {
                                        m_Rate[0].close = m_Rate[0].open =  m_Rate[0].high = m_Rate[0].low = m_ArrayInfoTicks[m_ReplayCount].Last;
                                        m_Rate[0].tick_volume = 0;
                                        m_Rate[0].time = m_ArrayInfoTicks[m_ReplayCount].dt - 60;
                                        CustomRatesUpdate(def_SymbolReplay, m_Rate, 1);
                                        _dt = TimeLocal();
                                }

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

                        }

Lo que hacen las líneas resaltadas es visible en el gráfico. Sin ellas, la primera barra siempre queda cortada, dificultando su correcta lectura. Pero cuando añadimos estas dos líneas, la visualización se vuelve mucho más agradable, permitiéndonos ver correctamente las barras generadas. Esto es de la primera barra. Es algo sencillo, pero al final marca la diferencia.

Sin embargo, volvamos a nuestra cuestión inicial, que es intentar crear un sistema adecuado para la presentación y creación de las barras. Aunque fuera posible reducir el tiempo, seguiríamos sin tener un sistema adecuado. Tendríamos que cambiar el enfoque utilizado. Eso es porque el EA no es la mejor manera de crear un sistema de repetición. Pero aún así, quiero mostrar una última cosa que puede ser curiosa para algunos. ¿Cuánto podemos realmente reducir o mejorar la formación de la barra de 1 minuto si utilizamos el menor tiempo posible para generar el evento OnTime? ¿Y si, cuando el valor no cambia dentro del mismo 1 minuto, comprimimos los datos aún más en relación a los ticks. ¿Habría alguna diferencia?


Yendo al extremo

Para ello, tenemos que hacer una última modificación en el código. Véase más abajo:

#define macroRemoveSec(A) (A - (A % 60))
#define macroGetMin(A)  (int)((A - (A - ((A % 3600) - (A % 60)))) / 60)
                void LoadFile(const string szFileName)
                        {
                                int file;
                                string szInfo;
                                double last;
                                long    vol;
                                uchar flag;
                                datetime mem_dt = 0;
                                
                                if ((file = FileOpen("Market Replay\\Ticks\\" + szFileName + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
                                {
                                        ArrayResize(m_ArrayInfoTicks, def_MaxSizeArray);
                                        m_ArrayCount = 0;
                                        last = 0;
                                        vol = 0;
                                        for (int c0 = 0; c0 < 7; c0++) FileReadString(file);
                                        Print("Carregando dados para Replay.\nAguarde ....");
                                        while ((!FileIsEnding(file)) && (m_ArrayCount < def_MaxSizeArray))
                                        {
                                                szInfo = FileReadString(file);
                                                szInfo += " " + FileReadString(file);                                           
                                                m_ArrayInfoTicks[m_ArrayCount].dt = macroRemoveSec(StringToTime(StringSubstr(szInfo, 0, 19)));
                                                m_ArrayInfoTicks[m_ArrayCount].milisec = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                                                m_ArrayInfoTicks[m_ArrayCount].Bid = StringToDouble(FileReadString(file));
                                                m_ArrayInfoTicks[m_ArrayCount].Ask = StringToDouble(FileReadString(file));
                                                m_ArrayInfoTicks[m_ArrayCount].Last = StringToDouble(FileReadString(file));
                                                m_ArrayInfoTicks[m_ArrayCount].Vol = vol + StringToInteger(FileReadString(file));
                                                flag = m_ArrayInfoTicks[m_ArrayCount].flag = (uchar)StringToInteger(FileReadString(file));
                                                if (((flag & TICK_FLAG_ASK) == TICK_FLAG_ASK) || ((flag & TICK_FLAG_BID) == TICK_FLAG_BID)) continue;
                                                if ((mem_dt == macroGetMin(m_ArrayInfoTicks[m_ArrayCount].dt)) && (last == m_ArrayInfoTicks[m_ArrayCount].Last)) vol += m_ArrayInfoTicks[m_ArrayCount].Vol; else
                                                {
                                                        mem_dt = macroGetMin(m_ArrayInfoTicks[m_ArrayCount].dt);
                                                        last = m_ArrayInfoTicks[m_ArrayCount].Last;
                                                        vol = 0;
                                                        m_ArrayCount++;
                                                }
                                        }
                                        FileClose(file);
                                        Print("Carregamento concluido.\nFoi gerados ", m_ArrayCount, " posições de movimento.\nIniciando Replay.");
                                        return;
                                }
                                Print("Falha no acesso ao arquivo de dados de ticks.");
                                ExpertRemove();
                        };
#undef macroRemoveSec
#undef macroGetMin

El código resaltado soluciona un pequeño problema que existía antes, pero que no se notaba. Cuando el precio era el mismo, pero se producía la transición de una barra a otra, la nueva barra tardaba un poco en crearse. Sin embargo, el verdadero problema era que el precio de apertura era diferente del que se mostraba en el gráfico original, pero esto se ha solucionado. Ahora, en caso de que todos los demás parámetros sean iguales o tengan una pequeña diferencia en los milisegundos, tendremos una sola posición almacenada.

Hecho esto, probamos el sistema con un EventSetMillisecondTimer de 20 y obtuvimos el siguiente resultado:

 

En este caso, el resultado fue de 2 minutos y 34 segundos para un evento de 20 milisegundos... A continuación, cambiamos el valor de EventSetMillisecondTimer a 10, que es el valor mínimo indicado en la documentación, y el resultado fue:

 

Aquí, el resultado fue 1 minuto y 56 segundos para un evento de 10 milisegundos. El resultado ha mejorado, pero todavía está muy lejos de lo que necesitamos. Y ahora no es posible reducir más el tiempo utilizando este método adoptado en este artículo, ya que la propia documentación informa de que esto no será posible o no tendremos suficiente estabilidad para proceder al siguiente paso.


Conclusión

En este artículo, he presentado los principios básicos para aquellos que deseen crear un sistema de repetición/simulador. Estos principios son la base de todo el sistema, pero para aquellos que no tienen experiencia en programación, entender cómo funciona la plataforma MetaTrader 5 puede ser un reto. Al ver estos principios aplicados en la práctica, puede haber una gran motivación para empezar a estudiar y dedicarse a aprender programación. Después de todo, las cosas sólo se vuelven interesantes cuando las ves funcionar; limitarse a mirar código tras código no es ni estimulante ni motivador.

En el momento en que te das cuenta de lo que se puede hacer y entiendes cómo funcionan las cosas, todo cambia. Es como si se abriera una puerta mágica, revelando un nuevo mundo totalmente desconocido y lleno de posibilidades. A lo largo de esta secuencia, verás cómo sucede esto. Iré desarrollando este sistema a medida que se vayan creando los artículos, así que te pido un poco de paciencia. Incluso cuando parezca que no hay progreso, siempre habrá algún avance, y el conocimiento nunca está de más. Puede que no nos haga más felices, pero siempre es valioso.

En el archivo adjunto están las dos versiones que se han tratado aquí. También encontrarás dos archivos tick reales para que puedas experimentar y ver cómo se comporta el sistema en tu propio hardware. Los resultados no serán muy diferentes de lo que he mostrado, pero puede ser bastante interesante ver cómo el ordenador maneja ciertos problemas, a menudo resolviéndolos de forma bastante creativa.

En el próximo artículo, haremos algunos cambios para intentar conseguir un sistema más adecuado. Exploraremos una solución diferente y bastante curiosa, que también será útil para aquellos que se estén iniciando en el área de la programación. Como dicen algunos,  el trabajo no ha hecho más que empezar.


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

Archivos adjuntos |
Replay.zip (10910.23 KB)
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 02): Primeros experimentos (II) Desarrollo de un sistema de repetición — Simulación de mercado (Parte 02): Primeros experimentos (II)
Intentemos esta vez un enfoque diferente para lograr el objetivo de 1 minuto. Sin embargo, esta tarea no es tan sencilla como muchos piensan.
Integración de modelos ML con el simulador de estrategias (Parte 3): Gestión de archivos CSV(II) Integración de modelos ML con el simulador de estrategias (Parte 3): Gestión de archivos CSV(II)
Este texto es una guía completa sobre la creación de una clase en MQL5 para la gestión eficaz de archivos CSV. En él comprenderás cómo se lleva a cabo la implementación de métodos de apertura, escritura, lectura y conversión de datos y cómo se pueden emplear para guardar y acceder a la información. Además, trataremos las restricciones y los aspectos cruciales a la hora de utilizar una clase de este tipo. Este es un material valioso para aquellos que deseen aprender a manipular archivos CSV en MQL5.
Redes neuronales de propagación inversa del error en matrices MQL5 Redes neuronales de propagación inversa del error en matrices MQL5
El artículo describe la teoría y la práctica de la aplicación del algoritmo de propagación inversa del error en MQL5 con la ayuda de matrices. Asimismo, incluye clases y ejemplos preparados del script, el indicador y el asesor.
Algoritmos de optimización de la población: Algoritmo de búsqueda gravitacional (GSA) Algoritmos de optimización de la población: Algoritmo de búsqueda gravitacional (GSA)
El GSA es un algoritmo de optimización basado en la población e inspirado en la naturaleza no viviente. La simulación de alta fidelidad de la interacción entre los cuerpos físicos, gracias a la ley de la gravedad de Newton presente en el algoritmo, permite observar la mágica danza de los sistemas planetarios y los cúmulos galácticos, capaz de hipnotizar en la animación. Hoy vamos a analizar uno de los algoritmos de optimización más interesantes y originales. Adjuntamos un simulador de movimiento de objetos espaciales.