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

MetaTrader 5Probador | 1 noviembre 2023, 09:50
1 259 0
Daniel Jose
Daniel Jose

Introducción

En el artículo anterior, "Desarrollo de un sistema de repetición — Simulación de mercado (Parte 22): FOREX (III)", hicimos algunas modificaciones en el sistema para lograr que el simulador pueda generar información basada en el BID y no solo en el LAST. Pero estas modificaciones no me dejaron satisfecho, y la razón es simple: estamos duplicando código. El hecho de que esta duplicación esté ocurriendo no me resulta cómodo en absoluto.

En ese mismo artículo, hay un momento en el que dejo clara mi insatisfacción:

"... No me preguntes por qué. Pero por alguna razón extraña, que personalmente no tengo ni idea de por qué. Tenemos que agregar esta línea aquí. Si no lo hacemos, el valor informado en el volumen de los 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 utilices el sistema de posicionamiento rápido, evitando que aparezca una barra extraña que esté fuera del tiempo en el gráfico del sistema. Pero aparte de eso, que es un motivo extremadamente extraño, todo lo demás funciona como se espera. Entonces, el nuevo cálculo será este y, con esto, estaremos contabilizando los ticks tanto cuando trabajamos con un activo de representación BID como con un activo que utiliza la representación LAST..."

Pero dado que el código ya estaba cerrado para el artículo y el mismo ya estaba casi listo, dejé las cosas como estaban. Pero eso me molestaba mucho. No tiene sentido que el código funcione en ciertas situaciones y no en otras. Incluso al depurar el código e intentar encontrar la causa del error, realmente no podía encontrarla. Pero al ignorar el código por un momento y observar el diagrama de flujo del sistema (y sí, siempre debes intentar usar un diagrama de flujo para agilizar la codificación), noté que podría hacer algunos cambios para evitar la duplicación del código. Y para empeorar las cosas, el código realmente se estaba duplicando. Esto era lo que causaba el problema que, personalmente, no podía resolver. Pero hay una solución, y vamos a comenzar este artículo resolviendo este problema. Ya que su existencia podría hacer que la codificación adecuada del simulador, para manejar los datos de mercado como se encuentran en FOREX, sea inviable.


Solución al problema del volumen de ticks

En este tema en particular, mostraré cómo se resolvió el problema que causaba un fallo en el volumen de ticks. Para empezar, el código de lectura de los ticks tuvo que ser modificado, quedando como se muestra a continuación:

datetime LoadTicks(const string szFileNameCSV, const bool ToReplay = true)
    {
        int      MemNRates,
                 MemNTicks;
        datetime dtRet = TimeCurrent();
        MqlRates RatesLocal[],
                 rate;
        bool     bNew;
        
        MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
        MemNTicks = m_Ticks.nTicks;
        if (!Open(szFileNameCSV)) return 0;
        if (!ReadAllsTicks(ToReplay)) return 0;         
        rate.time = 0;
        for (int c0 = MemNTicks; c0 < m_Ticks.nTicks; c0++)
        {
            if (!BuildBar1Min(c0, rate, bNew)) continue;
            if (bNew) ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
            m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate;
        }
        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;
    };

Este código solía formar parte del código que convertía los ticks en barras de 1 minuto. Pero ahora utilizaremos un código diferente. La razón es que esta llamada ahora servirá para más de un propósito, y el trabajo que realiza también se utilizará para crear las barras de repetición. Esto evitará que el código de creación de las barras esté duplicado en las clases.

Entonces, echemos un vistazo al código de conversión:

inline bool BuildBar1Min(const int iArg, MqlRates &rate, bool &bNew)
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;
               if (m_Ticks.Info[iArg].last == 0.0) return false;
               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;
               if ((dClose == 0.0) || (m_Ticks.Info[iArg].bid == 0.0)) return false;
               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;
      }
      return true;                    
   }

Todos los elementos borrados en el código se han eliminado, ya que obstaculizarían la creación adecuada que se utilizará en la clase C_Replay. Sin embargo, tuve que agregar estos puntos para informar al autor de llamada sobre lo que ocurrió durante la conversión.

