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

Desarrollo de un sistema de repetición — Simulación de mercado (Parte 22): FOREX (III)

MetaTrader 5Probador | 1 noviembre 2023, 09:41
432 0
Daniel Jose
Daniel Jose

Introducción

En el artículo anterior, "Desarrollo de un sistema de repetición — Simulación de mercado (Parte 21): FOREX (II)", prácticamente nos enfocamos en resolver los problemas del sistema, especialmente en lo que respecta al archivo de configuración de la repetición/simulación. Sin embargo, dado que este ya contenía mucha información para asimilar por parte de aquellos que están estudiando y siguiendo los artículos del sistema, con el objetivo de aprender a construir sus propios programas, se decidió concluir el artículo cuando el sistema de configuración ya estaba en un punto en el que podemos trabajar cómodamente durante un buen tiempo.

Pero si has estado probando el contenido adjunto, es posible que hayas notado que, a pesar de que el sistema de repetición/simulación funciona de manera bastante estable en lo que respecta al mercado de valores, esto no puede decirse lo mismo para el mercado similar al mercado de divisas (forex). Y aquí no me estoy refiriendo únicamente al forex en sí, sino a cualquier activo que siga los mismos conceptos de representación de precios utilizados en el forex, es decir, el uso del valor BID como valor principal.

Para aquellos que aún no han comprendido la diferencia entre el mercado de acciones y el mercado de divisas (forex), a pesar de que este ya es el tercer artículo en el que abordo esto, debo dejar claro que la gran diferencia es el hecho de que en forex no existe, o mejor dicho, no se nos informa acerca de algunas cosas que realmente ocurrieron en la negociación. Aunque esto puede parecer relacionado únicamente con el mercado de divisas, no me estoy refiriendo exclusivamente a él. Esto se debe a que en el mercado de divisas tenemos un método propio para obtener cierta información, y el modelo de operación es completamente diferente al modelo de mercado de valores. Encuentro más sencillo hacer esta distinción de esta manera. Pero todo lo que esté relacionado en estos artículos con el forex, debes entenderlo como relacionado con cualquier tipo de mercado en el que la forma de representación se hace a través del valor BID, a diferencia de lo que sucede en el mercado de valores, donde el valor es el LAST.

Entonces, al lograr abarcar el mercado de divisas de la misma manera que el mercado de valores, que ya está siendo cubierto por el sistema de repetición del mercado, tendremos la capacidad de realizar una repetición o simulación de cualquier tipo de mercado, independientemente de cuál sea. Y una de las cosas que aún nos está dando problemas en el sistema es el hecho de que si apagas la visualización de la creación de las barras que están siendo generadas por el repetidor o simulador, el gráfico se verá incorrecto. Ya enfrentamos este problema en el pasado al hacer lo mismo cuando todavía estábamos desarrollando el sistema para cubrir el mercado de valores. Pero dado que en el mercado de divisas la representación es diferente y el precio sigue el BID, el sistema actual no puede manejar este problema de manera adecuada. Y al intentar desplazar la posición de estudio a otro punto cuando la visualización de las barras está apagada, todos los indicadores estarán incorrectos, ya que no habrá ninguna barra entre la posición anterior y la nueva. Así que empecemos este artículo corrigiendo este problema.


Corrijamos el sistema de posicionamiento rápido

Para solucionar este problema, tendremos que realizar algunos cambios en el código, no porque esté incorrecto, sino porque no puede trabajar con la representación BID. Para ser más preciso en la explicación, el problema radica en que durante la lectura de los ticks y su conversión, aún en la clase C_FileTicks, a barras de 1 minuto para luego usarlas en el proceso de posicionamiento rápido, no es adecuado. Dado que no podemos representar valores de precio basados en el BID. Para comprender el motivo de esto, observa el código responsable de realizar esta conversión. Lo puedes ver 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);
        m_Ticks.ModePlot = PRICE_FOREX;
        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.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));
            m_Ticks.ModePlot = (def_Ticks.volume_real > 0.0 ? PRICE_EXCHANGE : m_Ticks.ModePlot);
            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
    }

