English Русский 中文 Deutsch 日本語 Português
preview
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)

MetaTrader 5Probador | 7 junio 2023, 09:46
406 3
Daniel Jose
Daniel Jose

Introducción

En el artículo anterior, titulado "Desarrollo de un sistema de repetición - Simulación de mercado (Parte 01): Primeros Experimentos (I)", mostré las limitaciones de intentar construir un sistema de eventos con un tiempo de ejecución lo suficientemente corto como para generar una simulación de mercado adecuada. Quedó claro que no es posible conseguir menos de 10 milisegundos utilizando ese enfoque. En muchos casos, ese tiempo es bastante bajo. Sin embargo, si has examinado los archivos adjuntos al artículo, te habrás dado cuenta de que 10 milisegundos no es un tiempo suficientemente bajo. Entonces, ¿existe otro método que nos permita alcanzar un tiempo de 1 ó 2 milisegundos?

Antes de empezar a considerar cualquier cosa relacionada con el uso de tiempos en el rango de unos pocos milisegundos, es importante recordar a todo el mundo que no se trata de una tarea realmente sencilla. La razón principal es que el propio temporizador proporcionado por el sistema operativo no puede alcanzar estos valores. Por lo tanto, tenemos un gran, por no decir GIGANTE, problema entre manos. En este artículo, intentaré responder a esta pregunta y mostrar cómo podemos intentar resolverlo yendo más allá del límite de tiempo establecido por el sistema operativo. Sé que muchos pueden pensar que una CPU moderna es capaz de hacer miles de millones de cálculos por segundo. Sin embargo, una cosa es que la CPU realice cálculos y otra cosa es garantizar que todo lo que se ejecuta en el ordenador pueda manejar la tarea que necesitamos realizar. Es importante señalar que la propuesta aquí es utilizar MQL5 exclusivamente para esto, sin utilizar ningún código externo o DLLs, sólo MQL5 puro.


Planificación

Bien, para verificar esto, tendremos que hacer algunos cambios en la metodología. Si el proceso tiene éxito, no tendremos que volver a retocar el sistema de creación de ticks. Nos centraremos en otras cuestiones que nos ayuden a realizar un estudio o entrenamiento utilizando valores de tick reales o valores simulados. El ensamblaje de las barras de 1 minuto seguirá siendo el mismo. Ese será el objetivo principal de este artículo.

Así pues, adoptaremos un enfoque lo más genérico posible, y la mejor forma que encontré fue utilizar un modelo similar al cliente-servidor. Esta técnica ya la comenté en otro artículo titulado "Desarrollando un EA de trading desde cero (Parte 16): Acceso a los datos en la Web (II)". En él, presenté tres formas de transmitir información dentro de MetaTrader 5. Aquí, utilizaremos una de esas formas, concretamente un SERVICIO. Sí, la repetición de mercado será un servicio de MetaTrader 5.

Quizás estés pensando que voy a crear todo desde cero, pero yo me pregunto " ¡¿Por qué iba a hacer eso?!". El sistema ya está funcionando, sin embargo no está alcanzando el marco de tiempo deseado de 1 minuto. Y puede que me mires con una expresión que diga: " ¡¿Y crees que con cambiar a un servicio se solucionará?!?". En realidad, cambiar el sistema a un servicio no resolverá nuestro problema. Sin embargo, si aislamos, desde el principio, la creación de las barras de 1 minuto del resto del sistema EA, tendremos menos trabajo después, porque el propio EA provocará una ligera latencia en la ejecución de la construcción de la barra, y la razón de esto la explicaremos más adelante.

¿Ahora ves por qué usamos un servicio? Es más práctico que los otros métodos discutidos anteriormente. Podremos controlarlo tal y como expliqué en el artículo sobre el intercambio de mensajes entre un EA y un servicio, titulado "Desarrollando un EA comercial desde cero (Parte 17): Acceso a los datos en la web (III)". Sin embargo, aquí no nos centraremos en generar este control, sólo queremos que el servicio genere las barras que se colocarán en el gráfico. Pero para hacer las cosas más interesantes, vamos a utilizar la plataforma de una manera más creativa, no limitada sólo al uso de un EA y un servicio.

