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

Desarrollo de un sistema de repetición — Simulación de mercado (Parte 18): Ticks y más ticks (II)

MetaTrader 5Probador | 26 octubre 2023, 15:02
303 0
Daniel Jose
Daniel Jose

Introducción

En el artículo anterior, "Desarrollo de un sistema de repetición — Simulación de mercado (Parte 17): Ticks y más ticks (I)", comenzamos a tener la posibilidad de mostrar el gráfico de ticks en la ventana de observación del mercado. Fue algo muy positivo, pero durante ese artículo mencioné en algunos momentos que el sistema tenía algunas fallas. Por eso, decidí desactivar algunas de las funciones del servicio hasta que se corrigieran esas fallas. Ahora, vamos a corregir muchas de esas fallas que surgieron cuando comenzamos a mostrar el gráfico de ticks.

Una de las fallas más notorias y tal vez la más molesta para mí está relacionada con el tiempo de simulación para la creación de las barras de 1 minuto. Aquellos que han estado siguiendo y probando el servicio de repetición/simulación deben haber notado que el tiempo está lejos de ser ideal. Esto es aún más evidente cuando el activo tiene una cierta liquidez, lo que puede llevarnos a quedarnos sin negocios reales durante algunos segundos. Desde el principio, he intentado cuidar este aspecto, tratando de hacer que la experiencia al utilizar la repetición/simulador sea similar a la de operar un activo real.

En este caso, es extremadamente claro que las métricas están muy lejos del tiempo ideal para la creación de barras de 1 minuto. Entonces, lo primero que realmente corregiremos es precisamente esto. Corregir la cuestión de la temporización no es algo complicado. Por más increíble que parezca, en realidad es bastante simple de hacer. Sin embargo, no realicé la corrección en el artículo anterior porque allí el objetivo era explicar cómo llevar los datos de los ticks que se estaban utilizando para generar las barras de 1 minuto en el gráfico a la ventana de observación del mercado.

Si hubiera realizado la corrección del temporizador, aquellos que deseaban saber cómo aplicar los datos reales de ticks almacenados en archivo en la ventana de observación del mercado no habrían comprendido cómo se hacía. De esa manera, al enfocarnos únicamente en mostrar cómo lanzar los ticks en la ventana de observación del mercado, creo que quedó claro cómo llevar a cabo ese proceso. El detalle importante es que no encontré ninguna otra referencia sobre cómo hacerlo. La única referencia era la documentación, y durante mi búsqueda encontré incluso en el foro de la comunidad a algunas personas que también querían saber cómo hacerlo. Sin embargo, no encontraron una respuesta que realmente les ayudara a comprender cómo debería ser el proceso. Por eso, el artículo anterior parece haber terminado en un punto un tanto extraño, dando la impresión de que no sabía cómo corregir los problemas que se mencionaron allí.

Pero aquí vamos a poner las cosas en marcha de verdad. Aún no en su totalidad, ya que hay cuestiones que son mucho más complejas de explicar. A pesar de que la implementación, muchas veces, es relativamente sencilla. Explicar algunas cosas que son completamente distintas entre sí, pero que de alguna manera tienen algún tipo de conexión. Y hacerlo en un mismo artículo creo que haría que el artículo sea muy confuso. En lugar de aclarar, terminaría complicando aún más a quien quiere entender cómo hacer las cosas.

Mi idea detrás de cada artículo es explicar e incentivar a las personas a conocer y explorar al máximo tanto la plataforma MetaTrader 5 como el lenguaje MQL5. Esto va mucho más allá de lo que se puede ver en los códigos difundidos por ahí. Quiero realmente que cada uno de ustedes produzca y se sienta motivado para explorar caminos que nunca antes se hayan recorrido. Y no quedarse siempre haciendo las mismas cosas, como si MQL5 o MetaTrader 5 no sirvieran para nada más que lo que todos hacen. Pero volvamos al artículo en sí.


Implementación de la corrección del tiempo de creación de la barra de 1 minuto

Empecemos corrigiendo el temporizador. Para hacerlo, vamos a cambiar un pequeño detalle, solo un pequeño detalle en todo el código. Este cambio se puede ver en el código a continuación:

