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

Desarrollo de un sistema de repetición — Simulación de mercado (Parte 09): Eventos personalizados

MetaTrader 5Ejemplos | 21 agosto 2023, 08:22
163 0
Daniel Jose
Daniel Jose

Introducción

En el artículo anterior, "Desarrollo de un sistema de repetición — Simulación de mercado (Parte 08): Bloqueo del indicador, te mostré cómo bloquear el indicador de control. Aunque hemos logrado cumplir esta tarea, aún quedan algunos aspectos por resolver. Si te has fijado bien, te habrás dado cuenta de que cada vez que cambias el punto de inicio de la repetición/simulación, se muestra brevemente la construcción de las barras de negociación. Esto, en cierto modo, no representa un problema significativo. Puede ser interesante para algunos y menos relevante para otros. Ahora pretendemos complacer a todos. Estudiaremos cómo implementar el servicio de repetición/simulador para adaptarlo a las preferencias individuales, permitiendo la visualización opcional de la construcción de las barras.


De camino a agradar a griegos y troyanos

El primer paso consiste en añadir una nueva variable o parámetro al archivo de servicio:

input string            user00 = "Config.txt";  //Arquivo de configuração do Replay.
input ENUM_TIMEFRAMES   user01 = PERIOD_M5;     //Tempo gráfico inicial.
input bool              user02 = true;          //Visualizar a construção das barras.

Con ello, comenzamos el proceso de permitir que el usuario tome la decisión. Como ya hemos dicho, hay personas que disfrutan viendo cómo se crean las barras, mientras que para otras, esto es indiferente.

Una vez completado este paso, pasaremos este parámetro a la clase C_Replay en el siguiente punto:

while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value)) && (!_StopFlag))
{
        if (!Info.s_Infos.isPlay)
        {
                if (!bTest) bTest = true;
        }else
        {
                if (bTest)
                {
                        delay = ((delay = Replay.AdjustPositionReplay(user02)) >= 0 ? 3 : delay);
                        bTest = false;
                        t1 = GetTickCount64();
                }else if ((GetTickCount64() - t1) >= (uint)(delay))
                {
                        if ((delay = Replay.Event_OnTime()) < 0) break;
                        t1 = GetTickCount64();
                }
        }
}

Ahora ya podemos entrar en la clase C_Replay y empezar a trabajar en ella. Aunque pueda parecer una tarea sencilla de realizar, está plagada de obstáculos y desafíos. Hasta ahora, los datos de repetición de mercado se basaban en ticks negociados, y el gráfico se construía con barras de 1 minuto. Así que no se trata sólo de añadir o quitar barras. Tenemos que tratar los distintos elementos de manera que sean uniformes. Todo un reto, ¿verdad? Sin embargo, me gustan los retos, y éste resulta bastante interesante de resolver.

El primer paso es, mientras leemos el fichero de ticks negociados, crear simultáneamente las barras de un minuto. Sin embargo, también hay que tener en cuenta otro aspecto. Procedamos con cautela, encontrando una solución para cada problema. Así es como abordaremos este reto. Por lo tanto, desde el principio, introduciremos un nuevo conjunto de variables en el sistema.

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

Este conjunto contendrá las barras de 1 minuto, que construiremos simultáneamente con la lectura del fichero de ticks. Al examinar el código hasta este punto, nos daremos cuenta de que la rutina Event_OnTime presente en la clase C_Replay tiene la capacidad de construir las barras de un minuto basándose en los valores de los ticks negociados. Sin embargo, no podemos invocar a esta rutina para que realice esta tarea por nosotros. De hecho, podríamos hacerlo con el debido cuidado, eliminando al final del proceso todas las barras creadas en el activo de repetición. De esta forma, el sistema estaría listo para su uso. Sin embargo, la forma en que funciona Event_OnTime genera un pequeño retardo con cada llamada, y el número de llamadas relacionadas con los ticks negociados suele ser considerablemente alto. Tendremos que adoptar un enfoque algo diferente.

Como ya hemos mencionado, deberemos buscar un enfoque algo diferente a la solución más obvia. Y así, surge la siguiente función:

inline bool BuiderBar1Min(MqlRates &rate, const MqlTick &tick)
                {
                        if (rate.time != macroRemoveSec(tick.time))
                        {
                                rate.real_volume = (long) tick.volume_real;
                                rate.tick_volume = 0;
                                rate.time = macroRemoveSec(tick.time);
                                rate.open = rate.low = rate.high = rate.close = tick.last;
                
                                return true;
                        }
                        rate.close = tick.last;
                        rate.high = (rate.close > rate.high ? rate.close : rate.high);
                        rate.low = (rate.close < rate.low ? rate.close : rate.low);
                        rate.real_volume += (long) tick.volume_real;
        
                        return false;
                }

Lo que hacemos aquí es esencialmente lo mismo que haría Event_OnTime. Sin embargo, lo hacemos tick a tick. Vamos a dar una breve explicación de lo que está ocurriendo: cuando el tiempo proporcionado en el tick sea diferente del tiempo registrado en la barra, tendremos la construcción inicial de la barra. Devolveremos "true" para indicar al autor de llamada que se creará una nueva barra, permitiéndole realizar los ajustes necesarios. En llamadas posteriores, ajustaremos los valores apropiadamente. En este caso, devolveremos "false" para indicar que no se ha creado una nueva barra. La rutina en sí es bastante sencilla, pero debemos tomar algunas precauciones al utilizarla.