Observa que para que se llame a la rutina que crea las barras, es necesario que exista un volumen negociado. Este volumen solo ocurre cuando el valor del último precio se modifica; en el caso de la modificación del valor del BID, este volumen siempre será cero, es decir, la rutina no se llama. Entonces, lo primero que debemos hacer es eliminar este código de aquí, ya que en realidad no sabemos si trabajaremos con una representación BID o LAST durante la lectura de los ticks. Por lo tanto, esta rutina anterior será modificada como se muestra 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;
            
        Print("Carregando ticks de replay. Aguarde...");
        ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
        m_Ticks.ModePlot = PRICE_FOREX;
        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.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));
            m_Ticks.ModePlot = (def_Ticks.volume_real > 0.0 ? PRICE_EXCHANGE : m_Ticks.ModePlot);
            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
    }

¿Y por qué estoy haciendo esto? El motivo es que solo al finalizar la lectura total y completa del archivo sabremos si la representación se hará utilizando el BID o LAST presente en los ticks. Y quien garantiza esto es esta línea en particular. Pero solo será válida cuando se complete la lectura del archivo. Bueno, ¿entonces cuándo invocaremos esta conversión de ticks a barras para ser utilizada durante la fase de representación? Al menos por ahora, esa no será la idea. Así que la rutina que lo hará se muestra a continuación:

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);
        }else
        {
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TRADE_CALC_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CALC_MODE_EXCH_STOCKS : SYMBOL_CALC_MODE_FOREX);
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_CHART_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CHART_MODE_LAST : SYMBOL_CHART_MODE_BID);
        }
        m_Ticks.bTickReal = true;
        
        return dtRet;
    };

No podemos invocar la rutina de conversión de ticks en cualquier lugar de esta rutina de arriba. Esto se debe a que en algunos momentos necesitaremos que los ticks se hayan convertido en barras, y la razón de esto es que estaremos utilizando un archivo de ticks reales como si fuera un archivo de barras previas de 1 minuto. Esto se puede observar al ver la presencia de la rutina CustomRatesUpdate en el código de arriba. Debido a esto, debemos llamar al procedimiento de conversión antes de que esta llamada a CustomRatesUpdate ocurra de hecho. Pero si observas la rutina de conversión, verás que no es adecuada para ser utilizada en el código de arriba. La rutina original de conversión se puede ver a continuación:

inline bool BuiderBar1Min(MqlRates &rate, const MqlTick &tick)
    {
        if (rate.time != macroRemoveSec(tick.time))
        {
            rate.real_volume = 0;
            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;
        rate.tick_volume += (tick.last > 0 ? 1 : 0);

        return false;
    }

Observa lo inadecuada que es para ser utilizada donde la necesitamos. Por lo tanto, tendremos que crear otro procedimiento para realizar la conversión. Pero además de esto, tenemos otro problema. ¿Cómo saber dónde debe comenzar la conversión? Recuerda que la llamada puede ocurrir en diferentes momentos y en cada uno de ellos podemos tener diferentes necesidades. En una de las llamadas, podríamos estar cargando ticks para usar en la repetición. Mientras que en otra llamada podríamos estar cargando ticks que deberán descartarse poco después, ya que se utilizarán como barras previas. Así que puedes ver que esto debe ser bien pensado para evitar entrar en un callejón sin salida.

Pero todo esto será mucho más simple y fácil de hacer si analizas los cambios que deben realizarse y los implementas siguiendo exactamente el plan trazado. De esta manera, comenzaremos modificando el procedimiento de llamada y luego nos ocuparemos de la rutina de conversión. El nuevo procedimiento de llamada se puede ver a continuación:

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;
        BuiderBar1Min(MemNTicks);
        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);
        }else
        {
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TRADE_CALC_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CALC_MODE_EXCH_STOCKS : SYMBOL_CALC_MODE_FOREX);
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_CHART_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CHART_MODE_LAST : SYMBOL_CHART_MODE_BID);
        }
        m_Ticks.bTickReal = true;
        
        return dtRet;
    };