inline bool ReadAllsTicks(const bool ToReplay)
                        {
#define def_LIMIT (INT_MAX - 2)
#define def_Ticks m_Ticks.Info[m_Ticks.nTicks]

                                string   szInfo;
                                MqlRates rate;
                                
                                Print("Carregando ticks de replay. Aguarde...");
                                ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                                while ((!FileIsEnding(m_File)) && (m_Ticks.nTicks < def_LIMIT) && (!_StopFlag))
                                {
                                        ArrayResize(m_Ticks.Info, m_Ticks.nTicks + 1, def_MaxSizeArray);
                                        szInfo = FileReadString(m_File) + " " + FileReadString(m_File);
                                        def_Ticks.time = StringToTime(StringSubstr(szInfo, 0, 19));
                                        def_Ticks.time_msc = (def_Ticks.time * 1000) + (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                                        def_Ticks.time_msc = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                                        def_Ticks.bid = StringToDouble(FileReadString(m_File));
                                        def_Ticks.ask = StringToDouble(FileReadString(m_File));
                                        def_Ticks.last = StringToDouble(FileReadString(m_File));
                                        def_Ticks.volume_real = StringToDouble(FileReadString(m_File));
                                        def_Ticks.flags = (uchar)StringToInteger(FileReadString(m_File));                                       
                                        if (def_Ticks.volume_real > 0.0)
                                        {
                                                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
                                                m_Ticks.nRate += (BuiderBar1Min(rate, def_Ticks) ? 1 : 0);
                                                m_Ticks.Rate[m_Ticks.nRate] = rate;
                                        }
                                        m_Ticks.nTicks++;
                                }
                                FileClose(m_File);
                                if (m_Ticks.nTicks == def_LIMIT)
                                {
                                        Print("Excesso de dados no arquivo de tick.\nNão é possivel continuar...");
                                        return false;
                                }
                                return (!_StopFlag);
#undef def_Ticks
#undef def_LIMIT
                        }

Listo. Ahora el temporizador funcionará de manera más adecuada. Puede que estés pensando: ¿Pero cómo es eso? No entiendo 🤔. El simple hecho de eliminar una línea (esta está eliminada) y reemplazarla por un pequeño cálculo ya resuelve completamente el problema del temporizador. Pero no es solo eso. También podríamos eliminar el valor del tiempo, dejándolo en cero.

Esto nos ahorraría algunos ciclos de máquina cuando agreguemos los ticks al gráfico de observación de mercado. Pero (y este "pero" realmente me hace pensar), tendríamos que realizar un cálculo adicional al crear las barras de 1 minuto, que luego serían trazadas por MetaTrader 5. Así que al final, terminaríamos gastando algunos ciclos de máquina solo para hacer el cálculo. Haciéndolo de la manera en que se está haciendo, el gasto sería mucho menor.

Debido a este cambio, inmediatamente podremos hacer otro:

inline void ViewTick(void)
                        {
                                MqlTick tick[1];

                                tick[0] = m_Ticks.Info[m_ReplayCount];
                                tick[0].time_msc = (m_Ticks.Info[m_ReplayCount].time * 1000) + m_Ticks.Info[m_ReplayCount].time_msc;
                                CustomTicksAdd(def_SymbolReplay, tick);
                        }

El cálculo borrado anteriormente ya no es necesario. Ya que se ejecuta en el momento en que estamos cargando los ticks reales desde el archivo. Nota que si se hubiera hecho esto en el artículo anterior, muchos no habrían entendido por qué los ticks están apareciendo en el gráfico de observación del mercado. Pero, como mencioné, de esta manera creo que se ha vuelto mucho más claro y sencillo. El simple hecho del cambio más suave hace que sean mucho más comprensibles para todos los que lean estos artículos.

Ahora viene una pregunta que tal vez te preocupe:

¿Es posible que en un activo con baja liquidez, donde las transacciones pueden demorar varios segundos en ocurrir realmente, se produzca una "paralización del servicio"? ¿Podría suceder que, si decidimos detenerlo mientras se está parando, en realidad no se detenga por completo? ¿Puede esto ocurrir debido a que el temporizador permanece en modo de espera durante varios segundos?

Esta es una pregunta válida de hecho. Veamos por qué esto no ocurrirá.

                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);
                        }

El problema real es que cuando la función Sleep se alcanza, el servicio se "detendrá" durante un tiempo. Y esto es un hecho. Pero de ninguna manera, el servicio no podrá ser detenido. De hecho, puede ser detenido por un pedido de STOP cuando una llamada cambie el estado de la flag Stop. ¿Cómo lo sé? Por el simple hecho de haber visto esto en la documentación de la función Sleep. A continuación, se muestra el fragmento en el que esto queda claro.

