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

Desarrollo de un sistema de repetición — Simulación de mercado (Parte 24): FOREX (V)

MetaTrader 5Probador | 5 febrero 2024, 17:31
274 0
Daniel Jose
Daniel Jose

Introducción

En el artículo anterior, "Desarrollo de un sistema de repetición — Simulación de mercado (Parte 23): FOREX (IV)", abordamos la implementación de un bloqueo parcial en el sistema de simulación. Este bloqueo fue necesario porque el sistema enfrentaba dificultades al tratar con volúmenes de operaciones extremadamente bajos. Esta limitación se hacía evidente al intentar realizar simulaciones basadas en el trazado de LAST, situación en la que el sistema corría el riesgo de bloquearse al intentar generar la simulación. Tal problema era particularmente pronunciado en momentos en que el volumen de operaciones, representado en la barra de 1 minuto, se mostraba insuficiente. Para sortear esta cuestión, mostraré cómo adaptar la implementación, siguiendo los principios aplicados anteriormente en la simulación basada en el trazado de BID. Este enfoque es ampliamente utilizado en las divisas, razón por la cual este es el quinto artículo dedicado al tema. Sin embargo, nuestro enfoque aquí no será específicamente las divisas. Nuestro objetivo es mejorar el sistema de simulación de bolsa de valores.


Iniciamos los cambios en la implementación

Como primer paso, introduciremos una estructura privada a nuestra clase. Esto se debe a la existencia de datos o información compartidos por ambos modos de simulación - LAST y BID. Estos elementos comunes, básicamente valores, serán consolidados en una única estructura. Así, definimos la siguiente estructura:

struct st00
{
   bool    bHigh, bLow;
   int     iMax;
}m_Marks;

Aunque simple a primera vista, esta estructura es lo suficientemente robusta para permitirnos mejorar nuestro código. Con todos los valores comunes centralizados en un único lugar, la eficiencia y la organización de nuestro trabajo mejoran significativamente.

Con esta preparación, estamos listos para nuestra primera modificación efectiva en el código.

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      m_Marks.iMax = (int) rate.tick_volume - 1;
      m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high);
      m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low);
      Simulation_Time(rate, tick);
      if (m_IsPriceBID) Simulation_BID(rate, tick);
      else Simulation_LAST(rate, tick);
      else return -1;
      CorretTime(tick);

      return m_Marks.iMax;
   }

Hoy eliminaremos la restricción que impedía la ejecución de simulaciones basadas en el trazado de LAST e introduciremos un nuevo punto de entrada específico para este tipo de simulación. Ahora, vean que todo el mecanismo operativo se fundamentará en los principios del mercado de divisas. La principal distinción en esta rutina reside en la separación entre las simulaciones BID y LAST. Pero, es importante notar que la metodología empleada en la aleatorización del tiempo y su ajuste para la compatibilidad con la clase C_Replay permanece idéntica en ambos tipos de simulación. Esta uniformidad es ventajosa, ya que cambios en un modo pueden beneficiar al otro, especialmente en lo que respecta a la gestión de tiempo entre ticks. Naturalmente, las modificaciones aquí discutidas también influenciarán la simulación basada en el trazado de BID. La comprensión de estos cambios es directa, por lo tanto, no me extenderé en estos detalles.

Luego de volver al código de nuestro interés, con la implementación de la llamada para la función de simulación del trazado de LAST, podemos examinar los aspectos iniciales de esta llamada. Esto es, el esqueleto de la función. Este se presenta en el fragmento de código a continuación:

inline void Simulation_LAST(const MqlRates &rate, MqlTick &tick[])
   {
      if (CheckViability_LAST(rate))
      {
      }else
      {
      }
      DistributeVolumeReal(rate, tick);
   }

Dentro de este contexto, realizaremos dos acciones esenciales al trabajar con el trazado de LAST. La primera implica la verificación de la viabilidad de utilización del sistema de paseo aleatorio para modelado, como se discutió en publicaciones anteriores. Para aquellos que no han seguido, recomiendo la lectura del artículo "Desarrollo de un sistema de repetición - Simulación de mercado (Parte 15): Nacimiento del SIMULADOR (V) - RANDOM WALK".  La segunda acción consiste en la distribución del volumen negociado entre los ticks posibles. Estos procedimientos son y siempre serán esenciales al tratar con simulaciones basadas en el trazado de LAST.

Antes de detallar la estructuración de ese esqueleto, vamos a explorar dos funciones cruciales para la simulación basada en trazados LAST. La primera función se presenta a continuación:

inline bool CheckViability_LAST(const MqlRates &rate)
   {
#define macro_AdjustSafetyFator(A) (A + (A * 1.4));
                                
      double  v0, v1, v2;
                                
      v0 = macro_AdjustSafetyFator(rate.high - rate.low);
      v1 = (rate.open - rate.low);
      v2 = (rate.high - rate.open);
      v0 += macro_AdjustSafetyFator(v1 > v2 ? v1 : v2);
      v1 = (rate.close - rate.low);
      v2 = (rate.high - rate.close);
      v0 += macro_AdjustSafetyFator(v1 > v2 ? v1 : v2);
      return ((int)(v0 / m_TickSize) < rate.tick_volume);
                                
#undef macro_AdjustSafetyFator
   }