La primera medida de precaución es asegurarnos de que inicializamos el array correctamente. Veamos a continuación dónde se hace esto.

bool SetSymbolReplay(const string szFileConfig)
{
        int     file;
        string  szInfo;
        bool    isBars = true;
                                
        if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
        {
                MessageBox("Falha na abertura do\narquivo de configuração.", "Market Replay", MB_OK);
                return false;
        }
        Print("Carregando dados para replay. Aguarde....");
        ArrayResize(m_Ticks.Rate, 540);
        m_Ticks.nRate = -1;
        m_Ticks.Rate[0].time = 0;
        while ((!FileIsEnding(file)) && (!_StopFlag))
        {
                szInfo = FileReadString(file);
                StringToUpper(szInfo);
                if (szInfo == def_STR_FilesBar) isBars = true; else
                if (szInfo == def_STR_FilesTicks) isBars = false; else
                if (szInfo != "") if (!(isBars ? LoadPrevBars(szInfo) : LoadTicksReplay(szInfo)))
                {
                        if (!_StopFlag)
                                MessageBox(StringFormat("O arquivo %s de %s\nnão pode ser carregado.", szInfo, (isBars ? def_STR_FilesBar : def_STR_FilesTicks), "Market Replay", MB_OK));
                        FileClose(file);
                        return false;
                }
        }
        FileClose(file);
        return (!_StopFlag);
}

Si esto no se hace correctamente y con antelación, no será posible utilizar la función de creación de las barras correctamente. La pregunta que surge entonces es: ¿Por qué estoy indicando el valor -1 en el índice del primer array? ¿No debería ser 0 el valor inicial? Sí, es 0, pero empiezo con -1 por la primera llamada que siempre dará como resultado true. Si se iniciara como 0, tendríamos que realizar una prueba adicional inmediatamente después de la llamada de construcción de la barra. Sin embargo, al poner -1, esta prueba adicional se hace innecesaria. Es importante notar que estamos inicializando al array con 540 posiciones, lo que corresponde al número de barras de 1 minuto normalmente presentes en un día típico de negociación en B3 (Bolsa de Valores de Brasil).

Una vez completado este paso, podemos pasar a la fase de lectura de los ticks negociados.