Observa cómo hemos resuelto la primera parte de nuestro problema. Al colocar esta línea, garantizaremos que los ticks se conviertan en barras de 1 minuto antes de la posible llamada a CustomRatesUpdate. Al mismo tiempo, informamos a la rutina de conversión dónde debe comenzar el proceso de conversión. La razón por la que colocamos la llamada a la rutina de conversión aquí, en lugar de en la rutina de lectura, es precisamente para evitar agregar una variable aparentemente innecesaria en la rutina de lectura. Ya que aquí tenemos acceso a la variable que necesitamos para determinar el rango en el que debemos trabajar. Entonces, está bien, podemos proceder a la implementación de la rutina que convertirá los ticks en barras de 1 minuto. Deberemos hacerlo de manera que funcione tanto para un mercado tipo FOREX como para un mercado tipo BOLSA. ¿Parece complicado, verdad? Una vez más, si no planeas lo que vas a hacer, podrías encontrarte atrapado en un bucle de codificación que te hará desistir de intentar implementar el código adecuado.

Aquí no quiero darte una solución. Quiero que aprendas a pensar de manera que puedas llegar a una solución adecuada para lo que realmente quieras hacer. Entonces, pensemos lo siguiente: El procedimiento original de conversión ya estaba logrando convertir un precio LAST en una barra de 1 minuto. Bueno, lo que necesitamos es agregar un bucle a ese procedimiento de manera que lea todos los ticks que se leyeron en el archivo. El punto de inicio de este bucle nos lo proporciona el autor de llamada, y el punto final será el último tick leído. Hasta aquí todo bien. Pero también necesitamos, en caso de que el sistema detecte que estamos utilizando una representación basada en el valor BID, que este valor reemplace al precio LAST que se usa originalmente. De esta manera, podremos convertir el valor BID en una barra de 1 minuto sin mucho esfuerzo. ¿Interesante, verdad? Esta es la idea que debemos implementar, y el resultado de la implementación es el procedimiento que se muestra a continuación:

inline void BuiderBar1Min(const int iFirst)
    {
        MqlRates rate;
        double   dClose = 0;
        
        rate.time = 0;
        for (int c0 = iFirst; c0 < m_Ticks.nTicks; c0++)
        {
            switch (m_Ticks.ModePlot)
            {
                case PRICE_EXCHANGE:
                    if (m_Ticks.Info[c0].last == 0.0) continue;
                    dClose = m_Ticks.Info[c0].last;
                    break;
                case PRICE_FOREX:
                    dClose = (m_Ticks.Info[c0].bid > 0.0 ? m_Ticks.Info[c0].bid : dClose);
                    if (dClose == 0.0) continue;
                    break;
            }
            if (rate.time != macroRemoveSec(m_Ticks.Info[c0].time))
            {
                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
                rate.time = macroRemoveSec(m_Ticks.Info[c0].time);
                rate.real_volume = 0;
                rate.tick_volume = 0;
                rate.open = rate.low = rate.high = rate.close = dClose;
            }else
            {
                rate.close = dClose;
                rate.high = (rate.close > rate.high ? rate.close : rate.high);
                rate.low = (rate.close < rate.low ? rate.close : rate.low);
                rate.real_volume += (long) m_Ticks.Info[c0].volume_real;
                rate.tick_volume++;
            }
            m_Ticks.Rate[(m_Ticks.nRate += (rate.tick_volume == 0 ? 1 : 0))] = rate;
        }
    }

La rutina original está toda en verde para que puedas identificar dónde está presente. Estos códigos son la parte nueva de la rutina. Observa que esta parte es responsable únicamente de cambiar LAST por BID, o viceversa, de modo que el precio de cierre sea adecuado para producir la barra de 1 minuto. El bucle del que mencioné en la explicación de lo que debía hacer se encuentra en este punto. A pesar de que esta rutina resuelve la mayor parte de nuestros problemas, no está resolviendo el problema del volumen de ticks, en caso de que estemos utilizando una representación tipo BID. Para resolver este problema, tendremos que modificar ligeramente la rutina anterior, de manera que el código final quede como se muestra a continuación:

inline void BuiderBar1Min(const int iFirst)
    {
        MqlRates rate;
        double  dClose = 0;
        bool    bNew;
        
        rate.time = 0;
        for (int c0 = iFirst; c0 < m_Ticks.nTicks; c0++)
        {
            switch (m_Ticks.ModePlot)
            {
                case PRICE_EXCHANGE:
                    if (m_Ticks.Info[c0].last == 0.0) continue;
                    dClose = m_Ticks.Info[c0].last;
                    break;
                case PRICE_FOREX:
                    dClose = (m_Ticks.Info[c0].bid > 0.0 ? m_Ticks.Info[c0].bid : dClose);
                    if ((dClose == 0.0) || (m_Ticks.Info[c0].bid == 0.0)) continue;
                    break;
            }
            if (bNew = (rate.time != macroRemoveSec(m_Ticks.Info[c0].time)))
            {
                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
                rate.time = macroRemoveSec(m_Ticks.Info[c0].time);
                rate.real_volume = 0;
                rate.tick_volume = (m_Ticks.ModePlot == PRICE_FOREX ? 1 : 0);
                rate.open = rate.low = rate.high = rate.close = dClose;
            }else
            {
                rate.close = dClose;
                rate.high = (rate.close > rate.high ? rate.close : rate.high);
                rate.low = (rate.close < rate.low ? rate.close : rate.low);
                rate.real_volume += (long) m_Ticks.Info[c0].volume_real;
                rate.tick_volume++;
            }
            m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate;
        }
    }

En este código, que realmente resuelve el problema del volumen de ticks, tuvimos que agregar una nueva variable. Su valor se define en este punto, donde evaluamos si agregaremos o no una nueva barra. Pero esta variable solo se utiliza en este punto, de modo que sabremos si agregaremos o no una nueva barra al sistema.

Ahora un detalle: Cuando utilizamos el modo de representación por LAST, para que el volumen de ticks sea correcto, debemos iniciar el contador en cero. Pero cuando usamos la representación por BID, debemos comenzar el valor en uno. De lo contrario, tendremos datos incorrectos en el volumen de ticks, y esto se logra al ejecutar este código.

Detalles, detalles. Pero si no tomas las precauciones adecuadas, podrían acabar por perjudicarte.


Corrección del volumen de ticks

Puede que estés pensando que el sistema ya no tiene ningún error. Sin embargo, aún existen fallas por resolver, y una de ellas es la cuestión del volumen. En el tema anterior, corregimos el volumen de ticks que el sistema informa en caso de realizar un cambio rápido en la posición que estamos analizando. Pero si ejecutas la repetición o simulación sin cambiar rápidamente la posición, la información sobre el volumen será incorrecta.

No es que el código esté defectuoso, todo lo contrario. Si estás utilizando el sistema para realizar una repetición o simulación de datos de un activo que utiliza el valor LAST como precio de representación, el volumen de ticks informado será correcto. Pero en el caso de utilizar el precio BID, como ocurre en FOREX, este volumen será incorrecto. Entonces, necesitamos resolver este problema para que la información de volumen sea la correcta. Para entender dónde está el problema, observa el código responsable de realizar este cálculo:

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

        bool    bNew;
        MqlTick tick[1];
        static double PointsPerTick = 0.0;

        if (bNew = (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)))
        {
            PointsPerTick = (PointsPerTick == 0.0 ? SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) : PointsPerTick);            
            if (bViewMetrics) Metrics();
            m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
            def_Rate.real_volume = 0;
            def_Rate.tick_volume = 0;
        }
        def_Rate.close = (m_Ticks.ModePlot == PRICE_EXCHANGE ? (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : def_Rate.close) :
               (m_Ticks.Info[m_ReplayCount].bid > 0.0 ? m_Ticks.Info[m_ReplayCount].bid : 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)
            {
                static double BID, ASK;
                double dSpread;
                int    iRand = rand();
    
                dSpread = PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? PointsPerTick : 0 ) : 0 );
                if (tick[0].last > ASK)
                {
                    ASK = tick[0].ask = tick[0].last;
                    BID = tick[0].bid = tick[0].last - dSpread;
                }
                if (tick[0].last < BID)
                {
                    ASK = tick[0].ask = tick[0].last + dSpread;
                    BID = tick[0].bid = tick[0].last;
                }
            }
            CustomTicksAdd(def_SymbolReplay, tick); 
        }
        m_ReplayCount++;
        