Esta función es responsable de verificar la viabilidad de generar un paseo aleatorio dentro de los límites disponibles. ¿Cómo procederemos? El método consiste en determinar la cantidad de ticks disponibles para uso, información derivada de la barra en análisis y del número de ticks a ser abarcados, cálculo este realizado por la función.

Importante resaltar que no utilizaremos directamente el valor calculado para definir el área de cobertura. Esto porque, si adoptáramos tal enfoque directo, el paseo aleatorio resultante tendría un aspecto artificial, con movimientos excesivamente previsibles. Para mitigar esta cuestión, introduciremos un ajuste en los cálculos. Este ajuste, implementado mediante una macro, consiste en definir un área 30% mayor, lo que actúa como un factor de seguridad para la adecuada formación del paseo aleatorio. Otro aspecto crucial es la necesidad de considerar siempre la mayor distancia posible en los cálculos, pues durante la aleatorización, esa extensión ampliada puede ser requerida. De esta manera, el proceso de cálculo ya contempla esta eventualidad.

El resultado final indica si el uso del paseo aleatorio es viable o si debemos recurrir a un método de aleatorización más directo. Sin embargo, esta decisión la toma el procedimiento autor de llamada, no aquí.

La segunda función se detalla a continuación:

inline void DistributeVolumeReal(const MqlRates &rate, MqlTick &tick[])
   {
      for (int c0 = 0; c0 <= m_Marks.iMax; c0++)
         tick[c0].volume_real = 1.0;
      for (int c0 = (int)(rate.real_volume - rate.tick_volume); c0 > 0; c0--)
         tick[RandomLimit(0, m_Marks.iMax)].volume_real += 1.0;                                  
   }

Aquí, el objetivo es distribuir de manera aleatoria el volumen total negociado en la barra de 1 minuto. El primer bucle realiza una distribución básica, para asegurar que cada tick reciba un volumen mínimo inicial. El segundo bucle, entonces, se encarga de distribuir aleatoriamente el volumen restante, para evitar, así, que este se concentre en un único tick, aunque esa posibilidad aún existe, es significativamente reducida por la metodología de distribución adoptada.

Estas funciones ya eran parte de la implementación original, discutida en los artículos sobre la implementación del paseo aleatorio. Sin embargo, estamos adoptando un enfoque más modular esta vez, con el fin de maximizar la reutilización del código desarrollado.

Luego de reflexionar un poco más, identificamos elementos aún compartidos entre las simulaciones de BID y LAST. Tales elementos incluyen la definición de puntos de entrada y salida, además de la posibilidad de identificar puntos extremos. Con esto en mente, buscamos reaprovechar códigos previamente elaborados. Para ello, será necesaria una modificación en un fragmento de código presentado en el artículo anterior. La modificación propuesta es la siguiente:

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

Iniciamos por la sustitución del nombre antiguo de la función por uno nuevo, seguida de la inserción de una prueba interna para determinar si la simulación será basada en el trazado de BID o LAST. De esta manera, la misma función puede ser adaptada para generar ticks basados tanto en BID como en LAST, conforme a los datos observados en el archivo de barras de 1 minuto.

Esta modificación demanda aún más dos ajustes. Recuerda: El objetivo es integrar de manera simplificada las simulaciones de BID y LAST, de modo que solo los aspectos verdaderamente exclusivos de cada uno sean manipulados por el método apropiado. Cualquier otro aspecto será tratado de forma común a ambos. Sigue, entonces, la próxima modificación en el código de la clase de simulación:

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

Los fragmentos que no aparecen fueron excluidos, para permitir que el código siga funcionando adecuadamente. Sin embargo, es importante observar que necesitamos esos fragmentos excluidos, los cuales son comunes al sistema de simulación para el trazado de LAST. Así, ese código común fue transferido a la función detallada abajo:

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      m_Marks.iMax = (int) rate.tick_volume - 1;
      m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high);
      m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low);
      Simulation_Time(rate, tick);
      MountPrice(0, rate.open, rate.spread, tick);    
      if (m_IsPriceBID) Simulation_BID(rate, tick);
      else Simulation_LAST(rate, tick);
      if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick);
      if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick);
      MountPrice(m_Marks.iMax, rate.close, rate.spread, tick);
      CorretTime(tick);

      return m_Marks.iMax;
   }