Ahora, hay un detalle importante: esta función originalmente era privada en la clase C_FileTicks. Sin embargo, su nivel de acceso se modificó para que pudiera utilizarse en la clase C_Replay. A pesar de esto, no quiero que salga demasiado de estos límites. Por lo tanto, no será pública, sino protegida. De esta manera, podemos limitar el acceso al nivel máximo permitido por la clase C_Replay. Recuerda que el nivel más alto es la clase C_Replay. Así que solo los procedimientos y funciones que se declaren como públicos en la clase C_Replay podrán ser accedidos fuera de la clase. La construcción interna del sistema estará completamente oculta dentro de esta clase C_Replay.

Ahora, veamos cómo quedó la nueva rutina de creación de las barras.

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

      bool    bNew;
      double  dSpread;
      int     iRand = rand();
                                
      if (BuildBar1Min(m_ReplayCount, def_Rate, bNew))
      {
         m_Infos.tick[0] = m_Ticks.Info[m_ReplayCount];
         if ((!m_Ticks.bTickReal) && (m_Ticks.ModePlot == PRICE_EXCHANGE))
         {                                               
            dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 );
            if (m_Infos.tick[0].last > m_Infos.tick[0].ask)
            {
               m_Infos.tick[0].ask = m_Infos.tick[0].last;
               m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread;
            }else   if (m_Infos.tick[0].last < m_Infos.tick[0].bid)
            {
               m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread;
               m_Infos.tick[0].bid = m_Infos.tick[0].last;
            }
         }
         if (bViewTicks) CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
         CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
      }
      m_ReplayCount++;
#undef def_Rate
   }

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. Pero también quiero que notes algo importante que se ha añadido al código anterior. La emulación de los precios BID y ASK solo ocurrirá si estamos en un sistema simulado y los datos simulados son similares a los del mercado bursátil. Es decir, si la representación se basa en BID, esta emulación no se ejecutará más. Esto es importante para lo que comenzaremos a diseñar en el próximo tema.


Comencemos la simulación de representación BID (Modo FOREX)

A partir de ahora, nos centraremos exclusivamente en la clase C_Simulation. Esto se hace para simular los datos que no están cubiertos en la implementación actual del sistema. Pero antes, necesitamos hacer una pequeña cosa:

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];
         if ((iRet = Simulation(rate[0], local)) < 0)
         {
            ArrayFree(local);
            delete pFileBars;
            return false;
         }
         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));
   }

Si llegara a ocurrir algo incorrecto y deseamos finalizar completamente el sistema, necesitamos una forma de comunicar a las demás clases que la simulación ha fallado. La forma más simple de hacerlo es la siguiente. A pesar de todo, no me gusta mucho la forma en que se creó esta rutina. Aunque dicho procedimiento funcione, le faltan ciertas cosas que debemos comunicar a la clase C_Simulation. Entonces, después de analizar el código, se decidió cambiar la forma en que el procedimiento funcionará. Esto es para evitar duplicar código nuevamente. Así que olvida la rutina anterior, funciona, pero de hecho, usaremos la siguiente rutina:

int SetSymbolInfos(void)
   {
      int iRet;
                                
      CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, iRet = (m_Ticks.ModePlot == PRICE_EXCHANGE ? 4 : 5));
      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);
                                
      return iRet;
   }
//+------------------------------------------------------------------+
   public  :