#undef def_Rate
    }

En el caso de trabajar con un activo cuyo precio de representación se basa en el BID, este cálculo aquí no tendrá ningún efecto, ya que en FOREX este tipo de volumen simplemente no existe. Sin embargo, este cálculo aquí estará generando valores incorrectos para un sistema de representación basado en el BID. Esto se debe a que en estos casos, el tick no contiene información sobre el volumen negociado. Entonces, el valor del volumen de ticks siempre será cero.

Pero en el cálculo utilizado para generar el volumen de ticks durante la fase en la que estamos creando las barras de 1 minuto para ser utilizadas en caso de un desplazamiento rápido, este cálculo ya proporcionará el valor correcto. Así que debemos corregir precisamente el procedimiento mencionado anteriormente. Quedará como se muestra a continuación:

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

        bool bNew;
        MqlTick tick[1];
        static double PointsPerTick = 0.0;

        if (bNew = (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)))
        {
            PointsPerTick = (PointsPerTick == 0.0 ? SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) : PointsPerTick);            
            m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
            if (m_Ticks.ModePlot == PRICE_FOREX) CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate, (def_Rate.time < m_MountBar.memDT ? 1 : 0));
            def_Rate.real_volume = 0;
            def_Rate.tick_volume = 0;
        }
        def_Rate.close = (m_Ticks.ModePlot == PRICE_EXCHANGE ? (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : def_Rate.close) :
                                                               (m_Ticks.Info[m_ReplayCount].bid > 0.0 ? m_Ticks.Info[m_ReplayCount].bid : 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.ModePlot == PRICE_FOREX) && (m_Ticks.Info[m_ReplayCount].bid > 0.0) ? 1 : (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)
            {
                static double BID, ASK;
                double dSpread;
                int    iRand = rand();
    
                dSpread = PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? PointsPerTick : 0 ) : 0 );
                if (tick[0].last > ASK)
                {
                    ASK = tick[0].ask = tick[0].last;
                    BID = tick[0].bid = tick[0].last - dSpread;
                }
                if (tick[0].last < BID)
                {
                    ASK = tick[0].ask = tick[0].last + dSpread;
                    BID = tick[0].bid = tick[0].last;
                }
            }
            CustomTicksAdd(def_SymbolReplay, tick); 
        }
        m_ReplayCount++;
        
#undef def_Rate
    }

No me preguntes por qué, pero por alguna extraña razón, que personalmente no tengo la menor idea de poder explicarte realmente, tenemos que agregar esta línea aquí, en caso de que no lo hagamos, el valor informado en el volumen de ticks será incorrecto. A pesar de esto, debes notar que hay una condición en la función. Esto evita problemas en caso de que uses el sistema de posicionamiento rápido. Evita que aparezca una barra extraña, que estaría fuera de tiempo en el gráfico del sistema. Pero aparte de eso, que es una razón extremadamente extraña. Todo lo demás funciona según lo esperado. Entonces, el nuevo cálculo será este, y con esto estaremos contabilizando los ticks tanto cuando estamos trabajando con un activo de representación BID como cuando trabajamos con un activo que utiliza la representación LAST.

Este cálculo es bastante simple, como habrás notado. Pero la parte realmente curiosa es que tenemos que enviar los valores de RATE dentro del activo personalizado una segunda vez. Después de que la barra se cierra, realmente no pude entender el motivo de esto. Tanto que este envío solo es necesario en el caso de una representación tipo BID, algo bastante curioso, por cierto.

Hay otra cosa que debes haber notado en la rutina anterior. Ahora ya no existe el sistema de métricas. En cierto modo, ya estaba considerando eliminar este sistema cuando se agregaron los ticks en la ventana de observación del mercado. Esto se debe a que podemos tener allí una buena medida del tiempo necesario para generar cada una de las barras. Por eso, el código de métricas se eliminó.


Preparando el terreno para el próximo desafío