Y ahí vamos. Ahora sí, llegamos a un punto crucial. Sin necesidad de realizar cambios significativos, solo a través de una reutilización inteligente del código, logramos integrar ambas simulaciones. Por lo tanto, ahora disponemos tanto de la simulación basada en el trazado de BID como en el de LAST. Incluimos los valores de entrada, salida y límites automáticamente, en caso de no haber sido previamente establecidos por la simulación aleatorizada. Con este ajuste estratégico, expandimos considerablemente el alcance de nuestra simulación. Además, la complejidad del código se mantuvo estable, sin incrementos significativos. Si utilizas el código como se ha presentado hasta ahora, tendrás un buen desempeño en la simulación de trazado de BID. Y, para el trazado de LAST, ya es posible insertar la barra en el gráfico, aunque el valor del mínimo esté incorrecto, registrándose como cero, a pesar de estar definido correctamente. Esta imprecisión ocurre porque, en los ticks no inicializados —solo con el tiempo definido—, todos los valores de LAST son cero. Esto no sería un problema, no fuera por el hecho de ya haber distribuido el volumen negociado. Por lo tanto, es hora de revisitar nuestra rutina de asignación de los valores de precio LAST a cada tick, para permitir de ese modo una simulación con datos precisos.

Nuestra rutina de simulación de precio LAST permanece igual. Sin embargo, al examinar el código de la simulación BID, surge una reflexión: ¿Sería posible aprovechar este mismo código para simular el precio LAST, especialmente si el número de ticks disponibles es insuficiente para una ejecución completa del paseo aleatorio? Esta pregunta también me ocurrió. Tras un análisis cuidadoso, concluimos que solo es necesaria una pequeña modificación en la rutina de simulación BID. No obstante, para evitar confusiones futuras, en caso de que se realicen modificaciones en el código, es esencial planificar cuidadosamente este cambio desde ahora. Vale recordar que el procedimiento de simulación BID es activado por la función que acabamos de mencionar. Considerando que el concepto para simular LAST es similar, podemos buscar una manera de mantener la llamada anterior intacta. Así, adaptaremos el procedimiento de simulación BID para que también pueda abarcar la simulación LAST, en situaciones donde el paseo aleatorio no se aplica.

Algunos podrían cuestionar el enfoque, pero aquí está cómo el código de simulación para el trazado de BID fue adaptado para incluir también la simulación para el trazado de LAST.

inline void Simulation_BID(const MqlRates &rate, MqlTick &tick[])
inline void Random_Price(const MqlRates &rate, MqlTick &tick[])
   {
      for (int c0 = 1; c0 < m_Marks.iMax; c0++)
      {
         MountPrice(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), (rate.spread + RandomLimit((int)(rate.spread | (m_Marks.iMax & 0xF)), 0)), tick);
         m_Marks.bHigh = (rate.high == (m_IsPriceBID ? tick[c0].bid : tick[c0].last)) || m_Marks.bHigh;
         m_Marks.bLow = (rate.low == (m_IsPriceBID ? tick[c0].bid : tick[c0].last)) || m_Marks.bLow;
         m_Marks.bHigh = (rate.high == tick[c0].bid) || m_Marks.bHigh;
         m_Marks.bLow = (rate.low == tick[c0].bid) || m_Marks.bLow;
      }
   }

Para evitar confusiones durante las ejecuciones, opté por renombrar la función. Este es un pequeño precio a pagar por el beneficio de tener un código más versátil. El punto crucial de la adaptación reside aquí. Estos dos elementos del código son particularmente notables. El operador ternario, a pesar de su controversia por ser considerado confuso por algunos, es una herencia valiosa del lenguaje C que ofrece gran utilidad. En estos segmentos, se verifica el tipo de trazado en ejecución para ajustar el precio límite adecuadamente. Es importante destacar que, independientemente del tipo de trazado, la aleatorización ocurre de forma idéntica. Así, logramos integrar los dos métodos,y creamos con eso un sistema de simulación efectivo tanto para BID como para LAST.

Tras las modificaciones, la simulación se asemeja mucho a lo discutido en el artículo "Desarrollo de un sistema de repetición - Simulación de mercado (Parte 13): Nacimiento del SIMULADOR (III)". Sin embargo, aún no hemos implementado la simulación vía paseo aleatorio en el sistema. Esto se debe a que, hasta el momento, el código ha sido ajustado conforme presentado:

inline void Simulation_LAST(const MqlRates &rate, MqlTick &tick[])
   {
      if (CheckViability_LAST(rate))
      {
      }else Random_Price(rate, tick);
      DistributeVolumeReal(rate, tick);
   }

Por lo tanto, aún no estamos simulando escenarios típicos donde el uso del paseo aleatorio sería viable y adecuado. Sin embargo, mi objetivo es permitir que la simulación de precio BID, bajo ciertas condiciones, pueda emplear el paseo aleatorio, así como la simulación para el trazado de precio LAST. Esto plantea la pregunta: «¿es posible?» Y más aún, ¿podemos hacer que este enfoque sea aún más interesante y sostenible, al punto de que mercados similares a FOREX también puedan beneficiarse del método paseo aleatorio para simular movimientos de precio? La respuesta es afirmativa. Sí, es posible realizar esto. Antes incluso de implementar el paseo aleatorio específicamente para el trazado de precio LAST, serán necesarios algunos cambios.