//+------------------------------------------------------------------+
      bool BarsToTicks(const string szFileNameCSV)
      {
         C_FileBars      *pFileBars;
         C_Simulation    *pSimulator = NULL;
         int             iMem = m_Ticks.nTicks,
                         iRet = -1;
         MqlRates        rate[1];
         MqlTick         local[];
         bool            bInit = false;
                                
         pFileBars = new C_FileBars(szFileNameCSV);
         ArrayResize(local, def_MaxSizeArray);
         Print("Convertendo barras em ticks. Aguarde...");
         while ((*pFileBars).ReadBar(rate) && (!_StopFlag))
         {
            if (!bInit)
            {
               m_Ticks.ModePlot = (rate[0].real_volume > 0 ? PRICE_EXCHANGE : PRICE_FOREX);
               pSimulator = new C_Simulation(SetSymbolInfos());
               bInit = true;
            }
            ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
            m_Ticks.Rate[++m_Ticks.nRate] = rate[0];
            if (pSimulator == NULL) iRet = -1; else iRet = (*pSimulator).Simulation(rate[0], local);
            if (iRet < 0) break;
            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;
         delete pSimulator;
         m_Ticks.bTickReal = false;
                                
         return ((!_StopFlag) && (iMem != m_Ticks.nTicks) && (iRet > 0));
      }

Esta segunda versión es considerablemente más eficiente en cuanto a lo que necesitamos hacer. Además de evitar duplicar código, principalmente porque al utilizarla, obtendremos los siguientes aspectos:

  • Eliminación de la herencia de la clase C_Simulation. Esto hará que el sistema sea aún más libre.
  • Inicialización de los datos del activo, que anteriormente solo se hacían cuando usábamos ticks reales.
  • Ancho adecuado de dígitos a utilizar en la representación gráfica.
  • Uso de la clase C_Simulation como un puntero. Es decir, un mejor aprovechamiento de la memoria del sistema, ya que una vez que la clase haya hecho su trabajo, la memoria que ocupaba será liberada.
  • Garantía de solo 1 punto de entrada y 1 punto de salida desde la función.
Con esto, algunas cosas cambiarán en comparación con lo que está presente en el código del artículo anterior. Pero vamos a seguir en este momento implementando la clase C_Simulation. El gran detalle para el desarrollo de la clase C_Simulation es que podremos tener cualquier cantidad de ticks en el sistema. Pero aunque esto no sea realmente un problema, al menos en este momento, lo complicado es que en muchos casos, el rango que tendremos que cubrir entre el máximo y el mínimo ya será mucho mayor que la cantidad de ticks informados o posibles de ser creados. Esto sin contar la pierna que comienza en el precio de apertura y se dirige hacia uno de los extremos. Y la pierna que comienza en uno de los extremos y va hasta el cierre. Si hacemos este cálculo tratando de usar el RANDOM WALK, en una cantidad enorme de casos, esto no será posible. Por lo tanto, tendremos que renunciar al random walk, que creamos en artículos anteriores, y desarrollar un nuevo método para crear los ticks. Dije que la cuestión de FOREX no era nada sencilla.

El problema de este tipo de enfoque es que muchas veces tendremos que crear y hacer que dos métodos diferentes funcionen lo más armoniosamente posible. Lo peor de todo, y este es el gran detalle, es que en algunos casos, el random walk tiene un modelado mucho más similar a lo que realmente ocurre en un activo real. Pero cuando estamos tratando con un bajo volumen de operaciones (menos de 500 operaciones en 1 minuto), el random walk es completamente inadecuado. En este tipo de escenario, utilizaremos un enfoque un poco más exótico para intentar cubrir todos los posibles casos. Por lo tanto, lo primero que haremos, ya que necesitamos inicializar la clase, es definir el constructor de la clase, y este código se puede ver a continuación:

C_Simulation(const int nDigits)
   {
      m_NDigits       = nDigits;
      m_IsPriceBID    = (SymbolInfoInteger(def_SymbolReplay, SYMBOL_CHART_MODE) == SYMBOL_CHART_MODE_BID);
      m_TickSize      = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
   }

Aquí simplemente estamos inicializando los datos privados de la clase para no tener que buscar estos mismos datos en otros lugares. Así que asegúrate de tener todas las configuraciones correctamente establecidas en el archivo de configuración del activo que se simulará o en cómo se realizará la repetición. De lo contrario, podrías generar errores extraños en el sistema.

Ahora podemos empezar a hacer que las cosas avancen, ya que hemos realizado la inicialización básica de la clase. Vamos a abordar los problemas a resolver. El primer punto es generar un valor aleatorio de tiempo, pero debe ser capaz de manejar todos los ticks que se generarán en las barras de 1 minuto. En realidad, esta es la parte más fácil de la implementación. Pero antes de comenzar a crear funciones, primero necesitamos crear un tipo especial de procedimiento. Esto se puede ver a continuación:

template < typename T >
inline T RandomLimit(const T Limit01, const T Limit02)
   {
      T a = (Limit01 > Limit02 ? Limit01 - Limit02 : Limit02 - Limit01);
      return (Limit01 >= Limit02 ? Limit02 : Limit01) + ((T)(((rand() & 32767) / 32737.0) * a));
   }

Y, ¿qué hace este procedimiento anterior en realidad? Es posible que te sientas asombrado al ver esta función sin entender realmente lo que está sucediendo. Bueno, intentaré explicar de la manera más sencilla posible lo que esta función está haciendo en realidad y por qué tiene esta apariencia tan extraña.

En el código que vamos a crear, necesitamos un tipo de función o procedimiento que sea capaz de generar un valor aleatorio entre dos extremos. Sin embargo, en algunos momentos, necesitaremos que este valor se genere como datos de tipo Double. Mientras que en otros momentos, necesitaremos valores de tipo entero. Sería bastante costoso crear dos procedimientos prácticamente idénticos solo para realizar el mismo tipo de factorización. Para evitar hacer esto, forzamos, o mejor dicho, informamos al compilador que utilice la misma factorización y la sobrecargue de manera que podamos usar la misma función en el código. Pero en términos ejecutables, tendremos de hecho dos funciones diferentes. Para lograr esto, utilizamos esta declaración aquí. Con esto definimos un tipo, que en este caso es la letra T. Esto debe repetirse en todos los puntos en los que necesitemos que el compilador ajuste el tipo para nosotros. Por lo tanto, debes tener cuidado de no mezclar las cosas. Deja que el compilador haga los ajustes para evitar problemas de conversión de tipo.

De esta manera, siempre realizaremos el mismo cálculo, pero se ajustará en función del tipo de variable que se utilice. Esto lo hará el compilador, ya que él decidirá cuál es el tipo correcto. Así, podremos generar un número pseudoaleatorio en cada llamada, independientemente del tipo que estemos usando. Pero ten en cuenta que el tipo de ambos límites debe ser el mismo. Es decir, no tiene sentido mezclar double con enteros o enteros largos con enteros cortos. Esto no funcionará. Esta es la única limitación de este tipo de enfoque en el que utilizamos la sobrecarga de tipos.

Pero aún no hemos terminado. La razón por la que creamos esta función anterior es precisamente para evitar la generación de macros dentro del código de la clase C_Simulation. Entonces, ahora pasamos al siguiente paso, generar el sistema de temporización de la simulación. Esta generación se puede ver en el código a continuación:

inline void Simulation_Time(int imax, const MqlRates &rate, MqlTick &tick[])
   {
      for (int c0 = 0, iPos, v0 = (int)(60000 / rate.tick_volume), v1 = 0, v2 = v0; c0 <= imax; c0++, v1 = v2, v2 += v0)
      {
         iPos = RandomLimit(v1, v2);
         tick[c0].time = rate.time + (iPos / 1000);
         tick[c0].time_msc = iPos % 1000;
      }
   }

Aquí estamos simulando el tiempo para que sea ligeramente aleatorio. Es cierto que parece bastante confuso a primera vista. Pero créeme, el tiempo aquí es aleatorio, aunque aún no sigue la lógica esperada por la clase C_Replay. Esto se debe a que el valor en milisegundos no está correctamente ajustado. Ese ajuste se realizará en otro punto. Aquí solo queremos que el tiempo se genere de forma aleatoria, pero dentro del límite de la barra de 1 minuto. ¿Y cómo lo estoy haciendo? Bueno, en primer lugar, dividimos el tiempo de 60 segundos, que en realidad son 60,000 milisegundos, entre el número de ticks que deben generarse. Este valor es importante para nosotros, ya que nos dirá cuál es el rango límite que utilizaremos. Una vez hecho esto, realizamos algunas asignaciones simples en cada iteración del bucle. Ahora, el secreto para generar un temporizador aleatorio está en estas tres líneas dentro del bucle. En la primera línea, le pedimos al compilador que genere una llamada en la que utilizaremos datos de tipo entero, y esta llamada devolverá un valor dentro del rango especificado. Luego realizamos dos cálculos muy simples. Primero ajustamos el valor generado al tiempo de un minuto de barra, y luego utilizamos ese mismo valor generado para ajustar el tiempo en milisegundos. De esta manera, cada uno de los ticks tendrá un valor completamente aleatorio en cuanto al tiempo. Recuerda que solo estamos ajustando el tiempo en este primer momento. El propósito de este ajuste es evitar que las cosas sean demasiado predecibles.

Genial. Ahora vamos a simular los precios. Recordando una vez más que me centraré únicamente en el sistema de representación BID. Luego, uniré el sistema de simulación para que tengamos una forma mucho más general de realizar esta simulación, que cubra tanto el BID como el LAST. Pero aquí, en primer lugar, me centraré en el BID. Para realizar esta simulación en este primer momento, mantendremos siempre el spread a la misma distancia. Esto es para no complicar innecesariamente el código antes de verificar si realmente funciona. Esta primera simulación se realiza mediante el uso de varias rutinas bastante cortas. Utilizaremos rutinas cortas para que todo sea lo más modular posible. Pero después entenderás la razón de esto.

Entonces, veamos la primera de las llamadas que se ejecutarán para generar la simulación del BID:

inline void Simulation_BID(int imax, const MqlRates &rate, MqlTick &tick[])
   {
      bool    bHigh  = (rate.open == rate.high) || (rate.close == rate.high), 
      bLow = (rate.open == rate.low) || (rate.close == rate.low);
                                                        
      Mount_BID(0, rate.open, rate.spread, tick);     
      for (int c0 = 1; c0 < imax; c0++)
      {
         Mount_BID(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), rate.spread, tick); 
         bHigh = (rate.high == tick[c0].bid) || bHigh;
         bLow = (rate.low == tick[c0].bid) || bLow;
      }
      if (!bLow) Mount_BID(Unique(imax, rate.high, tick), rate.low, rate.spread, tick);
      if (!bHigh) Mount_BID(Unique(imax, rate.low, tick), rate.high, rate.spread, tick);
      Mount_BID(imax, rate.close, rate.spread, tick);
   }