Observación

La función Sleep() no puede ser llamada por indicadores personalizados, porque los indicadores se ejecutan en el hilo de interfaz y no deben retrasarse. La función se ha incorporado para verificar la flag interrumpida cada 0.1 segundos.

De esta manera, no necesitas estar comprobando si el servicio se ha interrumpido o no. La implementación misma de MetaTrader 5 está haciendo esto por nosotros. Esto es algo muy bueno, por cierto. El hecho de que esto se haga ya nos ahorra mucho trabajo en la creación de una forma de prueba para mantener las cosas funcionando y, al mismo tiempo, mantener la interactividad con el usuario.


Implementación de las correcciones en el sistema de navegación rápida

Ahora resolveremos el problema en el sistema de navegación para poder recuperar las cosas como eran antes. Sin embargo, hay un pequeño inconveniente que realmente no pude resolver utilizando solo MQL5. Y dado que no pretendo forzar el uso de una DLL en este momento, será necesario utilizar un pequeño detalle en la plataforma de MetaTrader 5. Esto es necesario para que las cosas estén correctas. Pero no te preocupes pensando que es algo muy complicado. De hecho, lo que se va a hacer es bastante simple y, en cierto sentido, incluso tonto. Pero para entender lo que se hará, necesito que prestes atención a lo que se explicará. Porque, a pesar de que a primera vista puede parecer casi intuitivo, es posible que no puedas entenderlo realmente si no prestas atención.

Pero de una forma u otra, primero vamos a ver cómo se codificó la cosa.

#property service
#property icon "\\Images\\Market Replay\\Icon.ico"
#property copyright "Daniel Jose"
#property version   "1.18"
#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/11113"
//+------------------------------------------------------------------+
#define def_Dependence  "\\Indicators\\Market Replay.ex5"
#resource def_Dependence
//+------------------------------------------------------------------+
#include <Market Replay\C_Replay.mqh>
//+------------------------------------------------------------------+
input string            user00 = "Mini Dolar.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;
}
//+------------------------------------------------------------------+

Ahora tenemos nuevamente el sistema, con la posibilidad de ver o no la construcción de las barras hechas por el usuario. A pesar de que parece haber sido una decisión simple de tomar, en realidad esta decisión no solo me compete a mí. Puedes desactivar esta visualización si así lo deseas, no hará ninguna diferencia en términos de codificación. La razón es que, de una forma u otra, todavía tendremos que hacer algo dentro de la plataforma MetaTrader 5 si queremos utilizar la observación del mercado con un gráfico de ticks. Esto es necesario para que el gráfico tenga valores adecuados. Pero para el gráfico normal y las líneas de precios, no será necesario ningún tipo de cambio o intervención, ya que se ajustarán correctamente. (Bueno, eso es lo que pensé al hacer las cosas de esta manera. Pero luego verás que estaba equivocado. Hay un error que realmente no sé cómo corregir. Pero eso se verá en otro artículo).

Para esto, tuve que realizar algunos cambios en la clase C_Replay. El primer cambio fue precisamente en el procedimiento de creación de las barras. Así que mira el código a continuación para entender:

inline void CreateBarInReplay(const bool bViewMetrics, const bool bViewTicks)
                        {
#define def_Rate m_MountBar.Rate[0]

                                bool bNew;
                                MqlTick tick[1];

                                if (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time))
                                {                               
                                        if (bViewMetrics) Metrics();
                                        m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                                        def_Rate.real_volume = 0;
                                        def_Rate.tick_volume = 0;
                                }
                                bNew = (def_Rate.tick_volume == 0);
                                def_Rate.close = (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : def_Rate.close);
                                def_Rate.open = (bNew ? def_Rate.close : def_Rate.open);
                                def_Rate.high = (bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high);
                                def_Rate.low = (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;
                                CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
                                tick = m_Ticks.Info[m_ReplayCount];
                                if (bViewTicks) CustomTicksAdd(def_SymbolReplay, tick);
                                m_ReplayCount++;
                                
#undef def_Rate
                        }