bool LoadTicksReplay(const string szFileNameCSV)
{
        int     file,
                old;
        string  szInfo = "";
        MqlTick tick;
        MqlRates rate;
                                
        if ((file = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
        {
                ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                ArrayResize(m_Ticks.Rate, 540, 540);
                old = m_Ticks.nTicks;
                for (int c0 = 0; c0 < 7; c0++) szInfo += FileReadString(file);
                if (szInfo != def_Header_Ticks)
                {
                        Print("Arquivo ", szFileNameCSV, ".csv não é um arquivo de tick negociados.");
                        return false;
                }
                Print("Carregando ticks de replay. Aguarde...");
                while ((!FileIsEnding(file)) && (m_Ticks.nTicks < (INT_MAX - 2)) && (!_StopFlag))
                {
                        ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                        szInfo = FileReadString(file) + " " + FileReadString(file);
                        tick.time = macroRemoveSec(StringToTime(StringSubstr(szInfo, 0, 19)));
                        tick.time_msc = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                        tick.bid = StringToDouble(FileReadString(file));
                        tick.ask = StringToDouble(FileReadString(file));
                        tick.last = StringToDouble(FileReadString(file));
                        tick.volume_real = StringToDouble(FileReadString(file));
                        tick.flags = (uchar)StringToInteger(FileReadString(file));
                        if ((m_Ticks.Info[old].last == tick.last) && (m_Ticks.Info[old].time == tick.time) && (m_Ticks.Info[old].time_msc == tick.time_msc))
                                m_Ticks.Info[old].volume_real += tick.volume_real;
                        else
                        {                                                       
                                m_Ticks.Info[m_Ticks.nTicks] = tick;
                                if (tick.volume_real > 0.0)
                                {
                                        m_Ticks.nRate += (BuiderBar1Min(rate, tick) ? 1 : 0);
                                        rate.spread = m_Ticks.nTicks;
                                        m_Ticks.Rate[m_Ticks.nRate] = rate;
                                        m_Ticks.nTicks++;
                                }
                                old = (m_Ticks.nTicks > 0 ? m_Ticks.nTicks - 1 : old);
                        }
                }
                if ((!FileIsEnding(file)) && (!_StopFlag))
                {
                        Print("Excesso de dados no arquivo de tick.\nNão é possivel continuar...");
                        return false;
                }
        }else
        {
                Print("Aquivo de ticks ", szFileNameCSV,".csv não encontrado...");
                return false;
        }
        return (!_StopFlag);
};

Aquí hay un detalle relevante: será necesario ajustar el valor inicial y el valor de reserva. En caso de que el número de barras de 1 minuto sea superior al indicado aquí. Este valor es apropiado para un periodo de negociación de 9:00 a 18:00, lo que corresponde a 540 minutos. Sin embargo, si este periodo es más largo, es necesario aumentarlo previamente. Sin embargo, es importante tener en cuenta que el tiempo a considerar debe ser el de apertura y cierre de la ventana de negociación. Esto se aplica al fichero de ticks negociados, no al fichero de barras. Esto se debe a que las barras se generan con base en fichero de ticks y si esta ventana es diferente en un fichero concreto, pueden surgir problemas durante la ejecución ( RUN TIME ). Sin embargo, dado que B3 suele tener una ventana de 540 minutos, este valor es suficiente.

Hecha esta salvedad sobre el valor a utilizar, podemos seguir adelante e introducir el fichero de ticks negociados. Así, capturaremos un tick cada vez y construiremos las barras de 1 minuto. Sin embargo, es importante destacar el siguiente hecho: las barras sólo se generarán si hay algún volumen de negociación; de lo contrario, el tick representa algún ajuste en el BID o ASK del activo, y por lo tanto no se tiene en cuenta. Nota: en un futuro próximo, nos ocuparemos de este tipo de situación, ya que tenemos la intención de adaptar el sistema también para el mercado de divisas. Por ahora, ignoraremos estos ticks.

Dado que no utilizamos el valor del spread en la repetición/simulador, se empleará para un propósito más significativo. Sin embargo, ten en cuenta que éste no es el valor del spread. Por lo tanto, si necesitas el valor correcto del spread debido a algún indicador, necesitarás emplear otro enfoque para lograr lo que se está haciendo aquí. Se puede utilizar la variable que se utilizaría para almacenar el spread para guardar el valor de la posición donde estaba el contador. Esto será muy útil en un futuro próximo.

Ahora que todo está correctamente configurado, podemos almacenar los datos de la barra de 1 minuto y así pasar al siguiente paso. Esto se debe a la ausencia de otros cambios en el sistema de lectura. Por lo tanto, no es necesario comentar más sobre la rutina de lectura.

Examinemos ahora la función principal de este tema.

int AdjustPositionReplay(const bool bViewBuider)
{
        u_Interprocess Info;
        MqlRates       Rate[1];
        int            iPos = (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks);
        datetime       dt_Local;
                                
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (Info.s_Infos.iPosShift == iPos) return 0;
        iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / def_MaxPosSlider));
        if (iPos < m_ReplayCount)
        {
                dt_Local = m_dtPrevLoading;
                m_ReplayCount = 0;
                if (!bViewBuider) for (int c0 = 1; (c0 < m_Ticks.nRate) && (m_Ticks.Rate[c0 - 1].spread < iPos); c0++)
                {
                        dt_Local = m_Ticks.Rate[c0].time;
                        m_ReplayCount = m_Ticks.Rate[c0 - 1].spread;
                }
                CustomRatesDelete(def_SymbolReplay, dt_Local, LONG_MAX);
                if (m_dtPrevLoading == 0)
                {
                        Rate[0].close = Rate[0].open = Rate[0].high = Rate[0].low = m_Ticks.Info[m_ReplayCount].last;
                        Rate[0].tick_volume = 0;
                        Rate[0].time = m_Ticks.Info[m_ReplayCount].time - 60;
                        CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                }
        }
        for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag); m_ReplayCount++) Event_OnTime();
        return Event_OnTime();
}

Esta función no está completamente finalizada. Se seguirá modificando. Sin embargo, para evitar que futuras explicaciones resulten confusas, explicaré lo que se ha añadido o eliminado durante el desarrollo de este artículo. De este modo, todo el mundo entenderá mejor lo que está ocurriendo. Y si se desea hacer cambios, será más fácil hacerlo. Sólo tienes que consultar estos artículos y revisar qué hace realmente cada punto comentado. Recuerda que cualquier parte que no se mencione aquí ya se habrá tratado en artículos anteriores.

El primer paso a dar es declarar una variable local para ajustar la posición interna del tiempo dentro de la función. Este ajuste es necesario para evitar la necesidad de reiniciar la repetición desde el principio si avanzas y luego decides retroceder un poco. Sin embargo, llegaremos a ese punto en breve. Después de realizar algunos cálculos para determinar si la posición actual debe avanzar o retroceder un poco, encontramos la primera acción a tomar. Si la posición necesita retroceder, estas dos líneas inicializarán la repetición/simulador al comienzo de la actividad. Sin embargo, esto puede no ser necesario. Si tú o el usuario indican que no desean observar la formación de las barras a medida que se crean, entraremos en un breve bucle en el que examinaremos el contenido de todas las barras de 1 minuto que se registraron durante la lectura de los ticks negociados. 

Llegados a este punto, nos enfrentamos a una cuestión que puede no parecer muy clara en este momento: durante la conversión de los ticks negociados en barras de 1 minuto, adquirimos la posición relativa del contador al mismo tiempo que también poseemos el valor de la hora de apertura de la nueva barra. Este dato es útil y necesario para que podamos eliminar todas las barras que vengan después de la hora indicada. Es poco probable que el valor del contador sea idéntico al valor del nuevo posicionamiento relativo solicitado por el usuario. Por ello, el sistema realizará un pequeño ajuste para que las posiciones coincidan, pero este ajuste se realiza rápidamente. De esta forma, apenas se notará la creación de la barra.

Sin embargo, como se ha mencionado, esta rutina aún no está completa. La operación descrita sólo servirá para el caso en que el usuario retroceda desde la posición actual del contador. Si él avanza desde esa posición, la creación de las barras seguirá ocurriendo. Y como estamos intentando complacer a todos, griegos y troyanos por igual, necesitamos resolver este pequeño obstáculo para evitar que la creación de las barras sea visible durante un avance. Esto no es tan complicado de realizar como algunos podrían imaginar. Compare el código anterior, que no contiene el sistema de avance, con el código siguiente, que ya incorpora este sistema:

int AdjustPositionReplay(const bool bViewBuider)
{
#define macroSearchPosition     {                                                                                               \
                dt_Local = m_dtPrevLoading; m_ReplayCount = count = 0;                                                          \
                if (!bViewBuider) for (count = 1; (count < m_Ticks.nRate) && (m_Ticks.Rate[count - 1].spread < iPos); count++)  \
                        { dt_Local = m_Ticks.Rate[count].time;  m_ReplayCount = m_Ticks.Rate[count - 1].spread; }               \
                                }

        u_Interprocess  Info;
        MqlRates        Rate[def_BarsDiary];
        int             iPos = (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks),
                        count;
        datetime        dt_Local;
                                
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (Info.s_Infos.iPosShift == iPos) return 0;
        iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));
        if (iPos < m_ReplayCount)
        {
                macroSearchPosition;
                CustomRatesDelete(def_SymbolReplay, dt_Local, LONG_MAX);
                if (m_dtPrevLoading == 0)
                {
                        Rate[0].close = Rate[0].open = Rate[0].high = Rate[0].low = m_Ticks.Info[m_ReplayCount].last;
                        Rate[0].tick_volume = 0;
                        Rate[0].time = m_Ticks.Info[m_ReplayCount].time - 60;
                        CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                }
        }if ((iPos > m_ReplayCount) && (!bViewBuider))
        {
                macroSearchPosition;                    
                CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, count);
        }
        for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag); m_ReplayCount++) Event_OnTime();
        return Event_OnTime();
}

¿Notas alguna diferencia? Si estás pensando en la macro, olvídalo, porque está ahí sólo para evitar que tengamos que repetir el mismo código en dos sitios diferentes. De hecho, no hay prácticamente ninguna diferencia. Puede que lo único que sea diferente sea, de hecho, esta línea que añadirá las barras extra. Si ejecutas el sistema de repetición, notarás que difícilmente los puntos de avance o retroceso coincidirán con el cierre de una barra y la apertura de la siguiente. Esto se debe a que siempre habrá un residuo que se ajustará justo por esta línea. Sin embargo, debido a la rapidez de este ajuste, apenas notará este refinamiento.


Alerta al usuario

Nuestro sistema de repetición se encuentra ya en una fase en la que deberíamos empezar a incorporar algunos añadidos que antes no eran tan necesarios. Una de ellas es avisar al usuario cuando no haya más datos disponibles en el sistema para simular o continuar la repetición. Sin este aviso, el usuario puede suponer que el sistema simplemente se ha colgado o que ha ocurrido algo anormal. Para evitar que estas suposiciones surjan en la mente del usuario, empecemos incluyendo alguna información extra. El primer paso es avisar de que no hay más datos que utilizar. Para entender cómo lo haremos, consulta el código que aparece a continuación:

void OnStart()
{
        ulong t1;
        int delay = 3;
        long id = 0;
        u_Interprocess Info;
        bool bTest = false;
        
        Replay.InitSymbolReplay();
        if (!Replay.SetSymbolReplay(user00))
        {
                Finish();
                return;
        }
        Print("Aguardando permissão do indicador [Market Replay] para iniciar replay ...");
        id = Replay.ViewReplay(user01);
        while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(id) != "")) Sleep(750);
        if ((_StopFlag) || (ChartSymbol(id) == ""))
        {
                Finish();
                return;
        }
        Print("Permissão concedida. Serviço de replay já pode ser utilizado...");
        t1 = GetTickCount64();
        while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value)) && (!_StopFlag))
        {
                if (!Info.s_Infos.isPlay)
                {
                        if (!bTest) bTest = true;
                }else
                {
                        if (bTest)
                        {
                                if ((delay = Replay.AdjustPositionReplay(user02)) < 0) AlertToUser(); else
                                {
                                        delay = (delay >= 0 ? 3 : delay);
                                        bTest = false;
                                        t1 = GetTickCount64();
                                }                               
                        }else if ((GetTickCount64() - t1) >= (uint)(delay))
                        {
                                if ((delay = Replay.Event_OnTime()) < 0) AlertToUser();
                                t1 = GetTickCount64();
                        }
                }
        }
        Finish();
}
//+------------------------------------------------------------------+
void AlertToUser(void)
{
        u_Interprocess Info;
        
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        Info.s_Infos.isPlay = false;
        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
        MessageBox("No more data to use in replay-simulation", "Service Replay", MB_OK);
}
//+------------------------------------------------------------------+
void Finish(void)
{
        Replay.CloseReplay();
        Print("Serviço de replay finalizado...");
}
//+------------------------------------------------------------------+

Hay dos momentos en los que es posible generar un aviso de que se han agotado los datos. El primero ocurre durante la ejecución normal de la repetición, que es el caso más común. Sin embargo, también existe otro momento: cuando el usuario ajusta la posición al final de la barra de desplazamiento.

int AdjustPositionReplay(const bool bViewBuider)
{

// ... Código ...

        iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));

// ...  Restante do código ...