Observa que esta rutina anterior es bastante fácil de entender. Aunque la parte aparentemente más complicada sea la construcción aleatoria del valor del BID. Pero aún así, es relativamente simple. Generaremos valores pseudoaleatorios dentro de un rango, entre el valor máximo y mínimo de la barra. Pero ten en cuenta que normalizo el valor. Esto se debe a que normalmente el valor generado estará fuera del rango de precios. Por lo tanto, tenemos que normalizarlo. Pero creo que el resto de la rutina no debería ser un problema para que puedas entenderla realmente.

Si observas con atención, verás que tenemos dos funciones que se mencionan con frecuencia en la parte de la simulación: MOUNT_BID y UNIQUE. Cada una de ellas sirve para un propósito específico. Pero empecemos por ver la función Unique, cuyo código está a continuación:

inline int Unique(const int imax, const double price, const MqlTick &tick[])
   {
      int iPos = 1;
                                
      do
      {
         iPos = (imax > 20 ? RandomLimit(1, imax - 1) : iPos + 1);
      }while ((m_IsPriceBID ? tick[iPos].bid : tick[iPos].last) == price);
                                
      return iPos;
   }

Esta función sirve para evitar que el valor de uno de los límites o cualquier otro precio sea eliminado durante la generación de la posición aleatoria. Sin embargo, por ahora, la usaremos exclusivamente en los límites. Observa que podemos usar tanto el valor de modelado BID como el valor de modelado LAST. Pero por ahora, solo usaremos esta función en el BID. Ese es el único propósito de esta función: asegurarse de que no sobrescribamos un valor límite.