Fue necesario agregar un nuevo argumento a este procedimiento. Este argumento activará o desactivará el envío de los ticks a la ventana de observación del mercado. ¿Pero por qué hacer esto? Esto se debe a que, de alguna manera que no puedo explicar, no es posible actualizar el gráfico de ticks en la ventana del mercado al mismo tiempo que actualizamos el gráfico de barras. Esto ocurre en el caso de que ajustemos el servicio para que comience a ejecutarse en un punto cualquiera. Pero durante el uso normal, podemos enviar ticks tanto a la ventana de observación del mercado como al gráfico de barras sin ningún problema, lo cual es bastante extraño.

Bueno, entonces puedes pensar: ¿Entonces no podemos realmente cambiar el contenido de la ventana de observación del mercado? La respuesta a esta pregunta es SÍ, podemos. Pero no de cualquier manera. Todo lo que realmente podemos y haremos es eliminar los ticks antiguos. Pero esto no es gratuito, al menos hasta que los desarrolladores de la plataforma MetaTrader 5 corrijan el problema que involucra el uso de activos personalizados en la ventana de mercado. Esto se debe a que los ticks colocados en el activo personalizado no desaparecen de la ventana donde podemos ver los ticks personalizados. Extrañamente, permanecen allí, lo que dificulta la comprensión cuando hacemos un movimiento de regreso a una posición más antigua.

De todos modos, la rutina responsable de administrar el sistema de posición es la que se puede ver a continuación:

                void AdjustPositionToReplay(const bool bViewBuider)
                        {
                                u_Interprocess Info;
                                MqlRates       Rate[def_BarsDiary];
                                int            iPos, nCount;
                                
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                if (m_ReplayCount == 0)
                                        for (; m_Ticks.Info[m_ReplayCount].volume_real == 0; m_ReplayCount++);
                                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);
                                        CustomTicksDelete(def_SymbolReplay, m_Ticks.Info[iPos].time_msc, LONG_MAX);
                                        if ((m_dtPrevLoading == 0) && (iPos == 0)) FirstBarNULL(); 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(false, false);
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                Info.s_Infos.isWait = false;
                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                        }

El único cambio que ha experimentado esta rutina en comparación con lo que se podía ver en versiones anteriores es precisamente este punto. Simplemente una función que elimina los ticks a partir de un punto específico en el gráfico de ticks de la ventana de observación del mercado. ¿Pero por qué esta implementación no se vio antes? El detalle es que aún estaba tratando de hacer que los datos se actualizaran de manera dinámica en ambos gráficos (gráfico de barras y de ticks). Pero no pude hacerlo sin que se generaran errores y problemas relacionados con el sistema de actualización. En un momento dado, simplemente decidí que solo se actualizaría el gráfico de barras. Por eso, esta función ahora tiene 2 parámetros. 

Bueno, ahora que el sistema está casi igual que antes de implementar la visualización del gráfico de ticks y utilizando la ventana de observación del mercado, mostraré una última cosa antes de cerrar este artículo. Pero espero que hayas entendido cómo funciona la repetición/simulador cuando trabajamos con datos reales. Porque ahora agregaremos los ticks en la ventana de observación del mercado, pero cuando los datos son simulados. Y este es el tema del próximo apartado.


Uso de datos simulados en la Observación del Mercado

Dado que no quiero extender este tema sobre los ticks en la ventana de observación del mercado a otro artículo, veamos cómo hacerlo, o mejor dicho, veamos mi propuesta para este tipo de situación. Cuando se trata de simular ticks de una barra de 1 minuto, la cuestión aquí no es tan complicada. De hecho, es mucho más simple de lo que se ha hecho hasta ahora. Si has comprendido todo lo que se ha visto hasta ahora, no tendrás problemas para entender esto.

A diferencia de lo que sucede cuando estamos utilizando datos reales de negociación. Cuando vamos a usar datos simulados, inicialmente no tendremos algunos tipos de información. No es que sea imposible crearlos, sino que debes hacerlo con cuidado. La información de la que estoy hablando es cuando el precio de la última operación sale de la región limitada por el BID y el ASK. Si observas detenidamente la repetición, en algún momento donde esto ocurra, notarás que, aunque suceda, en realidad esta ruptura de la región limitada por el BID y el ASK es siempre muy rápida y de poca incidencia. Y de hecho, por mi experiencia en el mercado, este tipo de cosa sucede cuando hay un pico en la volatilidad del precio. Pero, como dije, son eventos raros.

NOTA: Entonces, no creas de ninguna manera que siempre puedes y operarás dentro del SPREAD. Puede suceder que a veces el sistema salga del spread. Es importante que lo sepas porque cuando desarrollemos el sistema de órdenes, esta información y su comprensión correcta serán esenciales. 