inline bool CheckViability(const MqlRates &rate)
   {
#define macro_AdjustSafetyFator(A) (int)(A + ceil(A * 1.7))
                                
      int i0, i1, i2;
                                
      i0 = macro_AdjustSafetyFator((rate.high - rate.low) / m_TickSize);
      i1 = (int)((rate.open - rate.low) / m_TickSize);
      i2 = (int)((rate.high - rate.open) / m_TickSize);
      i0 += macro_AdjustSafetyFator(i1 > i2 ? i1 : i2);
      i0 += macro_AdjustSafetyFator((i1 > i2 ? (rate.high - rate.close) : (rate.close - rate.low) / m_TickSize));

      return (i0 < rate.tick_volume);
                                
#undef macro_AdjustSafetyFator
   }

El procedimiento mencionado es una evolución de aquel previamente utilizado para evaluar la viabilidad de creación de un movimiento paseo aleatorio. Debido a detalles técnicos y la introducción de factores de seguridad ampliados, este enfoque fue refinado para evitar indicaciones erróneas sobre la posibilidad de ejecutar o no el movimiento. Este cambio se justifica porque el procedimiento de verificación ahora no se limita solo a la simulación de trazado de LAST. También se aplica para evaluar la aplicabilidad del paseo aleatorio en la simulación de trazado de BID. A primera vista, esto puede parecer simple, pero requiere precauciones específicas. Para ilustrar mejor, nos referimos a la figura 01.

Figura 01

Figura 01 - Cálculo del camino más largo posible

La función en cuestión realiza exactamente lo que la figura 01 ilustra: calcula el trayecto más extenso posible para generar la barra de 1 minuto. Esta metodología fue ajustada para optimizar el proceso, para permitir que el trazado de BID también se beneficie. Sin embargo, el aumento del factor de seguridad de 1.4 a 1.7 introduce una complejidad adicional para la utilización del paseo aleatorio en ciertos activos. El cálculo inicia por la determinación de la distancia entre el precio de apertura de la barra y sus extremos. Con esa información, utilizamos el valor más elevado en la primera etapa del cálculo.  A continuación, el otro valor se emplea para que el movimiento abarque la barra conforme demostrado en la figura 01. Concluimos con una verificación simple de la viabilidad del uso del paseo aleatorio.

Puede parecer que una nueva alteración en el código de la clase se hace necesaria. Y de hecho, es el caso. Sin embargo, esta modificación se realizará de forma que resulte en una integración más armoniosa.

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      m_Marks.iMax = (int) rate.tick_volume - 1;
      m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high);
      m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low);
      Simulation_Time(rate, tick);
      MountPrice(0, rate.open, rate.spread, tick);
      if (CheckViability(rate))
      {
      }else Random_Price(rate, tick);
      if (!m_IsPriceBID) DistributeVolumeReal(rate, tick);
      if (m_IsPriceBID) Random_Price(rate, tick);
      else Simulation_LAST(rate, tick);
      if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick);
      if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick);
      MountPrice(m_Marks.iMax, rate.close, rate.spread, tick);
      CorretTime(tick);

      return m_Marks.iMax;
   }

Los fragmentos que ya no se aplican fueron excluidos del código, mientras que las adiciones están destacadas en verde. Ahora, es posible observar casos en que una barra de 1 minuto, en mercados similares a FOREX, puede generar ticks de manera similar a los mercados de acciones, y viceversa. Esto permite que el simulador abarque una amplia variedad de movimientos de mercado, independientemente del volumen de ticks ocurridos. Sin embargo, es importante destacar que el código responsable por la creación del paseo aleatorio aún no ha sido incorporado al procedimiento mencionado. Vamos, entonces, a explorar cómo este código será implementado para atender a ambos tipos de trazado, enfocándonos específicamente en esta funcionalidad.


Implementación del paseo aleatorio para precios BID y LAST

Como se discutió anteriormente, la clase C_Simulation fue desarrollada para permitir un tratamiento uniforme entre las simulaciones de trazados BID y LAST. El objetivo fue crear una simulación lo más precisa posible. Hemos llegado a un punto crucial donde el próximo paso es implementar un procedimiento capaz de operar con el paseo aleatorio, utilizando el mínimo de código necesario, sin complicar el proceso. Esta adaptación se basa en lo que se detalló en el artículo "Desarrollo de un sistema de repetición - Simulación de mercado (Parte 15): Nacimiento del SIMULADOR (V) - RANDOM WALK". Por lo tanto, no entraré en detalles sobre la implementación original del paseo aleatorio o sobre cómo la idea fue concebida. Para aquellos interesados en profundizar, recomiendo la consulta al artículo mencionado. Aquí, el enfoque está en ajustar ese código para este nuevo contexto.