Ahora echemos un vistazo a la función Mount_BID, cuyo código se encuentra a continuación:

inline void Mount_BID(const int iPos, const double price, const int spread, MqlTick &tick[])
   {
      tick[iPos].bid = price;
      tick[iPos].ask = NormalizeDouble(price + (m_TickSize * spread), m_NDigits);
   }

A pesar de que en este primer momento, este código es bastante simple y se acerca a la belleza de la programación pura, facilita mucho nuestra vida. Evita que repitamos código en varios puntos y, sobre todo, evita que olvidemos normalizar el valor que debe colocarse en la posición del precio ask. Si esta normalización no se realiza, tendremos problemas más adelante cuando utilicemos este valor ASK. Algo importante a tener en cuenta aquí es que el valor del precio ask siempre estará desplazado del valor del spread. Sin embargo, por ahora, este desplazamiento es siempre constante. Esto se debe a que esta es la primera implementación y si implementara ahora el sistema de aleatorización, te resultaría muy confuso entender por qué y cómo se aleatoriza el valor del spread.

El valor del spread indicado aquí en realidad es el valor que se indica en la barra específica de 1 minuto. Cada una de las barras puede tener un spread diferente. Pero aquí hay otra cosa que también debes entender. Si estás realizando una simulación con la intención de tener un sistema que se asemeje a lo que podría haber ocurrido en el mercado real, es decir, los datos contenidos en un archivo de ticks reales, notarás que este spread que se debe usar es el menor de los valores presentes durante la formación de la barra de 1 minuto. Pero si estás realizando una simulación aleatoria en la que los datos pueden o no parecerse a los que posiblemente ocurrieron en el mercado real, este spread puede tener cualquier valor. Sin embargo, aquí me mantendré fiel a la idea de tratar de construir lo que podría haber ocurrido en el mercado. Por lo tanto, el valor del spread siempre será el que se indique en el archivo de barras.