Independientemente de ello, la respuesta será siempre la misma. Capturaremos el valor contenido en la variable global del terminal y lo utilizaremos para indicar que estamos en modo pausa. Luego lo volvemos a grabar y mostramos una ventana informando de lo sucedido. Básicamente, eso es lo que haremos, pero será de gran ayuda. Así, el pobre usuario sabrá lo que ha pasado.


Adición de un aviso de espera

Ahora que a nuestro sistema de repetición se le ha dado una forma para que el usuario informe si quiere ver o no el proceso de dibujo de las barras, surge un pequeño inconveniente si realmente quiere seguir el dibujo de las barras. Este es el motivo de este tema.

Cuando queremos ver el dibujo de las barras mientras esperamos a que el servicio de repetición alcance la posición correcta, tenemos la impresión de que podemos detener o iniciar el avance en cualquier momento. Esto se debe a que se nos presenta un botón de reproducción y otro de pausa. Sin embargo, en realidad, no podemos realizar ninguna de las dos acciones hasta que el servicio de repetición alcance la posición correcta para liberar el sistema. Y es en estas situaciones cuando la comprensión puede volverse un poco confusa. No estamos seguros de lo que está ocurriendo exactamente. Sin embargo, si sustituimos este botón presentado por otro que indique la necesidad de esperar, la situación cambia. ¿Estás de acuerdo?

Pues bien, para ello no basta con añadir un botón y ya está. Necesitamos realizar algunas acciones adicionales que permitan al servicio comunicar al indicador de control lo que debe mostrarse o no. Empezaremos por añadir una nueva variable en el fichero de cabecera InterProcess.mqh.

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#define def_GlobalVariableReplay        "Replay Infos"
#define def_GlobalVariableIdGraphics    "Replay ID"
#define def_SymbolReplay                "RePlay"
#define def_MaxPosSlider                400
#define def_ShortName                   "Market Replay"
//+------------------------------------------------------------------+
union u_Interprocess
{
        union u_0
        {
                double  df_Value;       // Valor da variável global de terminal...
                ulong   IdGraphic;      // Contem a ID do Grafico do ativo...
        }u_Value;
        struct st_0
        {
                bool    isPlay;         // Indica se estamos no modo Play ou Pause ...
                bool    isWait;         // Indica para o usuário aguardar...
                ushort  iPosShift;      // Valor entre 0 e 400 ...
        }s_Infos;
};
//+------------------------------------------------------------------+

Este valor, que se transmitirá entre el servicio y el indicador, tendrá prioridad sobre el resto de controles. De esta forma, si es necesario mostrarlo, el indicador de control no podrá realizar ninguna otra acción.  Ahora que hemos definido la variable, debemos ir al servicio de repetición y añadir el código necesario para que se comunique con el indicador de control. Para ello tendremos que añadir un poco de código a la clase C_Replay. No será demasiado complicado.

int AdjustPositionReplay(const bool bViewBuider)
{
#define macroSearchPosition     {                                                                                               \
                dt_Local = m_dtPrevLoading; m_ReplayCount = count = 0;                                                          \
                if (!bViewBuider) for (count = 1; (count < m_Ticks.nRate) && (m_Ticks.Rate[count - 1].spread < iPos); count++)  \
                        { dt_Local = m_Ticks.Rate[count].time;  m_ReplayCount = m_Ticks.Rate[count - 1].spread; }               \
                                }

        u_Interprocess  Info;
        MqlRates        Rate[def_BarsDiary];
        int             iPos = (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks),
                        count;
        datetime        dt_Local;
                                
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (Info.s_Infos.iPosShift == iPos) return 0;
        iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));
        if (iPos < m_ReplayCount)
        {
                macroSearchPosition;
                CustomRatesDelete(def_SymbolReplay, dt_Local, LONG_MAX);
                if (m_dtPrevLoading == 0)
                {
                        Rate[0].close = Rate[0].open = Rate[0].high = Rate[0].low = m_Ticks.Info[m_ReplayCount].last;
                        Rate[0].tick_volume = 0;
                        Rate[0].time = m_Ticks.Info[m_ReplayCount].time - 60;
                        CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                }
        }if ((iPos > m_ReplayCount) && (!bViewBuider))
        {
                macroSearchPosition;                    
                CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, count);
        }
        if (bViewBuider)
        {
                Info.s_Infos.isWait = true;
                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
        }
        for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag); m_ReplayCount++) Event_OnTime();
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        Info.s_Infos.isWait = false;
        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
        return Event_OnTime();
}

Este punto no se alcanzará normalmente y sólo se producirá cuando realmente haya que hacer algo. Si el usuario desea visualizar las barras que se están graficando, hacemos una señal para que el indicador muestre que el servicio no estará disponible durante un tiempo. Registramos esto en la variable global del terminal para que pueda ser interpretado por el indicador. A continuación, el servicio realizará la tarea que realmente debe cumplir. Poco después, liberamos el indicador de forma totalmente incondicional.

Con esto, podemos pasar al código del indicador de control para analizar lo que está ocurriendo. Muchos pueden creer que hará falta mucho código para que todo funcione aquí. Sin embargo, verás que haré todo el trabajo con el menor código posible. Para simplificar las cosas, ¡¡¡qué tal un poco de abstracción!!! Y para conseguirlo, empezamos añadiendo la siguiente línea al fichero de cabecera C_Control.mqh.