inline int RandomWalk(int In, int Out, const double Open, const double Close, double High, double Low, const int Spread, MqlTick &tick[], int iMode)
   {
      double vStep, vNext, price, vH = High, vL = Low;
      char i0 = 0;
                                
      vNext = vStep = (Out - In) / ((High - Low) / m_TickSize);
      for (int c0 = In, c1 = 0, c2 = 0; c0 <= Out; c0++, c1++)
      {
         price = (m_IsPriceBID ? tick[c0 - 1].bid : tick[c0 - 1].last) + (m_TickSize * ((rand() & 1) == 1 ? -1 : 1));
         price = (price > High ? price - m_TickSize : (price < Low ? price + m_TickSize : price));
         MountPrice(c0, price, (Spread + RandomLimit((int)(Spread | (m_Marks.iMax & 0xF)), 0)), tick);
         switch (iMode)
         {
            case 0:
               if (price == Close) return c0; else break;
            case 1:
               i0 |= (price == High ? 0x01 : 0);
               i0 |= (price == Low ? 0x02 : 0);
               vH = (i0 == 3 ? High : vH);
               vL = (i0 ==3 ? Low : vL);
               break;
            default: break;
         }
         if (((int)floor(vNext)) >= c1) continue;
         if ((++c2) <= 3) continue;
         vNext += vStep;
         if (iMode != 2)
         {
            if (Close > vL) vL = (i0 == 3 ? vL : vL + m_TickSize); else vH = (i0 == 3 ? vH : vH - m_TickSize);
         }else
         {
            vL = (((c2 & 1) == 1) ? (Close > vL ? vL + m_TickSize : vL) : (Close < vH ? vL : vL + m_TickSize));
            vH = (((c2 & 1) == 1) ? (Close > vL ? vH : vH - m_TickSize) : (Close < vH ? vH - m_TickSize : vH));
         }
      }
                                
      return Out;
   }

Las modificaciones introducidas buscan simplificar la estructura del código, manteniendo, sin embargo, su funcionamiento intacto. Un punto de interés aquí es la manera como se lee el valor anterior para generar el nuevo valor, adaptándose al tipo de trazado en uso, independiente de cuál sea. Esta flexibilidad es crucial para la funcionalidad del simulador. Para la definición de los valores, recurrimos a una función ya conocida e introducida en este artículo, lo que facilita así el desarrollo del proceso. Como mencioné, no detallaré el funcionamiento específico de este procedimiento, ya que ha sido abordado en otra publicación.

Ahora vamos a explorar cómo se estructuró el procedimiento final. Esta es nuestra primera tentativa de concluir esta etapa de la implementación, al tiempo que probamos si la función designada para generar las llamadas de la simulación, basada en los datos de las barras, alcanza el objetivo esperado. El objetivo es abarcar tanto la simulación de trazado de BID como LAST de forma efectiva. La descripción detallada del código de la función se presenta a continuación:

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      int     i0, i1;
      bool    b0 = ((rand() & 1) == 1);
                                
      m_Marks.iMax = (int) rate.tick_volume - 1;
      m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high);
      m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low);
      Simulation_Time(rate, tick);
      MountPrice(0, rate.open, rate.spread, tick);
      if (CheckViability(rate))
      {
         i0 = (int)(MathMin(m_Marks.iMax / 3.0, m_Marks.iMax * 0.2));
         i1 = m_Marks.iMax - i0;
         i0 = RandomWalk(1, i0, rate.open, (b0 ? rate.high : rate.low), rate.high, rate.low, rate.spread, tick, 0);
         RandomWalk(i0, i1, (m_IsPriceBID ? tick[i0].bid : tick[i0].last), (b0 ? rate.low : rate.high), rate.high, rate.low, rate.spread, tick, 1);
         RandomWalk(i1, m_Marks.iMax, (m_IsPriceBID ? tick[i1].bid : tick[i1].last), rate.close, rate.high, rate.low, rate.spread, tick, 2);
	 m_Marks.bLow = m_Marks.bHigh = true;
      }else Random_Price(rate, tick);
      if (!m_IsPriceBID) DistributeVolumeReal(rate, tick);
      if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick);
      if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick);
      MountPrice(m_Marks.iMax, rate.close, rate.spread, tick);
      CorretTime(tick);

      return m_Marks.iMax;
   }

El segmento enfatizado es crucial para establecer el patrón de simulación del paseo aleatorio en la barra. Aunque este proceso ya se utilizaba anteriormente, estaba integrado directamente en el código de generación. Ahora, ha sido reubicado a una posición que facilita la comprensión y análisis, haciéndose accesible incluso para programadores principiantes. Si analizamos con atención, nos daremos cuenta de que el sistema de simulación evalúa la viabilidad del uso del paseo aleatorio. Si es viable, el sistema lo empleará; de lo contrario, recurrirá a un método alternativo. De esta manera, garantizamos la generación de movimientos o desplazamientos de precios en cualquier circunstancia. Esto vale tanto para mercados similares al forex como para aquellos similares al de acciones. Tal es indiferente. El propósito es siempre adaptarse para proporcionar la mejor simulación posible e, igualmente, cubrir todos los puntos de precio alcanzables, sin desviarse de lo que indican las barras.