Con todos estos cambios implementados hasta ahora, podemos pasar al verdadero desafío: crear una forma de simular los ticks del mercado de FOREX basándonos únicamente en el contenido presente en archivos de barras de 1 minuto. Y créeme, el desafío será bastante grande, pero al mismo tiempo muy interesante y emocionante de enfrentar y superar. Para facilitar, dado que la tarea es mucho más compleja de lo que parece, vamos a dividir la clase C_FileTicks en 2 clases. Pero esto es solo para facilitar las cosas. Esta división no sería realmente necesaria, pero como tendremos que lidiar con algunas cosas bastante tediosas de procesar, y no me gusta tener una clase con más de 1000 líneas, vamos a dividir C_FileTicks en dos clases.

En esta división, lo que ocurrirá es que vamos sacar de la clase C_FileTicks la parte responsable de simular los ticks. Y así nace la clase C_Simulation, que será responsable de realizar esta creación, convirtiendo las barras de 1 minuto en ticks, de modo que esto pueda presentarse y trabajarse sin problemas, ya que la clase C_Simulation será invisible para el sistema de repetición. Para el servicio de repetición, los datos siempre provendrán de un archivo de ticks reales. En realidad, podrían provenir de una simulación. Incluso si intentas acceder a la clase C_Simulation desde dentro de la clase C_Replay, no será accesible. De esta manera, todo funcionará como esperamos que funcione. Dado que la clase C_Replay solo podrá ver la clase C_FileTicks y esta hace precisamente esto: carga los ticks reales presentes en un archivo para que la clase C_Replay pueda mostrarlos en el terminal gráfico de MetaTrader 5.

Entonces, la nueva declaración de la clase C_FileTicks queda como se muestra a continuación:

#include "C_FileBars.mqh"
#include "C_Simulation.mqh"
//+------------------------------------------------------------------+
#define macroRemoveSec(A) (A - (A % 60))
//+------------------------------------------------------------------+
class C_FileTicks : private C_Simulation
{

// ... Código interno da classe 

};

El hecho de hacer esto hace que la clase C_FileTicks herede de forma privada la clase C_Simulation. De esta manera, cumplimos exactamente con lo que describí anteriormente.

Pero debemos hacer algunos pequeños cambios en el código de la clase C_FileTicks. Esto se debe a que heredaremos la clase C_Simulation. Pero no quiero enviar los datos declarados como tipo protected a la clase C_Simulation. Esto es para que la clase C_Simulation permanezca oculta para el resto del sistema. Sin embargo, todavía debemos permitir que el trabajo realizado por la clase pueda ser utilizado por otras clases. Entonces, necesitamos modificar el siguiente código agregando las siguientes líneas:

bool BarsToTicks(const string szFileNameCSV)
    {
        C_FileBars  *pFileBars;
        int         iMem = m_Ticks.nTicks,
                    iRet;
        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))
        {
            ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
            m_Ticks.Rate[++m_Ticks.nRate] = rate[0];
            iRet = Simulation(rate[0], local);
            for (int c0 = 0; c0 <= iRet; c0++)
            {
                ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                m_Ticks.Info[m_Ticks.nTicks++] = local[c0];
            }
        }
        ArrayFree(local);
        delete pFileBars;
        m_Ticks.bTickReal = false;
        
        return ((!_StopFlag) && (iMem != m_Ticks.nTicks));
    }

Estas líneas resaltadas formaban parte del código que era ejecutado por el procedimiento que crea la simulación. Para mantener las cosas en su lugar, necesitamos que ahora la rutina que genera la simulación devuelva un valor. Este valor se utiliza para que la clase C_FileTicks sepa cuántos ticks deben almacenarse en la matriz para que luego sean utilizados por la clase C_Replay.

Ahora podremos centrar completamente nuestra atención en la clase C_Simulation y construir un sistema que sea capaz de realizar cualquier nivel de simulación basado únicamente en los datos contenidos en un archivo de barras de 1 minuto. Pero como ya estarás pensando, este será el tema del próximo tema de este artículo.


La clase C_Simulation