Hecho importante: El precio de hecho sale del BID y el ASK, pero al ver esto, no significa que haya una violación o un colapso en el sistema. Simplemente no recibimos a tiempo una actualización del servidor de negociación con los nuevos valores de BID y ASK. Pero si estás siguiendo el libro de órdenes, verás que las cosas son un poco diferentes de lo que muchos imaginan. Por eso, necesitas tener mucha experiencia con todo el sistema de negociación para realmente conocer los problemas que existen.

Entonces, sabiendo esto, incluso puedes considerar incluir tales movimientos en el sistema de simulación. Esto hará que las cosas sean un poco más realistas. Pero recuerda, siempre modera este tipo de evento. Lo ideal es que conozcas muy bien el activo en el que se simulará este tipo de movimiento. Solo así podrás hacer que las ocurrencias sean o se acerquen a las que realmente sucederían en un mercado real. Pero para que entiendas cómo incluir este tipo de movimiento en el simulador, primero veremos cómo debe implementarse el simulador para que el precio siempre se mantenga dentro de los límites definidos por el BID y el ASK.

Lo primero que debes hacer es agregar una nueva variable al sistema.

struct st00
        {
                MqlTick  Info[];
                MqlRates Rate[];
                int      nTicks,
                         nRate;
                bool     bTickReal;
        }m_Ticks;

Esta variable nos ayudará a saber si los ticks son de datos reales o de datos simulados. Es importante hacer esta distinción, ya que en realidad no simularemos los movimientos de BID y ASK. Lo que haremos es construir estos límites basados en el valor del último precio negociado generado por el simulador. Pero la razón principal es que los valores de BID y ASK no participan realmente en el valor informado en el volumen de operaciones. Entonces, para no complicar la rutina de simulación, haremos este ajuste para generar el BID y el ASK en otro lugar.

Una vez que tenemos esta nueva variable, necesitamos inicializarla adecuadamente. Entonces, hay dos lugares donde realmente se inicializará. El primero es para indicar que estamos trabajando con ticks simulados.

                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;
                                m_Ticks.bTickReal = false;
                                
                                return ((!_StopFlag) && (iMem != m_Ticks.nTicks));
                        }

Y el otro lugar donde inicializaremos esta misma variable es para indicar que estamos trabajando con ticks reales.

                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(ToReplay)) 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);
                                }
                                m_Ticks.bTickReal = true;
                                                                        
                                return dtRet;
                        };

Ahora que ya sabemos si estamos lidiando con ticks reales o simulados, podemos comenzar a trabajar. Pero antes de pasar a la clase C_Replay y comenzar a ajustar las cosas, debemos hacer un pequeño cambio en el propio simulador. Recuerda el siguiente hecho: cuando cargamos los ticks reales, ajustamos el tiempo de manera que el valor en el campo de milisegundos se ajuste para representar un momento específico. Sin embargo, el simulador aún no realiza este ajuste. Entonces, si intentas ejecutar el sistema incluso después de modificar la clase C_Replay, no obtendrás una presentación real de los datos simulados. Esto se debe a que el tiempo expresado en el campo de milisegundos está incorrecto.

Para corregir esto, haremos los siguientes cambios:

inline void Simulation(const MqlRates &rate, MqlTick &tick[])
                        {
#define macroRandomLimits(A, B) (int)(MathMin(A, B) + (((rand() & 32767) / 32767.0) * MathAbs(B - A)))

                                long     il0, max, i0, i1;
                                bool     b1 = ((rand() & 1) == 1);
                                double   v0, v1;
                                MqlRates rLocal;
                                
                                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
                                m_Ticks.Rate[++m_Ticks.nRate] = rate;
                                max = rate.tick_volume - 1;     
                                v0 = 4.0;
                                v1 = (60000 - v0) / (max + 1.0);
                                for (int c0 = 0; c0 <= max; c0++, v0 += v1)
                                {
                                        tick[c0].last = 0;
                                        tick[c0].flags = 0;
                                        il0 = (long)v0;
                                        tick[c0].time = rate.time + (datetime) (il0 / 1000);
                                        tick[c0].time_msc = (tick[c0].time * 1000) + (il0 % 1000);
                                        tick[c0].time_msc = il0 % 1000;
                                        tick[c0].volume_real = 1.0;
                                }
                                tick[0].last = rate.open;
                                tick[max].last = rate.close;
                                for (int c0 = (int)(rate.real_volume - rate.tick_volume); c0 > 0; c0--)
                                        tick[macroRandomLimits(0, max)].volume_real += 1.0;                                     
                                i0 = (long)(MathMin(max / 3.0, max * 0.2));
                                i1 = max - i0;
                                rLocal = rate;  
                                rLocal.open = rate.open;
                                rLocal.close = (b1 ? rate.high : rate.low);
                                i0 = RandomWalk(1, i0, rLocal, tick, 0);
                                rLocal.open = tick[i0].last;
                                rLocal.close = (b1 ? rate.low : rate.high);
                                RandomWalk(i0, i1, rLocal, tick, 1);
                                rLocal.open = tick[i1].last;
                                rLocal.close = rate.close;
                                RandomWalk(i1, max, rLocal, tick, 2);
                                for (int c0 = 0; c0 <= max; c0++)
                                {
                                        ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                                        m_Ticks.Info[m_Ticks.nTicks++] = tick[c0];
                                }
#undef macroRandomLimits
                        }