Sólo para recapitular, en el último intento de reducir el tiempo, obtuvimos el resultado que se muestra a continuación:

Esa fue la mejor marca. Aquí, vamos a batir ese tiempo desde el principio. Sin embargo, no quiero que te obsesiones totalmente con esos valores o con las pruebas que aquí se muestran. Esta secuencia de artículos sobre la creación de un sistema de repetición/simulador ya está en una fase mucho más avanzada, en la que he cambiado varias veces varios conceptos para que el sistema funcione realmente como se espera. A pesar de que, en este punto, lo que se presentará parece adecuado y alentador, cometí algunos errores o equivocaciones relacionadas con las pruebas de temporización. Estos errores no son simples de notar o entender en una etapa temprana del sistema. A medida que esta secuencia de artículos se desarrolla, te darás cuenta de que la cuestión de la sincronización es mucho más compleja e implica más cosas que simplemente conseguir que la CPU y la plataforma MetaTrader 5 arrojen una cierta cantidad de datos en el gráfico para una inmersión en el sistema de repetición/simulador.

Así que no tomes todo al pie de la letra de lo que se verá aquí. Sigue esta secuencia de artículos, ya que lo que se hará aquí está lejos de ser algo simple y fácil de lograr.


Implementación

Empecemos por crear las bases de nuestro sistema, que incluyen:

  1. Servicio para crear las barras de 1 minuto;
  2. Script para iniciar el servicio;
  3. EA para realizar las simulaciones (esto se desarrollará más adelante);


Definición del servicio de repetición de mercado

Para trabajar correctamente con el servicio, necesitamos actualizar nuestra clase C_Replay. Sin embargo, los cambios son tan menores que no entraré en detalles, pero básicamente se trata de código de retorno. Sin embargo, hay un punto que vale la pena destacar ya que hará algo extra, y el código se muestra a continuación.

#define macroGetMin(A)  (int)((A - (A - ((A % 3600) - (A % 60)))) / 60)
                int Event_OnTime(void)
                        {
                                bool isNew;
                                int mili;
                                static datetime _dt = 0;
                                
                                if (m_ReplayCount >= m_ArrayCount) return -1;
                                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();
                                }
                                isNew = m_dt != m_ArrayInfoTicks[m_ReplayCount].dt;
                                m_dt = (isNew ? m_ArrayInfoTicks[m_ReplayCount].dt : m_dt);
                                mili = m_ArrayInfoTicks[m_ReplayCount].milisec;
                                while (mili == m_ArrayInfoTicks[m_ReplayCount].milisec)
                                {
                                        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);
                                        isNew = false;
                                        m_ReplayCount++;
                                }
                                m_Rate[0].time = m_dt;
                                CustomRatesUpdate(def_SymbolReplay, m_Rate, 1);
                                mili = (m_ArrayInfoTicks[m_ReplayCount].milisec < mili ? m_ArrayInfoTicks[m_ReplayCount].milisec + (1000 - mili) : m_ArrayInfoTicks[m_ReplayCount].milisec - mili);
                                if ((macroGetMin(m_dt) == 1) && (_dt > 0))
                                {
                                        Print("Tempo decorrido : ", TimeToString(TimeLocal() - _dt, TIME_SECONDS));
                                        _dt = 0;
                                }                               
                                return (mili < 0 ? 0 : mili);
                        };
#undef macroGetMin

Las partes resaltadas fueron agregadas al código original de la clase C_Replay. Lo que estamos haciendo es definir un tiempo de retardo, es decir, vamos a utilizar exactamente el valor informado por la línea, pero en milisegundos. Recuerda que este tiempo no será exacto, porque depende de algunas variables. Sin embargo, intentaremos que sea lo más cercano posible a 1 milisegundo.

Una vez hechos esos cambios, veamos el código del servicio, que está abajo en su totalidad:

#property service
#property copyright "Daniel Jose"
#property version   "1.00"
//+------------------------------------------------------------------+
#include <Market Replay\C_Replay.mqh>
//+------------------------------------------------------------------+
input string    user01 = "WINZ21_202110220900_202110221759"; //Arquivo com ticks
//+------------------------------------------------------------------+
C_Replay Replay;
//+------------------------------------------------------------------+
void OnStart()
{
        ulong t1;
        int delay = 3;
        
        if (!Replay.CreateSymbolReplay(user01)) return;
        Print("Aguardando permissão para iniciar replay ...");
        GlobalVariableTemp(def_GlobalVariable01);
        while (!GlobalVariableCheck(def_SymbolReplay)) Sleep(750);
        Print("Serviço de replay iniciado ...");
        t1 = GetTickCount64();
        while (GlobalVariableCheck(def_SymbolReplay))
        {
                if ((GetTickCount64() - t1) >= (uint)(delay))
                {
                        if ((delay = Replay.Event_OnTime()) < 0) break;
                        t1 = GetTickCount64();
                }
        }
        GlobalVariableDel(def_GlobalVariable01);
        Print("Serviço de replay finalizado ...");
}
//+------------------------------------------------------------------+

El código anterior es el responsable de crear las barras.  Colocando este código aquí, el sistema de repetición funcionará de forma independiente, estando poco influenciado por el funcionamiento de la plataforma MetaTrader 5. Esto nos permitirá trabajar en otras áreas relacionadas con el control, análisis y simulación de la repetición. Sin embargo, esto se hará más adelante.

Ahora una cosa interesante: observa en las zonas resaltadas que tenemos una función GetTickCount64. Nos proporcionará un sistema equivalente al visto en el artículo anterior, pero con una ventaja, aquí la resolución bajará a 1 milisegundo. La precisión no es exacta, es aproximada, pero el nivel de aproximación es muy cercano a lo que sería el movimiento real del mercado. Esto es independiente del hardware que se utilice. No obstante, se puede crear un bucle que garantice una mayor precisión, pero requerirá mucho trabajo, porque dependerá del tipo de hardware que se esté utilizando.

El siguiente paso es el script, que se muestra completo justo debajo:

#property copyright "Daniel Jose"
#property version   "1.00"
//+------------------------------------------------------------------+
#include <Market Replay\C_Replay.mqh>
//+------------------------------------------------------------------+
C_Replay Replay;
//+------------------------------------------------------------------+
void OnStart()
{
        Print("Aguardando Serviço de Replay ...");
        while((!GlobalVariableCheck(def_GlobalVariable01)) && (!IsStopped())) Sleep(500);
        if (IsStopped()) return;
        Replay.ViewReplay();
        GlobalVariableTemp(def_SymbolReplay);
        while ((!IsStopped()) && (GlobalVariableCheck(def_GlobalVariable01))) Sleep(500);
        GlobalVariableDel(def_SymbolReplay);
        Print("Script de Replay finalizado...");
        Replay.CloseReplay();
}
//+------------------------------------------------------------------+

Ambos códigos son bastante simples, como puede verse. Pero se comunican entre sí a través de variables globales mantenidas por la plataforma. De esta forma, tenemos el siguiente esquema:

Este esquema será mantenido por la propia plataforma. Si el script finaliza, el servicio se detendrá. Si el servicio se detiene, el símbolo que estamos utilizando para ejecutar la repetición dejará de recibir datos. Esto hace las cosas super sencillas y altamente sostenibles. Cualquier mejora, ya sea en la plataforma o en el hardware, se reflejará automáticamente en el rendimiento general. Esto es debido a las pequeñas latencias que se producen durante cada una de las operaciones realizadas por el proceso de servicio. Sólo afectará efectivamente al sistema en su conjunto, no tenemos que preocuparnos por el script o el EA que se desarrollen en el futuro. Cualquier mejora afectará únicamente al servicio.

Para ahorrarte la molestia de probar el sistema, puedes previsualizar el resultado en la imagen inferior. Así, tú, querido lector, no tienes que esperar un minuto entero para ver el resultado en tu gráfico.