enum EventCustom {Ev_WAIT_ON, Ev_WAIT_OFF};

Estamos insertando un nivel adicional de abstracción para facilitar nuestras acciones posteriores. No podemos olvidarnos de la imagen que utilizaremos, que se añade en el siguiente fragmento:

#define def_ButtonPlay  "Images\\Market Replay\\Play.bmp"
#define def_ButtonPause "Images\\Market Replay\\Pause.bmp"
#define def_ButtonLeft  "Images\\Market Replay\\Left.bmp"
#define def_ButtonRight "Images\\Market Replay\\Right.bmp"
#define def_ButtonPin   "Images\\Market Replay\\Pin.bmp"
#define def_ButtonWait  "Images\\Market Replay\\Wait.bmp"
#resource "\\" + def_ButtonPlay
#resource "\\" + def_ButtonPause
#resource "\\" + def_ButtonLeft
#resource "\\" + def_ButtonRight
#resource "\\" + def_ButtonPin
#resource "\\" + def_ButtonWait

Usar una imagen aquí realmente simplifica las cosas. Recuerda que sólo queremos indicar al usuario que el servidor está funcionando y que no podrá responder a ninguna otra petición durante esta operación.

Continuando dentro del archivo de clase, añadimos una variable interna privada a la clase para controlar las acciones internas. 