Es importante ser consciente de que, en algunas situaciones, una barra específica puede no ser adecuada para la simulación vía paseo aleatorio, mientras que una subsiguiente inmediatamente podría utilizar este proceso. Esto puede resultar en movimientos de precios que varían entre armoniosos y suaves a otros más abruptos y significativos. Esta variación no es necesariamente un indicativo de fallo en el sistema de simulación o repetición, sino más bien una consecuencia de la necesidad de movimiento rápido de los precios en esa barra, lo que puede no haber sido acompañado por un volumen significativo de operaciones para una simulación más fluida a través del paseo aleatorio. Lo opuesto también es cierto: un volumen alto de operaciones puede permitir la aplicación del paseo aleatorio, lo que no significa que el precio se haya movido suavemente en la realidad. En algunos casos, el desplazamiento puede haber sido abrupto, pero la densidad de ticks negociados posibilitó la aplicación del paseo aleatorio en la simulación, lo que no refleja necesariamente las condiciones reales del mercado en esa barra específica.

Puede parecer que ya hemos alcanzado una solución ideal y maravillosa; que hemos cumplido nuestros objetivos. Pero la realidad es otra: aún no hemos llegado allí. A pesar de que el paseo aleatorio se utiliza ampliamente cuando el número de operaciones en una barra de 1 minuto es grande, no se aplica cuando ese número es un poco inferior al necesario. Y peor aún, usar un enfoque totalmente aleatorio para simular el movimiento de la barra cuando la distancia entre el precio máximo y mínimo está cerca del número de ticks, resulta en una simulación que parece extraña. En estos casos, es necesario reconsiderar un modelo mencionado en otro artículo de esta serie, "Desarrollo de un sistema de repetición - Simulación de mercado (Parte 11): Nacimiento del SIMULADOR (I)", donde propusimos un sistema que crea un pivote dentro de la barra.

La idea de implementar tal sistema parece no solo adecuada, sino también plausible. El objetivo es generar movimientos realistas y admisibles, y evitar lanzar valores de precio de manera completamente aleatoria. Por lo tanto, la cuestión central ya no es el tiempo, sino el valor indicado en el precio. Utilizar una rutina que genera valores sin ninguna lógica, especialmente en situaciones que un negociante experimentado identificaría alguna lógica en el movimiento del precio, es desmotivante. Sin embargo, existe una solución para esto. Ella busca integrar los enfoques desde la parte 11 de esta serie hasta el momento actual. Aunque esta solución puede no ser inmediatamente obvia para los principiantes en programación, es bastante clara para aquellos con más experiencia. Así, no crearemos una nueva rutina o procedimiento de simulación desde cero. Lo que haremos es alternar entre generar movimientos menos y más suaves, según lo determine el propio simulador. La decisión sobre la suavidad del movimiento se basará en solo cinco datos: precio de apertura, precio de cierre, precio máximo, precio mínimo y volumen de ticks. Estos son los únicos datos necesarios para esa elección. Por lo tanto, no presentaré aquí una solución final y definitiva. Mi objetivo es mostrar uno de los muchos caminos posibles para crear y simular movimientos dentro de la barra de 1 minuto.


Exploramos el uso del paseo aleatorio en diversos escenarios - optamos por el camino del menor esfuerzo

Como se mencionó anteriormente, es esencial buscar un método que incorpore algún tipo de lógica al sistema. La dependencia exclusiva en la aleatorización no produce resultados satisfactorios, incluso cuando se aplica dentro de una barra de 1 minuto y se usa en tiempos gráficos mayores, como 10 o 15 minutos. Idealmente, los movimientos deben ser graduales, para evitar transiciones abruptas de un extremo al otro inmediatamente. Así, el movimiento se dibuja progresivamente, con lo cual surge aleatoriedad, cuando, en realidad, resulta de cálculos matemáticos simples que crean una complejidad aparente. Este es uno de los fundamentos de los movimientos estocásticos.

Para promover un flujo más elaborado y suave, será necesario eliminar algunas funciones existentes y establecer reglas que dirijan el movimiento de manera controlada. Es importante destacar que no se debe intentar forzar el movimiento en una dirección específica, sino definir reglas para que MetaTrader 5 conduzca el proceso de la manera que considere más apropiada. Para ello, el primer paso es modificar el código del procedimiento de paseo aleatorio. El código revisado, con las modificaciones destacadas, se presenta a continuación:

inline int RandomWalk(int In, int Out, const double Open, const double Close, double High, double Low, const int Spread, MqlTick &tick[], int iMode, int iDesloc)
   {
      double vStep, vNext, price, vH = High, vL = Low;
      char i0 = 0;
                                
      vNext = vStep = (Out - In) / ((High - Low) / m_TickSize);
      for (int c0 = In, c1 = 0, c2 = 0; c0 <= Out; c0++, c1++)
      {
         price = (m_IsPriceBID ? tick[c0 - 1].bid : tick[c0 - 1].last) + (m_TickSize * ((rand() & 1) == 1 ? -1 : 1));
         price = (price > vH ? price - m_TickSize : (price < vL ? price + m_TickSize : price));                                  
         price = (m_IsPriceBID ? tick[c0 - 1].bid : tick[c0 - 1].last) + (m_TickSize * ((rand() & 1) == 1 ? -iDesloc : iDesloc));
         price = (price > vH ? vH : (price < vL ? vL : price));
         MountPrice(c0, price, (Spread + RandomLimit((int)(Spread | (m_Marks.iMax & 0xF)), 0)), tick);
         switch (iMode)
         {
            case 1:
               i0 |= (price == High ? 0x01 : 0);
               i0 |= (price == Low ? 0x02 : 0);
               vH = (i0 == 3 ? High : vH);
               vL = (i0 ==3 ? Low : vL);
               break;
            case 0:
               if (price == Close) return c0;
            default:
               break;
         }
         if (((int)floor(vNext)) >= c1) continue; else if ((++c2) <= 3) continue;
         vNext += vStep;
         vL = (iMode != 2 ? (Close > vL ? (i0 == 3 ? vL : vL + m_TickSize) : vL) : (((c2 & 1) == 1) ? (Close > vL ? vL + m_TickSize : vL) : (Close < vH ? vL : vL + m_TickSize)));
         vH = (iMode != 2 ? (Close > vL ? vH : (i0 == 3 ? vH : vH - m_TickSize)) : (((c2 & 1) == 1) ? (Close > vL ? vH : vH - m_TickSize) : (Close < vH ? vH - m_TickSize : vH)));
         if (iMode == 2)
         {
            vL = (((c2 & 1) == 1) ? (Close > vL ? vL + m_TickSize : vL) : (Close < vH ? vL : vL + m_TickSize));
            vH = (((c2 & 1) == 1) ? (Close > vL ? vH : vH - m_TickSize) : (Close < vH ? vH - m_TickSize : vH));
         }else
         {
            if (Close > vL) vL = (i0 == 3 ? vL : vL + m_TickSize); else vH = (i0 == 3 ? vH : vH - m_TickSize);
         }                                       
      }
                                
      return Out;
   }

Las modificaciones incluyen la sustitución de segmentos del código por nuevos fragmentos en verde. Aunque los cambios puedan parecer sutiles, permiten una flexibilidad mucho mayor que la versión anterior. Antes, el movimiento era continuo, de tick a tick, sin espacios ("GAPs") entre ellos, lo cual requería un volumen de operaciones elevado para una simulación de paseo aleatorio suave. La introducción de GAPs dentro de una barra de 1 minuto reduce significativamente el número de operaciones necesarias, lo que permite pruebas del sistema con variados volúmenes y parámetros de barras de 1 minuto. Esto resulta en una adaptación gráfica del movimiento generado por el paseo aleatorio, al tiempo que se alcanzan los cuatro valores principales: apertura, cierre, máxima y mínima. El comportamiento intermedio está determinado por el paseo aleatorio. Sin embargo, un aspecto crucial reside en la función que invoca el paseo aleatorio. Esta función se detalla a continuación:

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      int     i0, i1, i2;
      bool    b0;
                                
      m_Marks.iMax = (int) rate.tick_volume - 1;
      m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high);
      m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low);
      Simulation_Time(rate, tick);
      MountPrice(0, rate.open, rate.spread, tick);
      if (CheckViability(rate))
      if (m_Marks.iMax > 10)
      {
         i0 = (int)(MathMin(m_Marks.iMax / 3.0, m_Marks.iMax * 0.2));
         i1 = m_Marks.iMax - i0;
         i2 = (int)(((rate.high - rate.low) / m_TickSize) / i0);
         i2 = (i2 == 0 ? 1 : i2);
         b0 = (m_Marks.iMax >= 1000 ? ((rand() & 1) == 1) : (rate.high - rate.open) < (rate.open - rate.low));
         i0 = RandomWalk(1, i0, rate.open, (b0 ? rate.high : rate.low), rate.high, rate.low, rate.spread, tick, 0, i2);
         RandomWalk(i0, i1, (m_IsPriceBID ? tick[i0].bid : tick[i0].last), (b0 ? rate.low : rate.high), rate.high, rate.low, rate.spread, tick, 1, i2);
         RandomWalk(i1, m_Marks.iMax, (m_IsPriceBID ? tick[i1].bid : tick[i1].last), rate.close, rate.high, rate.low, rate.spread, tick, 2, i2);
         m_Marks.bHigh = m_Marks.bLow = true;
      }else Random_Price(rate, tick);
      if (!m_IsPriceBID) DistributeVolumeReal(rate, tick);
      if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick);
      if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick);
      MountPrice(m_Marks.iMax, rate.close, rate.spread, tick);
      CorretTime(tick);

      return m_Marks.iMax;
   }

Esta función muestra que ahora ya no comprobaremos la viabilidad del paseo aleatorio de la forma en que lo hemos venido haciendo a lo largo de este artículo. La evaluación ahora ocurre íntegramente dentro de esta función. De manera sorprendente, el sistema intentará ejecutar un paseo aleatorio con un volumen mínimo de solo 10 operaciones. Para volúmenes iguales o inferiores a este límite, se adoptará una aleatorización pura, considerada más eficiente en este contexto específico que el paseo aleatorio. Un aspecto innovador introducido es la creación de "gaps" dentro de la barra de 1 minuto, asegurados por cálculos específicos mencionados. Es importante, sin embargo, asegurar la generación de al menos 1 tick para que el paseo aleatorio funcione adecuadamente.