Observa que el resultado está muy cerca del ideal. Los 9 segundos extra pueden eliminarse fácilmente con ajustes en el sistema. Lo ideal es que el tiempo sea inferior a 1 minuto, lo que facilita los ajustes, ya que sólo tenemos que añadir un retardo en el sistema. Es más fácil añadir latencia que reducirla. Pero si crees que no se puede reducir el tiempo del sistema, veámoslo más a fondo.

Si nos fijamos, hay un punto que genera un retraso en el sistema, y está en el servicio. El punto que realmente genera este retraso está resaltado en el siguiente extracto. Pero, ¿qué pasaría si convirtiéramos esa línea en un comentario? ¿Qué pasaría con el sistema?

        t1 = GetTickCount64();
        while (GlobalVariableCheck(def_SymbolReplay))
        {
// ...  COMENTARIO ...  if ((GetTickCount64() - t1) >= (uint)(delay))
                {
                        if ((delay = Replay.Event_OnTime()) < 0) break;
                        t1 = GetTickCount64();
                }
        }
        GlobalVariableDel(def_GlobalVariable01);

La línea resaltada ya no se ejecutará. En ese caso, te ahorraré probar el sistema localmente y tener que esperar un minuto más. Puedes ver el resultado de la ejecución en el siguiente vídeo. Puedes verlo entero o saltar a la parte en la que muestro sólo el resultado final. Siéntete libre de elegir.



En resumen, la gran dificultad es generar un retraso correctamente. Pero la pequeña variación en el tiempo de 1 minuto para crear la barra no es realmente un problema. Incluso en una cuenta real, no tendremos un tiempo exacto porque hay un pequeño retraso debido a la latencia en la transmisión de la información. Este retraso es bastante pequeño, pero aún así existe.


Velocidad máxima. ¿Es realmente así?

Aquí haremos un último intento para que el sistema funcione por debajo de 1 minuto.

Al observar los valores en milisegundos, te darás cuenta de que a veces hay una variación de sólo 1 milisegundo entre una línea y otra. Sin embargo, todos estos valores están dentro del mismo segundo. Por lo tanto, podemos hacer un pequeño cambio en el código. Le añadiremos un bucle, que puede suponer una gran diferencia en el conjunto del sistema.

El cambio se muestra a continuación:

#define macroGetMin(A)  (int)((A - (A - ((A % 3600) - (A % 60)))) / 60)
inline int Event_OnTime(void)
                        {
                                bool isNew;
                                int mili;
                                static datetime _dt = 0;
                                
                                if (m_ReplayCount >= m_ArrayCount) return -1;
                                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();
                                }
                                isNew = m_dt != m_ArrayInfoTicks[m_ReplayCount].dt;
                                m_dt = (isNew ? m_ArrayInfoTicks[m_ReplayCount].dt : m_dt);
                                mili = m_ArrayInfoTicks[m_ReplayCount].milisec;
                                do
                                {
                                        while (mili == m_ArrayInfoTicks[m_ReplayCount].milisec)
                                        {
                                                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);
                                                isNew = false;
                                                m_ReplayCount++;
                                        }
                                        mili++;
                                }while (mili == m_ArrayInfoTicks[m_ReplayCount].milisec);
                                m_Rate[0].time = m_dt;
                                CustomRatesUpdate(def_SymbolReplay, m_Rate, 1);
                                mili = (m_ArrayInfoTicks[m_ReplayCount].milisec < mili ? m_ArrayInfoTicks[m_ReplayCount].milisec + (1000 - mili) : m_ArrayInfoTicks[m_ReplayCount].milisec - mili);
                                if ((macroGetMin(m_dt) == 1) && (_dt > 0))
                                {
                                        Print("Tempo decorrido : ", TimeToString(TimeLocal() - _dt, TIME_SECONDS));
                                        _dt = 0;
                                }                               
                                return (mili < 0 ? 0 : mili);
                        };
#undef macroGetMin

Observa que ahora tenemos un bucle externo que realiza esta prueba de 1 milisegundo. Dado que es muy difícil ajustar adecuadamente dentro del sistema para aprovechar ese único milisegundo, podría ser mejor eliminarlo.

Bueno, sólo hemos hecho este cambio, pero echa un vistazo al resultado en el vídeo a continuación.