Muy bien, ahora que ya hemos separado las cosas y, para cualquier efecto, estaremos trabajando con cualquier tipo de mercado, necesitamos definir un pequeño detalle. Sin embargo, este detalle es bastante importante: ¿Qué tipo de simulación realmente vamos a crear? ¿Queremos que los ticks creados sean del tipo que se encuentran en un mercado de valores o en un mercado de divisas?  Pues bien, esta cuestión parece generar confusión y nos obliga a tener dos tipos de modos a simular. Pero debes pensar en lo siguiente: Para ti, que estás programando el sistema, esta cuestión es relativamente simple y puramente burocrática. Pero para el usuario, esto es bastante confuso y muchas veces no quiere realmente saber si está simulando una base de datos similar al mercado de divisas o al mercado de valores. Simplemente quiere que el sistema haga lo que debe hacer, es decir, simular los datos.

Pero al trabajar con información de archivos de barras de 1 minuto, a primera vista, parece que no hay forma de saber si los datos en ese archivo específico provienen de un mercado similar al forex o a un mercado similar al de valores. Al menos en un primer momento. Pero al observar detenidamente estos archivos y comparar los datos en ellos, pronto notarás un cierto patrón. Gracias a este patrón, es posible definir de manera muy clara y efectiva el tipo de representación que se debe utilizar, ya sea BID o LAST. Esto simplemente observando el contenido del archivo de barras.

Por lo tanto, como programador, siempre debes asumir la tarea de hacer que el programa se adapte a un modelo u otro de representación. De modo que para el usuario, la cuestión no necesite estudios sobre el tipo de modelado presente en el archivo de barras de 1 minuto. Porque el programador resolverá este problema para él, el usuario. Si no entiendes de qué estoy hablando, no te preocupes. Sé que puede parecer extraño que esté hablando de esto. Pero el hecho de entender cómo funciona la base de datos para este asunto específico es lo que me llevó a dividir el código del sistema de simulación. Esto para que se implementara mejor. Entonces, antes de entrar en la clase C_Simulation, vamos a entender cómo funcionan las cosas para diferenciar un sistema de representación tipo BID de uno que utiliza el LAST.

Creo que has entendido cómo el sistema puede saber si estaremos reproduciendo datos del mercado de divisas o del mercado de valores. Si no quedó claro, te recomiendo que vuelvas a leer los artículos anteriores hasta que realmente entiendas cómo el sistema puede hacer esta diferenciación. Pero si ya comprendes la parte sobre la repetición, vamos a la gran pregunta: ¿Cómo puede el sistema saber si estará trabajando con datos de un mercado similar al de Forex o al de valores, cuando la única información es un archivo de barras de 1 minuto? 

La forma de saber si debemos utilizar el simulador para crear los ticks de manera que la información sea similar al mercado de Forex o al mercado de valores es: ¡el volumen! ¿Cómo? ¡Sí, el volumen! Así es. La información que determina si el archivo de barras pertenece a un mercado similar al de Forex (donde utilizaremos y usaremos el valor BID como precio negociado) o al mercado de valores (donde utilizaremos el LAST como precio negociado) es precisamente el volumen.

Si no lo has entendido, observa las imágenes a continuación donde se pueden ver fragmentos de un archivo de barras de 1 minuto.


Forex

Figura 02 - Archivo de un activo de Forex


Bolsa

Figura 03 - Archivo de un activo de Bolsa

Aunque no tenemos información sobre si estamos utilizando valores BID o valores LAST como referencia de precio real negociado, debido a que las imágenes de arriba son fragmentos de archivos de barras de 1 minuto, inmediatamente notarás que la única diferencia es el valor del volumen. Por eso se resalta este valor en las imágenes, para que notes dónde está la diferencia.


Consideraciones finales de este artículo