Hay otra rutina necesaria para el sistema. Esta será responsable de ajustar el tiempo de manera que la clase C_Replay tenga los valores correctos de temporización. Este código se puede ver a continuación:

inline void CorretTime(int imax, MqlTick &tick[])
   {
      for (int c0 = 0; c0 <= imax; c0++)
         tick[c0].time_msc += (tick[c0].time * 1000);
   }

Lo que hace este procedimiento es simplemente corregir adecuadamente el tiempo dado en milisegundos. Si observas, verás que el cálculo es el mismo que se utiliza en la rutina que carga los ticks reales de un archivo. La razón de hacerlo de manera tan modular se debe a que puede ser interesante llevar un registro de cada una de las funciones ejecutadas. Si el código estuviera todo unido, sería más complicado crear tales registros. Pero de esta manera, puedes crear los registros y estudiarlos, y así verificar qué se debe o no se debe mejorar para satisfacer tus necesidades particulares.

Un detalle importante: en este primer momento, bloquearé el uso del sistema basado en la representación LAST. Esto se debe a que lo modificaremos en algunos puntos para que pueda funcionar con activos o momentos de baja liquidez. Actualmente, esto no es posible, pero lo corregiremos más adelante. Así que no te preocupes si intentas ejecutar una simulación basada en la representación LAST y el sistema no lo permite. Se corregirá más adelante.

Para garantizar esto, usaremos una artimaña de programación. Algo de extrema complejidad y muy elaborado. Mira en el fragmento a continuación de qué se trata:

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      int imax;
                        
      imax = (int) rate.tick_volume - 1;
      Simulation_Time(imax, rate, tick);
      if (m_IsPriceBID) Simulation_BID(imax, rate, tick); else return -1;
      CorretTime(imax, tick);

      return imax;
   }

Ahora, atención: cada vez que el sistema utilice el modo de representación LAST, mostrará un error. Pero como se mencionó antes, no debes desesperarte. Esto se debe a que la simulación basada en el modo LAST (utilizado en Bolsa) se mejorará. Por lo tanto, fue necesario agregar esta artimaña extremadamente compleja y elaborada. Si se intenta ejecutar la simulación basada en LAST, devolveremos un valor NEGATIVO. ¿No es esto una forma extremadamente elaborada de hacer las cosas? [RISAS].

Pero antes de cerrar este artículo, mejoraremos el tema de la simulación de representación BID. Así tendremos una forma un poco mejor, al menos en lo que respecta a la parte de aleatorización, en los resultados. Básicamente, necesitamos modificar un solo punto para tener un valor de spread aleatorio. Puedes elegir hacerlo en la función Mount_Bid o en la función Simulation_Bid. De alguna manera, esto no importa mucho, pero para garantizar que tengamos un valor de spread mínimo según se informa en el archivo de barras de 1 minuto, realizaremos la modificación en la función que se muestra a continuación:

inline void Simulation_BID(int imax, const MqlRates &rate, MqlTick &tick[])
   {
      bool    bHigh  = (rate.open == rate.high) || (rate.close == rate.high), 
      bLow = (rate.open == rate.low) || (rate.close == rate.low);

      Mount_BID(0, rate.open, rate.spread, tick);     
      for (int c0 = 1; c0 < imax; c0++)
      {
         Mount_BID(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), (rate.spread + RandomLimit((int)(rate.spread | (imax & 0xF)), 0)), tick);
         bHigh = (rate.high == tick[c0].bid) || bHigh;
         bLow = (rate.low == tick[c0].bid) || bLow;
      }
      if (!bLow) Mount_BID(Unique(imax, rate.high, tick), rate.low, rate.spread, tick);
      if (!bHigh) Mount_BID(Unique(imax, rate.low, tick), rate.high, rate.spread, tick);
      Mount_BID(imax, rate.close, rate.spread, tick);
   }