Para los que quieran algo aún más rápido, comprueben el resultado:

Creo que es suficiente. Ahora tenemos la creación de la barra de 1 minuto por debajo de ese tiempo. Podemos hacer ajustes para lograr el tiempo perfecto mediante la adición de retrasos en el sistema. Sin embargo, no lo haré, porque la idea es tener un sistema que nos permita realizar estudios simulados. Cualquier tiempo cercano a 1 minuto está bien para que el operador se entrene y practique. No hace falta que sea algo exacto.


Conclusión

Ahora que tenemos el sistema básico para crear la repetición, podemos pasar a los siguientes puntos. Es importante señalar que todo se resolvió sólo con ajustes y el uso de las funciones presentes en el propio lenguaje MQL5, lo que demuestra que es capaz de hacer mucho más de lo que muchos creen que es posible.

Sin embargo, es importante señalar que el trabajo no ha hecho más que empezar. Aún queda mucho por hacer.

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

Archivos adjuntos |
Replay.zip (10746.69 KB)
Miguel Carmona
Miguel Carmona | 17 jun. 2023 en 01:18
Saludos Daniel. 

Antes que nada quiero agradecerte por toda la información y el apoyo que nos brindas con tus publicaciones. 
Estoy siguiendo este gran proyecto "Desarrollo de un sistema de repetición", pero me ha surgido un problema que no he podido solucionar, el cual expongo a continuación;
Al exportar los "Ticks" de MT5  para crear la base de datos e ingresarlos para el programa "C_Replay", me da el siguiente error "Foi gerados 0 posições de movimento" he intentado resolverlo pero no he tenido éxito. Quizás es un problema muy básico pero en verdad no he encontrado la manera de resolverlo. Me he percatado de que las columnas de <LAST><VOLUME> no tienen datos, pero así es como los proporciona MT5. 

Gracias por tu atención. 

Miguel Carmona
Miguel Carmona | 17 jun. 2023 en 05:51
Miguel Carmona #:
Saludos Daniel. 

Antes que nada quiero agradecerte por toda la información y el apoyo que nos brindas con tus publicaciones. 
Estoy siguiendo este gran proyecto "Desarrollo de un sistema de repetición", pero me ha surgido un problema que no he podido solucionar, el cual expongo a continuación;
Al exportar los "Ticks" de MT5  para crear la base de datos e ingresarlos para el programa "C_Replay", me da el siguiente error "Foi gerados 0 posições de movimento" he intentado resolverlo pero no he tenido éxito. Quizás es un problema muy básico pero en verdad no he encontrado la manera de resolverlo. Me he percatado de que las columnas de <LAST><VOLUME> no tienen datos, pero así es como los proporciona MT5. 

Gracias por tu atención. 





Encontré una solución. 

if (((flag & TICK_FLAG_ASK) == TICK_FLAG_ASK) || ((flag & TICK_FLAG_BID) == TICK_FLAG_BID)) continue;
Se debe adaptar la línea anterior con base en los datos proporcionados para el programa "C Replay". 
Daniel Jose
Daniel Jose | 21 jun. 2023 en 12:49
Miguel Carmona # :


Eu encontrei uma solução.

A linha anterior deve ser adaptada   com base nos dados fornecidos para o programa "C Replay".

Gracias por la sugerencia y por interesarte en el artículo. Pero sigue la secuencia y verás cuál fue la solución que encontré en el momento en que se escribieron estos artículos. Creo que comenzarás a ver el mercado de una manera diferente.

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.
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)
¿Qué te parece crear un sistema para estudiar el mercado cuando está cerrado o simular situaciones de mercado? Aquí iniciaremos una nueva secuencia de artículos para tratar este tema.
Algoritmos de optimización de la población: Búsqueda armónica (HS) Algoritmos de optimización de la población: Búsqueda armónica (HS)
Hoy estudiaremos y pondremos a prueba un algoritmo de optimización muy potente, la búsqueda armónica (HS), que se inspira en el proceso de búsqueda de la armonía sonora perfecta. ¿Qué algoritmo lidera ahora mismo nuestra clasificación?
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.