Eliminaremos el código borrado y en su lugar utilizaremos el código destacado. De esta manera, el tiempo en milisegundos será compatible con lo que espera la clase C_Replay. Ahora podemos pasar a ella y realizar los cambios para visualizar el contenido simulado.

En la clase C_Replay, nos centraremos en hacer cambios en una sola función. Esta se muestra en el siguiente código:

inline void CreateBarInReplay(const bool bViewMetrics, const bool bViewTicks)
                        {
#define def_Rate m_MountBar.Rate[0]

                                bool bNew;
                                MqlTick tick[1];

                                if (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time))
                                {                               
                                        if (bViewMetrics) Metrics();
                                        m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                                        def_Rate.real_volume = 0;
                                        def_Rate.tick_volume = 0;
                                }
                                bNew = (def_Rate.tick_volume == 0);
                                def_Rate.close = (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : def_Rate.close);
                                def_Rate.open = (bNew ? def_Rate.close : def_Rate.open);
                                def_Rate.high = (bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high);
                                def_Rate.low = (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;
                                CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
                                if (bViewTicks)
                                {
                                        tick = m_Ticks.Info[m_ReplayCount];
                                        if (!m_Ticks.bTickReal)
                                        {
                                                tick[0].bid = tick[0].last - m_PointsPerTick;
                                                tick[0].ask = tick[0].last + m_PointsPerTick;
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }
                                m_ReplayCount++;
                                
#undef def_Rate
                        }

Aquí tenemos un cambio bastante simple. En realidad, esto solo sirve para crear y presentar los valores de BID y ASK. Pero ten en cuenta que esta creación se basa en el valor del último precio negociado y solo se realizará cuando estemos tratando con valores simulados. Al ejecutar este código, tendrás una vista interna del gráfico generado por el sistema RANDOM WALK. Al igual que se hacía cuando usábamos EXCEL para esto. Un detalle que vale la pena mencionar en este momento es que cuando expliqué en el artículo "Desarrollo de un sistema de repetición — Simulación de mercado (Parte 15): Nacimiento del SIMULADOR (V) - RANDOM WALK" que habría otras formas de hacer la misma visualización, me refería a este modelo. Pero en ese momento, no era apropiado decir cómo hacerlo, pero ahora es otra historia.

Si la creación de estos valores de BID y ASK no se realiza de hecho, solo podrás visualizar el valor basado en el último precio simulado. Quizás esto sea suficiente para ti. Pero a algunos les gusta realmente observar el valor de BID y ASK. Sin embargo, no es del todo apropiado usar la cosa de esta manera. El hecho de que se ejecuten operaciones exactamente dentro de los límites de BID y ASK, sin que realmente sean tocados, indica que en realidad lo que está ocurriendo en el mercado son operaciones directas. Donde la negociación se produce sin que el libro de órdenes, o mejor dicho, sin que las ofertas publicadas en el libro, sean atacadas. En este caso, el precio no debería moverse. Aunque se mueva como veremos en el simulador. Por lo tanto, necesitamos corregir solo esta parte que está resaltada en verde. Esto es para que el movimiento sea al menos adecuado a lo que realmente se esperaría.

Y para no repetir todo el código anterior, nos centraremos en el fragmento destacado y lo modificaremos como se muestra a continuación:

                                if (bViewTicks)
                                {
                                        tick = m_Ticks.Info[m_ReplayCount];
                                        if (!m_Ticks.bTickReal)
                                        {
                                                static double BID, ASK;
                                                
                                                if (tick[0].last > ASK)
                                                {
                                                        ASK = tick[0].ask = tick[0].last;
                                                        BID = tick[0].bid = tick[0].last - m_PointsPerTick;
                                                }
                                                if (tick[0].last < BID)
                                                {
                                                        ASK = tick[0].ask = tick[0].last + m_PointsPerTick;
                                                        BID = tick[0].bid = tick[0].last;
                                                }
                                                tick[0].ask = tick[0].last + m_PointsPerTick;
                                                tick[0].bid = tick[0].last - m_PointsPerTick;
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }

Hemos tachado la parte que estaba destacada, que era el código original, y hemos agregado algunas otras cosas. Debes notar que los valores de BID y ASK son estáticos, y esto es necesario para que podamos construir un pequeño indicador. Algo bastante simple, pero que será suficiente para provocar una revolución. Y dado que, al iniciar el sistema, es muy probable que estos valores estén en cero (el linkeditor siempre crea una forma de que estos valores comiencen en cero), tendremos inicialmente la ejecución de la llamada donde la agresión primero será en el ASK y se creará un canal bastante estrecho, con solo 1 tick de distancia. Entonces, hasta que el último precio negociado salga de este canal, se mantendrá allí.

Como mencioné, es algo bastante simple pero funcional. Ahora, piensa en lo siguiente: No debes hacer que el valor de BID colisione con el valor de ASK (esto en el mercado de BOLSA, en el mercado de FÓREX la historia es diferente, pero esto se verá en otro momento). Quien realmente causa esta colisión es el valor del último negocio ejecutado. Pero, ¿y si hacemos un pequeño cambio en el fragmento mostrado anteriormente? Algo muy sutil. De manera que el valor de BID o ASK cambie sin que realmente el precio del último negocio haya cambiado, ¿qué sucedería?

Para verificarlo, modificaremos nuevamente el código de manera que el fragmento quede como se muestra a continuación:

                                if (bViewTicks)
                                {
                                        tick = m_Ticks.Info[m_ReplayCount];
                                        if (!m_Ticks.bTickReal)
                                        {
                                                static double BID, ASK;
                                                
                                                if (tick[0].last > ASK)
                                                {
                                                        ASK = tick[0].ask = tick[0].last;
                                                        BID = tick[0].bid = tick[0].last - (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                }
                                                if (tick[0].last < BID)
                                                {
                                                        ASK = tick[0].ask = tick[0].last + (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                        BID = tick[0].bid = tick[0].last;
                                                }
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }

Mira qué curioso. Al agregar cierta aleatoriedad al sistema, ahora logramos la inclusión de órdenes de tipo directo. Es decir, órdenes que ocurrirán sin que se toque BID o ASK. El gran detalle es que en un mercado real, este tipo de órdenes no es tan común. No de la forma en que el sistema las estará mostrando. Pero si pasas por alto este hecho, ya tendrás un buen sistema en el que en algunos momentos tendremos la inclusión de un pequeño spread entre el BID y el ASK. En otras palabras, el simulador se está adaptando casi a una situación mucho más común en el mercado real. Sin embargo, el exceso de órdenes directas es algo que debe observarse. Sin embargo, podemos evitar este exceso haciendo las cosas un poco menos aleatorias.

Para hacer esto, necesitamos hacer una última modificación, que te mostraré aquí. Esto se puede ver a continuación:

                                if (bViewTicks)
                                {
                                        tick = m_Ticks.Info[m_ReplayCount];
                                        if (!m_Ticks.bTickReal)
                                        {
                                                static double BID, ASK;
                                                double  dSpread;
                                                int     iRand = rand();
                                                
                                                dSpread = m_PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_PointsPerTick : 0 ) : 0 );
                                                if (tick[0].last > ASK)
                                                {
                                                        ASK = tick[0].ask = tick[0].last;
                                                        BID = tick[0].bid = tick[0].last - dSpread;
                                                        BID = tick[0].bid = tick[0].last - (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                }
                                                if (tick[0].last < BID)
                                                {
                                                        ASK = tick[0].ask = tick[0].last + (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                        ASK = tick[0].ask = tick[0].last + dSpread;
                                                        BID = tick[0].bid = tick[0].last;
                                                }
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }

Lo que estoy haciendo aquí es controlar el nivel de complejidad en la generación aleatoria. Esto se hace de tal manera que mantendremos todo dentro de un cierto grado adecuado de imprevisibilidad. De modo que, aún así, tendremos ocasionalmente la incidencia de órdenes directas. Pero estas se realizarán en una cantidad mucho más controlada. Para hacerlo, simplemente ajustaremos estos valores aquí. Al ajustar estos valores, creamos una pequeña ventana donde surgirá la posibilidad de un spread un poco mayor que el valor mínimo posible. Y como resultado, tendremos en algunos momentos la incidencia de órdenes directas creadas por el sistema de simulación. Algo que no era posible en artículos anteriores, o hasta ahora.


Conclusión

En este artículo, te mostré cómo se realizó el sistema de ajuste y creación de ticks en un gráfico en la observación del mercado. Comenzamos a hacer esto en el artículo anterior, sin muchas pretensiones. Terminamos generando un sistema de simulación capaz incluso de simular órdenes directas. Esto no era algo que estuviera en mis metas o objetivos para el sistema de simulación. Aunque todavía estamos bastante lejos de que la cosa sea completamente adecuada para ser utilizada en algunos tipos de sistemas de negociación. Pero lo que se hizo aquí es un comienzo.

En el próximo artículo, continuaremos nuestra saga para construir un sistema de simulación/repetición del mercado. En el archivo adjunto, tendrás acceso a 4 activos diferentes para probar y verificar el funcionamiento del sistema. Recuerda que proporcionaré tanto datos de ticks reales como de barras previas de 1 minuto para que puedas ver la diferencia entre los valores simulados y reales. Esto te permitirá comenzar a analizar las cosas de manera más profunda. Para comprender todo lo que se ha explicado aquí, deberás ejecutar el servicio de repetición/simulador en ambos modos. Es decir, primero observa cómo se presenta el activo personalizado cuando se realiza la simulación y luego observa cómo se verá cuando se realice la repetición. Pero presta atención a la ventana de ticks, no al gráfico en sí. Notarás que la diferencia es realmente NOTORIA. Al menos en lo que respecta al contenido en la ventana de observación del mercado.


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

Archivos adjuntos |
Market_Replay_rvt_18.zip (12899.62 KB)
Programación orientada a objetos (OOP) en MQL5 Programación orientada a objetos (OOP) en MQL5
Como desarrolladores, debemos aprender a crear y desarrollar software que sea reutilizable y flexible sin duplicar código, especialmente si tenemos diferentes objetos con comportamientos distintos. Esto se puede lograr fácilmente utilizando las técnicas y principios de la programación orientada a objetos. En este artículo le presentamos los conceptos básicos de la programación orientada a objetos en MQL5.
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 17): Ticks y más ticks (I) Desarrollo de un sistema de repetición — Simulación de mercado (Parte 17): Ticks y más ticks (I)
Aquí vamos a empezar a ver cómo implementar algo realmente interesante y curioso. Pero al mismo tiempo, es extremadamente complicado debido a algunas cuestiones que muchos confunden. Y lo peor que puede pasar es que algunos operadores que se autodenominan profesionales no tienen idea de la importancia de estos conceptos en el mercado de capitales. Sí, a pesar de que el enfoque aquí es la programación, comprender algunas cuestiones relacionadas con las operaciones en los mercados es de suma importancia para lo que vamos a empezar a implementar aquí.
Teoría de categorías en MQL5 (Parte 11): Grafos Teoría de categorías en MQL5 (Parte 11): Grafos
El presente artículo continúa la serie sobre la implementación de la teoría de categorías en MQL5. Aquí veremos cómo podemos integrar la teoría de grafos con los monoides y otras estructuras de datos al desarrollar una estrategia de cierre del sistema comercial.
DoEasy. Elementos de control (Parte 32): "ScrollBar" horizontal, desplazamiento con la rueda del ratón DoEasy. Elementos de control (Parte 32): "ScrollBar" horizontal, desplazamiento con la rueda del ratón
En este artículo completaremos el desarrollo de la funcionalidad del objeto de barra de desplazamiento horizontal. Asimismo, haremos posible el desplazamiento del contenido del contenedor moviendo el control deslizante de la barra de desplazamiento y girando la rueda del ratón. También introduciremos ciertas adiciones a la biblioteca considerando la nueva política de ejecución de órdenes aparecida en el terminal y los nuevos códigos de error de ejecución en MQL5.