Sin embargo, el proceso no está exento de desafíos. Un control adicional es necesario para la efectividad del paseo aleatorio. Este control adicional se delinea mediante una prueba específica, cuyo valor puede ajustarse según la necesidad. Si el volumen de operaciones excede 1000 en 1 minuto, el sistema de simulación puede optar por un recorrido, decidiendo aleatoriamente si se dirige primero al máximo o al mínimo. Por otro lado, si el volumen es menor que lo estipulado, la dirección inicial del paseo aleatorio se determinará, basándose en la proximidad del precio de apertura con los valores de máxima o mínima de la barra.

Este método, conocido como "camino del menor esfuerzo", es efectivo para situaciones en que la cantidad de movimientos necesarios es menor que la distancia total a recorrer. Esto evita elecciones que resultarían en recorridos más largos y complicados innecesariamente. Debido a este enfoque calculado, algunas de las discusiones y métodos propuestos en este artículo pueden no estar presentes en el anexo final. Para ilustrar el desempeño del sistema, se presentan dos figuras: una con el gráfico basado en datos reales de los ticks y otra que muestra el resultado de la simulación utilizando la estrategia del menor esfuerzo.

Figura 02

Figura 02 - Gráfico generado a partir de datos reales


Figura 03

Figura 03 - Gráfico producido utilizando los datos simulados por el sistema

Aunque a primera vista los gráficos puedan parecer idénticos, no lo son. Una observación más detenida revelará diferencias, como la fuente de datos indicada en cada figura, para destacar la distinción entre los dos. Esta comparación invita a los usuarios a reproducir el experimento utilizando los datos proporcionados en el anexo, específicamente para el activo EURUSD, un par de divisas del mercado FOREX. Esta demostración evidencia la capacidad del método de simulación de adaptarse tanto para trazados LAST como BID, para ofrecer la posibilidad de probar la eficacia del sistema con los datos disponibles.


Conclusión

Este artículo marca un paso crucial en la preparación para hacer el sistema de repetición/simulador plenamente funcional. En el siguiente artículo, abordaremos los ajustes finales necesarios antes de proceder con detalles adicionales del servicio de repetición/simulación. Esta etapa es esencial para aquellos que buscan comprender la operatividad y la eficiencia del sistema en condiciones de prueba.

Es importante resaltar sobre los archivos adjuntos: Debido al volumen de datos, especialmente los ticks reales para activos futuros, se proporcionarán cuatro archivos, cada uno asociado a un activo o mercado específico. El archivo principal incluye el código fuente del sistema hasta el estado actual de desarrollo. Para garantizar la integridad de la estructura y la funcionalidad del sistema, todos los archivos deben ser descargados y almacenados en el directorio especificado por el editor MQL5. Así, el sistema podrá ser utilizado con los archivos de demostración proporcionados, replicando los procedimientos mostrados. Este proceso sigue la metodología aplicada en los tutoriales en video presentados.

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

Archivos adjuntos |
Files_-_BOLSA.zip (1358.24 KB)
Files_-_FOREX.zip (3743.96 KB)
Files_-_FUTUROS.zip (11397.51 KB)
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 25): Preparación para la próxima etapa Desarrollo de un sistema de repetición — Simulación de mercado (Parte 25): Preparación para la próxima etapa
En este artículo, concluimos la primera fase del desarrollo del sistema de repetición y simulador. Con este hito, afirmo, estimado lector, que el sistema ha alcanzado un nivel avanzado, abriendo camino para la incorporación de nuevas funcionalidades. El objetivo es enriquecer aún más el sistema, convirtiéndolo en una herramienta poderosa para estudios y para el desarrollo de análisis de mercado.
Teoría de categorías en MQL5 (Parte 18): Cuadrado de la naturalidad Teoría de categorías en MQL5 (Parte 18): Cuadrado de la naturalidad
El artículo continúa la serie sobre teoría de categorías, presentando transformaciones naturales que suponen un elemento clave de la teoría. Hoy echaremos un vistazo a su definición (aparentemente compleja) y luego profundizaremos en los ejemplos y métodos de aplicación de las transformaciones para pronosticar la volatilidad.
Redes neuronales: así de sencillo (Parte 56): Utilizamos la norma nuclear para incentivar la exploración Redes neuronales: así de sencillo (Parte 56): Utilizamos la norma nuclear para incentivar la exploración
La exploración del entorno en tareas de aprendizaje por refuerzo es un problema relevante. Con anterioridad, ya hemos analizado algunos de estos enfoques. Hoy le propongo introducir otro método basado en la maximización de la norma nuclear, que permite a los agentes identificar estados del entorno con un alto grado de novedad y diversidad.
Redes neuronales: así de sencillo (Parte 55): Control interno contrastado (CIC) Redes neuronales: así de sencillo (Parte 55): Control interno contrastado (CIC)
El aprendizaje contrastivo (Contrastive learning) supone un método de aprendizaje de representación no supervisado. Su objetivo consiste en entrenar un modelo para que destaque las similitudes y diferencias entre los conjuntos de datos. En este artículo, hablaremos del uso de enfoques de aprendizaje contrastivo para investigar las distintas habilidades del Actor.