Aquí estamos asegurando una aleatorización del valor del spread, aunque es cierto que esta aleatorización es solo demostrativa. Si lo deseas, puedes hacer algo un poco diferente en términos de límites. Solo tendrás que ajustar un poco las cosas. Ahora debes entender que estoy utilizando esta aleatorización un tanto extraña para algunos, pero en realidad, lo que estoy haciendo es asegurarme de que se pueda utilizar el valor máximo posible para aleatorizar el spread. Este valor se basa en un cálculo en el que combinamos bit a bit el valor del spread con un valor que puede variar de 1 a 16, ya que solo estamos utilizando una parte de todos los bits. Pero debes tener en cuenta lo siguiente: si el spread es cero, y en algunos momentos de hecho será cero, aún así tendremos un valor que será como mínimo 3, ya que los valores 1 y 2 en realidad no crearán una aleatorización del spread, esto se debe a que el valor 1 indica solo apertura igual al cierre, y el valor 2 indica que la apertura puede ser igual o diferente al cierre. Pero en este caso, el valor 2 es el que realmente creará el valor. En todos los demás casos, tendremos la creación de una aleatorización en el spread.

De esta manera, creo que queda claro por qué no he colocado la aleatorización en la rutina Mount_Bid. Si se hiciera esto, en algunos momentos, el spread mínimo informado por el archivo de barras no sería realmente el valor correcto. Pero como mencioné, eres libre de experimentar y adaptar el sistema a tu gusto y estilo.


Conclusión

En este artículo, resolvimos los problemas relacionados con la duplicación de código. Creo que ha quedado claro y perfectamente entendido los problemas que surgen cuando utilizamos código duplicado. En proyectos muy grandes, siempre debemos tener cuidado con esto. Incluso este código aquí, que no es tan grande, puede tener graves problemas debido a este descuido.

Un último detalle que también merece ser mencionado es el hecho de que en un archivo de ticks reales, hay momentos en los que realmente tendremos algún tipo de movimiento "falso". Pero aquí eso no ocurre, estos movimientos "falsos" son cuando ocurren variaciones solo en uno de los precios, ya sea en el BID o en el ASK. Pero por simplicidad y para no complicar innecesariamente el código, dejé fuera tales situaciones. En mi opinión, no tiene mucho sentido que un sistema de repetición que está simulando un mercado tenga realmente esos movimientos. Esto no aportaría ninguna mejora en términos de operatividad. Ya que por cada cambio en el BID sin la presencia del ASK, tendríamos que hacer un ASK sin la presencia del BID. Esto para mantener el equilibrio requerido por el mercado real.

Básicamente, así cerramos la cuestión de la simulación del BID, al menos en este primer intento de hacerlo. Puede ser que en el futuro realice cambios en este sistema para que funcione de otra manera. Pero al usarlo con datos del mercado de divisas, noté que funciona de manera bastante adecuada, aunque para otros mercados podría no ser suficiente.

En el archivo adjunto, tendrás acceso al sistema en su estado actual de desarrollo. Pero como mencioné durante este artículo, no debes intentar hacer simulaciones con activos de la bolsa, solo con los de Forex. Aunque puedas realizar una repetición con todos ellos, la simulación estará deshabilitada para los activos de la bolsa. En el próximo artículo corregiremos esto, mejorando el sistema de simulación de la bolsa para que pueda funcionar en momentos de baja liquidez. Así concluimos esta cuestión sobre la simulación. ¡Nos vemos en el próximo artículo!

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

Archivos adjuntos |
Market_Replay_7vx23.zip (14388.45 KB)
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 22): FOREX (III) Desarrollo de un sistema de repetición — Simulación de mercado (Parte 22): FOREX (III)
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.
Redes neuronales: así de sencillo (Parte 48): Métodos para reducir la sobreestimación de los valores de la función Q Redes neuronales: así de sencillo (Parte 48): Métodos para reducir la sobreestimación de los valores de la función Q
En el artículo anterior, presentamos el método DDPG, que nos permite entrenar modelos en un espacio de acción continuo. Sin embargo, al igual que otros métodos de aprendizaje Q, el DDPG tiende a sobreestimar los valores de la función Q. Con frecuencia, este problema provoca que entrenemos los agentes con una estrategia subóptima. En el presente artículo, analizaremos algunos enfoques para superar el problema mencionado.
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.