class C_Controls
{
        private :
//+------------------------------------------------------------------+
                string  m_szBtnPlay;
                long    m_id;
                bool    m_bWait;
                struct st_00
                {
                        string  szBtnLeft,
                                szBtnRight,
                                szBtnPin,
                                szBarSlider;
                        int     posPinSlider,
                                posY;
                }m_Slider;
//+------------------------------------------------------------------+

Al añadir esta variable, ya tenemos una idea del estado del servicio de repetición/simulación. Sin embargo, debe ser inicializada en el lugar apropiado, el constructor de la clase es el punto dulce.

C_Controls() : m_id(0), m_bWait(false)
        {
                m_szBtnPlay             = NULL;
                m_Slider.szBarSlider    = NULL;
                m_Slider.szBtnPin       = NULL;
                m_Slider.szBtnLeft      = NULL;
                m_Slider.szBtnRight     = NULL;
        }

Nótese que necesitamos inicializarla como "false", ya que el servicio de repetición/simulación siempre comenzará libre, siendo capaz de responder a cualquier comando. Incluso si esta inicialización se produjera aquí, nos aseguraremos del estado correcto en otras llamadas. Pero por ahora, esto será suficiente para nuestros propósitos.

Ahora debemos considerar lo siguiente: ¿qué tipo de evento queremos bloquear realmente?  Cada vez que movemos la posición de repetición hacia delante o hacia atrás, observamos que el botón cambia de "reproducir" a "pausar", y queremos bloquear exactamente eso: el acceso del usuario a ese botón. El simple hecho de pulsarlo hará que el indicador de control solicite acciones al servicio de repetición/simulación. Aunque el servicio no responderá durante la fase en la que está ocupado posicionándose para la repetición/simulación.

Si observas el código, te darás cuenta de que el sistema siempre está respondiendo a eventos; es decir, es un sistema basado en eventos. Por eso creamos la enumeración EventCustom, para mantener el sistema basado en eventos. No tenemos intención de cambiar esto. De hecho, ni siquiera me planteo hacer tal cambio, ya que nos obligaría a adoptar enfoques algo más complejos que el uso de eventos. Sin embargo, limitarse a añadir una enumeración que indique la presencia de eventos no nos da una solución. De hecho, necesitamos aplicar estas medidas. Pero ya os daré una pista de lo que haremos. Vamos a modificar el procedimiento DispatchMessage para que, si el servicio está ocupado, no se genere ningún evento al pulsar el botón reproducir/pausar. Esto se puede realizar fácilmente añadiendo la siguiente prueba:

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
        {
                u_Interprocess Info;
                static int six = -1, sps;
                int x, y, px1, px2;
                                
                switch (id)
                {

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

                        case CHARTEVENT_OBJECT_CLICK:
                                if (m_bWait) break;
                                if (sparam == m_szBtnPlay)
                                {
                                        Info.s_Infos.isPlay = (bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE);
                                        if (!Info.s_Infos.isPlay) CreteCtrlSlider(); else
                                        {
                                                RemoveCtrlSlider();
                                                m_Slider.szBtnPin = NULL;
                                        }
                                        Info.s_Infos.iPosShift = (ushort) m_Slider.posPinSlider;
                                        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                        ChartRedraw();
                                }else   if (sparam == m_Slider.szBtnLeft) PositionPinSlider(m_Slider.posPinSlider - 1);
                                else if (sparam == m_Slider.szBtnRight) PositionPinSlider(m_Slider.posPinSlider + 1);
                                break;

// ... Restante do código ....

Añadiendo sólo esta línea de prueba, evitaremos que el indicador siga enviando peticiones al servicio mientras esté ocupado. Sin embargo, esto aún no resuelve completamente nuestro problema, ya que el usuario puede sentirse molesto al hacer clic en el botón "reproducir/pausar" y que éste no cambie. Tenemos que tomar más medidas. Además, todavía no hemos podido ajustar correctamente el valor de la variable que estamos probando.

Esta parte puede parecer ahora un poco confusa, pero todo lo que haremos en realidad es modificar el valor de la variable m_bWait y analizar este valor. Esto nos permitirá determinar qué mapas de bits deben mostrarse en el gráfico. El objetivo es que el botón "reproducir/pausar" se cambie por otra figura mientras el servicio esté ocupado, y vuelva al botón "reproducir/pausar" tradicional cuando el servicio se libere. Para ello, utilizaremos un enfoque sencillo:

void CreateBtnPlayPause(bool state)
{
        m_szBtnPlay = def_PrefixObjectName + "Play";
        CreateObjectBitMap(5, 25, m_szBtnPlay, (m_bWait ? def_ButtonWait : def_ButtonPause), (m_bWait ? def_ButtonWait : def_ButtonPlay));
        ObjectSetInteger(m_id, m_szBtnPlay, OBJPROP_STATE, state);
}

Ten en cuenta que simplemente estoy probando la variable. En función de su valor, aplicaremos el botón "reproducir/pausar" o el botón que representará una señal de espera. Sin embargo, te estarás preguntando: ¿cómo vas a controlar este botón? ¿Estará constantemente leyendo el valor de la variable global del terminal? Pues será algo parecido. Recuerda el siguiente detalle: cada vez que el servicio añada una nueva entrada al activo de repetición de mercado, se reflejará en el indicador. Así, MetaTrader 5 generará un evento que activará la función OnCalculate. Aquí es donde entraremos nosotros, pero no estaremos continuamente monitorizando el indicador. Lo haremos de una forma más elegante. Para entender lo que se hará, mira la imagen de abajo, que representa el flujo de llamadas en el código:

Exactamente esta secuencia se ejecutará para controlar correctamente el botón en el indicador de control. El procedimiento CreateBtnPlayPause ya ha sido presentado anteriormente y creo que quedó bastante claro lo que sucede. Ahora, vamos a cubrir los otros puntos hasta que este diagrama de flujo esté completamente descrito. Lo abordaremos al revés, ya que el procedimiento OnCalculate involucra una lógica un poco más compleja y requiere entender los pasos que ocurren en DispatchMessage

Debido a esto, vayamos al fragmento básico del manejo de eventos personalizados. Echa un vistazo al siguiente fragmento de código:

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        u_Interprocess Info;
        static int six = -1, sps;
        int x, y, px1, px2;
                                
        switch (id)
        {
                case (CHARTEVENT_CUSTOM + Ev_WAIT_ON):
                        m_bWait = true;
                        CreateBtnPlayPause(true);
                        break;
                case (CHARTEVENT_CUSTOM + Ev_WAIT_OFF):
                        m_bWait = false;
                        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                        CreateBtnPlayPause(Info.s_Infos.isPlay);
                        break;

// ... Restante do código ...

Cuando la función DispatchMessage es llamada por OnChartEvent, presente en el indicador de control, se proporcionarán datos que nos permitirán manejar tanto mensajes de eventos reportados por la plataforma MetaTrader 5 como eventos personalizados disparados por nuestro código en puntos específicos. Los eventos personalizados los trataremos más adelante. La función buscará el código correspondiente si se está utilizando el evento personalizado Ev_WAIT_ON. Esto nos indicará que el servicio está ocupado, haciendo que la variable m_bWait reciba un valor true. A continuación, llamamos a la creación del botón "reproducir/pausar", que mostrará realmente una figura indicando el estado de ocupado. Cuando se dispara el evento personalizado Ev_WAIT_OFF, queremos que se indique el estado actual del servicio, es decir, si está en modo reproducción o pausa. Por tanto, la variable m_bWait recibirá un valor que indique que el servicio está disponible para recibir peticiones. También necesitamos capturar los datos de la variable global del terminal, que contendrá el estado actual del servicio. A continuación, llamamos a la rutina que crea el botón "reproducir/pausar" para que el usuario pueda interactuar con el sistema.

Este tipo de enfoque es bastante intuitivo, y creo que todo el mundo será capaz de entender la idea. La gran pregunta es: ¿cómo se activarán estos eventos? ¿Tendremos un código extremadamente complejo y difícil de entender? No, la forma de disparar eventos en el lenguaje MQL5 es bastante sencilla, al igual que la forma de analizar y manejar estos eventos personalizados. En el fragmento de código anterior, se puede ver cómo manejar dos eventos personalizados. Ahora vamos a explorar cómo desencadenar estos eventos. Para ello, ten en cuenta lo siguiente: cuando activamos un evento personalizado, en realidad estamos realizando una llamada a la función OnChartEvent. Esta función siempre será llamada cuando se dispare un evento, ya sea personalizado o proveniente de la plataforma MetaTrader 5. La función llamada siempre será la misma. Ahora echa un vistazo al código de esta función en el indicador de control:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        Control.DispatchMessage(id, lparam, dparam, sparam);
}

Es decir, cuando se acciona un evento, su manejo se delega a la clase C_Control, y la función a ejecutar es DispatchMessage. ¿Te has dado cuenta de cómo se hace todo? Si el código contenido en la función DispatchMessage estuviera dentro de la función de manejo de eventos, el resultado sería el mismo. Sin embargo, es importante notar algo: la función OnChartEvent recibe 4 parámetros, pero la función que dispara los eventos personalizados utilizará más parámetros. De hecho, se utilizan 5 parámetros para activar los eventos personalizados. Por lo tanto, podemos distinguir los eventos personalizados de los eventos procedentes de MetaTrader 5. Si nos fijamos bien, nos daremos cuenta de que el valor utilizado en la selección es la suma del valor indicado en la enumeración EventCustom y otro dato CHARTEVENT_CUSTOM. De esta forma obtenemos el valor correcto. 

Pero, ¿cómo se crea este valor? ¿Cómo podemos generar eventos personalizados utilizando MQL5? Para entenderlo, veamos el código principal de nuestro indicador de control: la función OnCalcule. Se puede ver justo debajo:

int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
{
        static bool bWait = false;
        u_Interprocess Info;
        
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (!bWait)
        {
                if (Info.s_Infos.isWait)
                {
                        EventChartCustom(m_id, Ev_WAIT_ON, 0, 0, "");
                        bWait = true;
                }
        }else if (!Info.s_Infos.isWait)
        {
                EventChartCustom(m_id, Ev_WAIT_OFF, 0, Info.u_Value.df_Value, "");
                bWait = false;
        }
        
        return rates_total;
}

Vamos a entender cómo funciona este código de arriba. Lo primero que debes tener en cuenta es que este código es el manejador de eventos que MetaTrader 5 llamará. Es decir, cada vez que haya un cambio en el precio del activo o el activo reciba un nuevo tick negociado, la función OnCalcule será llamada automáticamente por MetaTrader 5. De esta forma, no es necesario y no utilizaremos ningún tipo de temporizador dentro del indicador. De hecho, el uso de temporizadores debe evitarse en la medida de lo posible en los indicadores, ya que no sólo afectan al indicador en cuestión, sino a todos los demás. Por lo tanto, vamos a utilizar esta llamada realizada por la plataforma MetaTrader 5 para comprobar lo que está sucediendo con el servicio. Considerando que el servicio estará enviando entradas al activo de repetición/simulación y, en consecuencia, llamando indirectamente a la función OnCalcule.


Conclusión

Espero que hayas entendido la idea, ya que es la base de todo lo demás. Así, en cada llamada a OnCalcule, capturaremos el valor presente en la variable terminal global y comprobaremos si la variable estática local tiene un valor verdadero o no. Si no tiene un valor verdadero, comprobaremos si el servicio está ocupado. Si se cumple esta condición, lanzaremos un evento personalizado para informar de ello. Inmediatamente después, modificaremos el valor de la variable estática local para indicar que el indicador de control es consciente de que el servicio de repetición/simulación está ocupado. Así, la próxima vez que se llame a OnCalcule, comprobaremos si el servicio de repetición/simulación está libre para realizar su actividad. En el momento en que esto ocurra, lanzaremos un evento personalizado para informar de que el servicio ya puede recibir peticiones del indicador de control. Y el ciclo se repite, una vez que la variable estática local tendrá un valor verdadero.

Ahora, preste atención al hecho de que estamos utilizando algo común para desencadenar los eventos personalizados, que es la función EventChartCustom. Aquí, estamos restringidos al gráfico actual y al indicador de control solamente. Sin embargo, puede activar eventos para cualquier gráfico, indicador e incluso para un Expert. El único cuidado necesario es rellenar correctamente los parámetros de la función EventChartCustom. Si se hace esto, el resto será responsabilidad de la plataforma MetaTrader 5, y sólo tendrá que manejar el evento personalizado en el momento, ya sea en un indicador o en un EA. Este es un aspecto poco explorado, y por lo que he notado, la gente a veces cree que la plataforma MetaTrader 5 no es capaz de realizar ciertas acciones. 

En el siguiente video, demostraré el sistema en su estado actual de desarrollo. Espero que os estén gustando los artículos y os estén siendo útiles para aprender y conocer mejor la plataforma MetaTrader 5, así como las posibilidades que ofrece el lenguaje MQL5.



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

Archivos adjuntos |
Market_Replay.zip (13060.83 KB)
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 10): Sólo datos reales para la repetición Desarrollo de un sistema de repetición — Simulación de mercado (Parte 10): Sólo datos reales para la repetición
Aquí veremos cómo se pueden utilizar datos más fiables (ticks negociados) en el sistema de repetición, sin tener que preocuparnos necesariamente de si están ajustados o no.
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 08): Bloqueo del indicador Desarrollo de un sistema de repetición — Simulación de mercado (Parte 08): Bloqueo del indicador
En este artículo te mostraré cómo bloquear un indicador, simplemente utilizando el lenguaje MQL5, de una forma muy interesante y sorprendente.
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 11): Nacimiento del SIMULADOR (I) Desarrollo de un sistema de repetición — Simulación de mercado (Parte 11): Nacimiento del SIMULADOR (I)
Para poder usar datos que forman barras, debemos abandonar la repetición y comenzar a desarrollar un simulador. Utilizaremos las barras de 1 minuto precisamente porque nos ofrecen un nivel de complejidad mínimo.
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 07): Primeras mejoras (II) Desarrollo de un sistema de repetición — Simulación de mercado (Parte 07): Primeras mejoras (II)
En el artículo anterior realizamos correcciones en algunos puntos y agregamos pruebas a nuestro sistema de repetición para garantizar la mayor estabilidad posible. Asimismo, comenzamos a crear y utilizar un archivo de configuración para dicho sistema.