Ahora, comprendamos una cosa: En el artículo "Desarrollo de un sistema de repetición — Simulación de mercado (Parte 11): Nacimiento del SIMULADOR (I)", donde comenzamos a diseñar un sistema de simulación, hemos estado utilizando algunos de los recursos presentes en las barras de 1 minuto para simular un random walk. Este sería un probable movimiento del mercado dentro de esa barra de 1 minuto, según lo informado por el archivo. Sin embargo, en ningún momento de la construcción de dicho mecanismo, y esto incluye el artículo "Desarrollo de un sistema de repetición — Simulación de mercado (Parte 15): Nacimiento del SIMULADOR (V) - RANDOM WALK", donde se construyó el random walk, no fue necesario, o mejor dicho, no consideré necesario tratar casos en los que el volumen fuera cero o el volumen de ticks fuera muy bajo. Pero debido a que hay mercados y activos que tienen estos valores bastante peculiares, se vuelve necesario cubrir tales casos. Si esto no se hace, el servicio se bloqueará o se bloqueará en algún momento, dejándote en un aprieto cuando desees ejecutar algún tipo de simulación. Recuerda: el problema se presenta en el caso de la simulación. En el caso de la repetición, las cosas funcionarán normalmente, siempre y cuando todo esté en perfecta armonía. Dado que no será necesario trabajar con situaciones exóticas, como lo que haremos a partir de ahora, donde tendremos que crear, o mejor dicho, simular posibles movimientos del mercado.

Bueno, por ahora te dejaré asimilar lo que ha sucedido hasta aquí. Ya que la parte de creación del simulador para un mercado similar al de FOREX, donde utilizaremos los valores de BID como método de representación, merece ser examinada con detenimiento. Dado que la cantidad de cosas que se deben explicar para comprender el sistema será bastante diferente de lo que se ha visto hasta ahora. El motivo es que tendremos que reformular algunas partes del cálculo de simulación. Nos vemos en el próximo artículo de esta serie.



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

Archivos adjuntos |
Market_Replay_7vc22.zip (14387.78 KB)
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 23): FOREX (IV) Desarrollo de un sistema de repetición — Simulación de mercado (Parte 23): FOREX (IV)
La creación ahora se realiza en el mismo punto en el que convertimos los ticks en barras. Así, si algo va mal durante la conversión, nos daremos cuenta del error enseguida. Esto se debe a que el mismo código que coloca las barras de 1 minuto en el gráfico cuando avanzamos rápidamente también se utiliza para el sistema de posicionamiento y para colocar las barras durante el avance normal. En otras palabras, el código responsable de esta tarea ya no se duplica en ningún lugar. De esta manera, tenemos un sistema mucho más adecuado tanto para el mantenimiento como para las mejoras.
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 21):  FOREX (II) Desarrollo de un sistema de repetición — Simulación de mercado (Parte 21): FOREX (II)
Vamos a continuar el armado del sistema para cubrir el mercado FOREX. Entonces, para resolver este problema, primero necesitaríamos declarar la carga de los ticks antes de cargar las barras previas. Esto soluciona el problema, pero al mismo tiempo obliga al usuario a seguir un tipo de estructura en el archivo de configuración que, en mi opinión, no tiene mucho sentido. La razón es que, al desarrollar la programación responsable de analizar y ejecutar lo que está en el archivo de configuración, podemos permitir que el usuario declare las cosas en cualquier orden.
Mejore sus gráficos comerciales con una GUI interactiva basada en MQL5 (Parte II): Interfaz móvil (II) Mejore sus gráficos comerciales con una GUI interactiva basada en MQL5 (Parte II): Interfaz móvil (II)
Descubra el potencial de la presentación dinámica de datos en sus estrategias y utilidades comerciales con nuestra guía detallada para crear GUI móviles en MQL5. Sumérjase en los principios fundamentales de la programación orientada a objetos y aprenda a diseñar y utilizar de manera fácil y eficiente una o más GUI móviles en un solo gráfico.
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 20): FOREX (I) Desarrollo de un sistema de repetición — Simulación de mercado (Parte 20): FOREX (I)
La intención inicial de este artículo no será cubrir todas las características de FOREX, sino más bien adaptar el sistema de manera que puedas realizar al menos una repetición del mercado. La simulación quedará para otro momento. Sin embargo, en caso de que no tengas los ticks y solo tengas las barras, con un poco de trabajo, puedes simular posibles transacciones que podrían haber ocurrido en FOREX. Esto será hasta que te muestre cómo adaptar el simulador. El hecho de intentar trabajar con datos provenientes de FOREX dentro del sistema sin modificarlo conlleva errores de rango.