Desarrollo de un sistema de repetición (Parte 66): Presionando play en el servicio (VII)
Introducción
En este artículo comenzaremos haciendo algo un poco diferente. Pero antes, recordemos lo que se abordó en el artículo anterior, "Desarrollo de un sistema de repetición (Parte 65): Presionando play el servicio (VI). Allí resolvimos el problema de la indicación porcentual que nos muestra el indicador del mouse, que durante el uso de este mismo indicador en el sistema de repetición o simulación nos proporcionaba un valor incorrecto. Además, implementamos un sistema que nos permite avanzar rápidamente hasta un punto determinado y evitar esperar varios minutos o incluso horas. Sin embargo, me gustaría que vieras el video siguiente antes de seguir leyendo este artículo. Esto te ayudará a entender lo que explicaré al principio del artículo.
Algo que ver...
Imagino que el video habla por sí solo. Sin embargo, me gustaría explicar lo que está sucediendo. Esta aplicación de repetición/simulador está diseñada para utilizar un símbolo personalizado de manera masiva. Esto es un hecho con el que creo que todos estarán de acuerdo. Sin embargo, existen fallos que no están causados por la programación que estamos realizando. Dichos fallos tienen otras causas. Muchas de ellas son extrañas y otras ocurren de manera casi aleatoria. Si se puede analizar la causa del fallo, se puede solucionar. No obstante, cuando el fallo es aleatorio, la situación cambia y nos enfrentamos a un problema con el que debemos aprender a convivir.
En el momento de escribir este artículo, la versión más reciente de la plataforma MetaTrader 5 es precisamente la que se muestra en el video. Desconozco dónde se encuentra el problema, pero existe y es muy probable que ya te hayas enfrentado a él o lo hagas en el futuro. Por lo tanto, hasta que se solucione el fallo mostrado en el video, aprende a convivir con él. Es decir, antes de cargar la aplicación que realizará la repetición o simulación, configúrala como la usarás durante todo el proceso. Así evitarás tener que cambiar el marco temporal, ya que es precisamente en ese momento cuando, de forma extraña, MetaTrader 5 pierde el contacto con los ticks o barras del símbolo personalizado. Tal vez, cuando leas este artículo, el fallo mostrado en el video ya haya sido resuelto. Si es así, perfecto. Si no, sigue la recomendación que acabo de dar para evitar una experiencia desagradable al utilizar la aplicación de repetición/simulador.
Dichas estas palabras, podemos continuar con lo que haremos a partir de este punto en el artículo. Muy bien, en el artículo anterior quedó pendiente un tema. Dado que es más inmediato, comenzaremos con él.
Implementamos el tiempo restante para la próxima barra
Algo que muchos operadores del mercado financiero valoran y suelen observar es el tiempo que resta hasta que comienza la próxima barra. Tal vez esto parezca trivial o una pérdida de tiempo, pero en muchos casos esta información es importante, ya que el operador suele posicionar la orden pocos segundos antes de que se inicie la nueva barra.
Muy bien, en ciertos marcos temporales o gracias a la experiencia del operador, es posible hacerse una idea aproximada del tiempo restante. Sin embargo, los operadores principiantes aún no cuentan con esta habilidad. Por lo tanto, es necesario proporcionarles algún tipo de indicador. En el caso del mercado en vivo, hacer esto es relativamente sencillo, ya que siempre estamos conectados al servidor de trading, ya sea en una cuenta demo o real. Además, el hecho de que las operaciones estén en constante evolución facilita aún más la implementación de esta indicación, ya que el tiempo no se detiene. Pero precisamente este último aspecto complica un poco las cosas en la aplicación de repetición/simulación. El hecho de que el usuario pueda pausar la aplicación por un tiempo indefinido o incluso avanzar hasta una posición en la que la barra esté al inicio o cerca de su final complica mucho las cosas. Esto llega a tal punto que se requiere un verdadero malabarismo para evitar crear un Frankenstein solo para informar el tiempo restante de la barra.
Antes de hacer nada, veamos qué tenemos disponible. En el artículo anterior, modificamos el código del indicador del mouse para empezar a utilizar una versión diferente de la función que gestiona el evento OnCalculate. Este cambio, en cierto modo, nos será bastante útil en este caso. La razón es que tenemos un array que recibe el valor del tiempo. Con un pequeño ajuste, obtenemos el fragmento que se muestra a continuación.44. //+------------------------------------------------------------------+ 45. int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], 46. const double& high[], const double& low[], const double& close[], const long& tick_volume[], 47. const long& volume[], const int& spread[]) 48. { 49. Print(TimeToString(time[rates_total - 1], TIME_DATE | TIME_SECONDS)); // To Testing ... 50. GL_PriceClose = close[rates_total - 1]; 51. m_posBuff = rates_total; 52. (*Study).Update(m_Status); 53. 54. return rates_total; 55. } 56. //+------------------------------------------------------------------+
Fragmento del código fuente: Mouse Study.mq5
Se ha añadido una nueva línea. La línea 49 nos permitirá visualizar el último valor del array de tiempo. Ahora presta mucha atención a algo importante. Este valor en el array es el que se ha ajustado para que la barra de un minuto se construya correctamente, es decir, dentro de la ventana de un minuto.
Si ejecutas la aplicación de repetición/simulador con este nuevo código presente en el indicador del mouse, verás en la caja de herramientas una información muy parecida a la que se muestra en la siguiente animación.
Muy bien, puedes ver que el indicador del mouse imprime información cada vez que se llama a la función OnCalculate. Sin embargo, el valor en segundos no se está modificando. Entonces, podrías pensar de manera astuta: "Bien, ¿y si añadimos un valor en segundos a esta información?. Entonces, ¿podríamos calcular cuánto tiempo falta para que aparezca la próxima barra?" Si has pensado esto, significa que has comprendido lo que realmente necesitamos hacer para crear la indicación. Probemos esta idea. Para ello, realizaremos un pequeño ajuste en el código del archivo de cabecera C_Replay.mqh. Este cambio se muestra en el fragmento siguiente.
69. //+------------------------------------------------------------------+ 70. inline void CreateBarInReplay(bool bViewTick) 71. { 72. bool bNew; 73. double dSpread; 74. int iRand = rand(); 75. 76. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 77. { 78. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 79. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 80. { 81. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 82. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 83. { 84. m_Infos.tick[0].ask = m_Infos.tick[0].last; 85. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 86. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 87. { 88. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 89. m_Infos.tick[0].bid = m_Infos.tick[0].last; 90. } 91. } 92. if (bViewTick) CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 93. m_Infos.Rate[0].time = m_MemoryData.Info[m_Infos.CountReplay].time; //< To Testing... 94. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 95. } 96. m_Infos.CountReplay++; 97. } 98. //+------------------------------------------------------------------+
Fragmento del código fuente: C_Replay.mqh
Es interesante y sencillo de implementar. Bastó con añadir la línea 93 al fragmento de código para poder agregar el valor de los segundos. Más simple que esto, imposible. Pero, ¿realmente funcionará? Recuerda lo siguiente: la función CustomRatesUpdate especifica en su documentación que el valor de tiempo en el que se deben crear los Rates debe estar dentro de la ventana de un minuto. Podrías pensar entonces que esto no es un problema, ya que no estamos cambiando la ventana, solo estamos añadiendo el valor de los segundos, que muy probablemente será ignorado por la función, pero se transmitirá al indicador del mouse para que se detecte en la función OnCalculate. En cierto modo, debo estar de acuerdo contigo. Sin embargo, hasta que probemos la hipótesis planteada, no será más que eso: una hipótesis teórica. Entonces, compilamos el código y el resultado se muestra en el siguiente video.
Una demostración rápida
Pero, ¿qué fue eso? ¡Qué cosa tan loca acaba de suceder! La verdad es que no esperaba que ocurriera. Pensaba que funcionaría. En realidad, la idea no está completamente equivocada; habrás notado que el indicador del mouse mostró lo que deseábamos. Sin embargo, el contenido del gráfico... Bueno, será mejor buscar otra forma de hacerlo. Pero seguramente habrás observado que sí, podemos hacer algunas cosas bastante interesantes. De hecho, utilicé esta misma idea para crear un indicador en el pasado. Puedes ver ese indicador, al menos en su versión abierta, en el artículo "Desarrollando un EA de trading desde cero (Parte 13): Times And Trade (II). Esa versión ya está obsoleta, pero sirve para que entiendas lo que ocurrió aquí. Y antes de que alguien pregunte: NO No voy a vender, regalar, prestar ni mostrar la versión más actual de dicho indicador. Es de uso personal.
Muy bien. Creo que ya has entendido cómo funciona realmente el proceso y por qué algo aparentemente sencillo requiere una manipulación más elaborada para obtener los resultados deseados.
Es posible que pienses que tendremos que idear una nueva forma de hacer las cosas utilizando algún tipo de manipulación de datos. Sin embargo, la solución ya existía en la versión que utilizaba variables globales del terminal. Ahora bien, como ya no utilizaremos esa misma solución, necesitamos ser creativos y entender realmente cómo los programas intercambian información entre sí. De cierta forma, ya estamos haciendo esto al usar el indicador de control para gestionar el servicio, mientras que el servicio informa al indicador sobre la posición en la que nos encontramos. Lo mismo deberá aplicarse aquí, pero con un detalle: no podemos generar eventos personalizados cada segundo, o peor aún, con cada tick, para informar al indicador del mouse sobre el tiempo actual. Si lo hiciéramos, degradaríamos en gran medida el rendimiento de la aplicación y nos veríamos obligados a reducir aún más el número máximo de ticks por minuto. Este tipo de situación quiero evitarla a toda costa. Sin embargo, necesitamos algún tipo de sincronización entre la información contenida en los ticks y los datos relativos al tiempo de la barra actual. Esta es, efectivamente, la cuestión principal. Por lo tanto, desharemos las modificaciones mostradas en los fragmentos anteriores y nos centraremos en intentar desarrollar otra solución para abordar el problema. Este tema se tratará en un próximo apartado.
Pensamos en otra forma de informar el tiempo restante
A diferencia de lo que sucede cuando estamos conectados a un servidor de trading real, en el sistema de repetición/simulación la cuestión es mucho más complicada de resolver. Al menos, de la manera en que quiero abordarlo, ya que no quiero utilizar una variable global del terminal para transferir información. Cuando estamos conectados a un servidor, todos los relojes tienden y necesitan estar sincronizados, al menos en términos de segundos. Tal vez no comprendas cómo esto afecta a las cosas, pero para verificar el tiempo restante en segundos, basta con calcular cuántos segundos faltan para un momento específico. En el caso de estar conectados a un servidor real, esto es muy simple porque podemos usar el reloj interno del sistema para realizar ese cálculo y obtener el tiempo restante.
Sin embargo, cuando utilizamos el sistema de repetición/simulación, todo se vuelve mucho más complicado. Este incremento en la complejidad se debe precisamente al hecho de que estamos ejecutando una repetición o simulación. Tal vez te estés preguntando: ¿cómo puede el simple hecho de hacer un replay o simulación complicar tanto la cuestión del tiempo restante? Esto no tiene sentido. De hecho, si lo piensas superficialmente, no parece lógico que ejecutar una repetición o simulación complique tanto saber cuánto tiempo falta para la apertura de una nueva barra. Pero el problema radica precisamente en el manejo del reloj.
Pensemos en lo siguiente: supongamos que la primera barra de un minuto que se traza en el gráfico tarda de hecho un minuto completo en trazarse. Muy bien. Esta es la primera condición para entender el problema: la barra se trazará en un minuto. Ahora, si iniciamos la aplicación para que la graficación comience exactamente en el segundo cero, cada minuto del reloj del sistema tendrá una nueva barra. Perfecto, problema resuelto. Esto se debe a que la sincronización sería perfecta. Así, todo lo que tendríamos que hacer sería mirar el reloj del sistema para saber cuánto falta para que comience la próxima barra.
Sin embargo, rara vez, por no decir nunca, la primera barra durará exactamente un minuto. Otro problema es que difícilmente lograrás que la aplicación comience a trazar la barra en el instante cero según el reloj del sistema. Pero en este caso podríamos hacer un pequeño ajuste o, mejor dicho, implementar algún tipo de prueba que haga arrancar la repetición/simulador para generar la sincronización con el reloj del sistema. Este tipo de solución es viable y factible. Sin embargo, si configuraras la aplicación para que esté lista y permita trazar la barra, tendrías que esperar a que se produzca la sincronización. Este tiempo no sería muy largo, como máximo 59 segundos, en el peor de los casos. Este enfoque es aceptable cuando la aplicación es para uso personal. Pero incluso para uso personal, tarde o temprano terminarías cansándote de esperar hasta 59 segundos cada vez que abras la aplicación.
Debo reconocer, y seguramente tú también, que aunque obligar a la aplicación a mantenerse sincronizada con el reloj del sistema simplificaría nuestra vida en lo que respecta a saber el tiempo restante de la barra, también haría que su uso fuera algo irritante. Y la peor situación se presenta cuando hacemos una pausa en medio de la simulación o repetición. En tal caso, tendríamos que esperar 59 segundos para que la aplicación pueda volver a trazar el gráfico, ya que el reloj del sistema tendría que llegar al punto exacto. Desde un punto de vista práctico, esta solución no parece muy adecuada.
Sin embargo, podríamos encontrar un punto intermedio. Algo que nos permita hacer ciertos ajustes para mantener la sincronización con el reloj del sistema sin necesidad de esperar esos 59 segundos para que la aplicación que traza las barras se habilite y se determine así el tiempo restante de la barra. Ahora esto empieza a parecer más interesante.
Hago esta descripción no para complicar las cosas ni para confundirte, estimado lector, sino para mostrarte que, antes de ponernos a codificar algo, primero debemos reflexionar. Hay que analizar las posibilidades y los costes en términos de la dificultad de implementación. Muchas personas creen que programar consiste simplemente en escribir un montón de código en un archivo y que así resolverán todo. Sin embargo, la codificación en sí misma es solo una pequeña parte del trabajo. La mayor parte del esfuerzo consiste realmente en planificar y analizar la solución que debemos implementar. Y eso es exactamente lo que hemos hecho hasta ahora. Con este enfoque, hemos logrado hacernos una buena idea de lo que necesitamos hacer. Sin embargo, será necesario realizar algunos cambios. O, mejor dicho, necesitaremos mejorar nuestra organización. Pero para entender realmente lo que haremos, avancemos hacia un nuevo apartado.
Implementamos la versión base para informar el tiempo restante
En este apartado, haremos algo que, en mi opinión, es relativamente simple pero bastante atrevido. Implementaremos un mecanismo para transferir rápidamente la información del tiempo actual de la barra al indicador del mouse. Comenzaremos realizando algunos cambios en el código del indicador de mouse. Estos cambios consistirán en agregar un poco más de código, como se muestra en el fragmento siguiente.
12. //+------------------------------------------------------------------+ 13. double GL_PriceClose; 14. datetime GL_TimeAdjust; 15. //+------------------------------------------------------------------+ 16. #include <Market Replay\Auxiliar\Study\C_Study.mqh> 17. //+------------------------------------------------------------------+ 18. C_Study *Study = NULL; 19. //+------------------------------------------------------------------+ 20. input color user02 = clrBlack; //Price Line 21. input color user03 = clrPaleGreen; //Positive Study 22. input color user04 = clrLightCoral; //Negative Study 23. //+------------------------------------------------------------------+ 24. C_Study::eStatusMarket m_Status; 25. int m_posBuff = 0; 26. double m_Buff[]; 27. //+------------------------------------------------------------------+ 28. int OnInit() 29. { 30. ResetLastError(); 31. Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04); 32. if (_LastError != ERR_SUCCESS) return INIT_FAILED; 33. if ((*Study).GetInfoTerminal().szSymbol != def_SymbolReplay) 34. { 35. MarketBookAdd((*Study).GetInfoTerminal().szSymbol); 36. OnBookEvent((*Study).GetInfoTerminal().szSymbol); 37. m_Status = C_Study::eCloseMarket; 38. }else 39. m_Status = C_Study::eInReplay; 40. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 41. ArrayInitialize(m_Buff, EMPTY_VALUE); 42. 43. return INIT_SUCCEEDED; 44. } 45. //+------------------------------------------------------------------+ 46. int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], 47. const double& high[], const double& low[], const double& close[], const long& tick_volume[], 48. const long& volume[], const int& spread[]) 49. { 50. GL_PriceClose = close[rates_total - 1]; 51. GL_TimeAdjust = (spread[rates_total - 1] < 60 ? spread[rates_total - 1] : 0); 52. m_posBuff = rates_total; 53. (*Study).Update(m_Status); 54. 55. return rates_total; 56. } 57. //+------------------------------------------------------------------+
Fragmento del código fuente: Mouse Study.mq5
Si comparas este fragmento mostrado con el último código fuente del indicador de mouse, verás que se ha añadido la línea 14. Es decir, ahora tenemos una nueva variable global dentro del indicador. No tengo intención de añadir más variables globales a este módulo del indicador de mouse. Sin embargo, esta variable es especial por un motivo que entenderás pronto. En cualquier caso, esta variable recibirá un valor en la línea 51. Este valor se comunicará a través del spread. Aquí reside un peligro que, al mismo tiempo, coquetea con algo que puede resultar útil. En cualquier caso, esta variable global solo se utiliza de manera muy específica. Para entenderlo mejor, observa el nuevo código del archivo C_Study.mqh que se muestra a continuación.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "..\C_Mouse.mqh" 005. //+------------------------------------------------------------------+ 006. #define def_ExpansionPrefix def_MousePrefixName + "Expansion_" 007. //+------------------------------------------------------------------+ 008. class C_Study : public C_Mouse 009. { 010. private : 011. //+------------------------------------------------------------------+ 012. struct st00 013. { 014. eStatusMarket Status; 015. MqlRates Rate; 016. string szInfo, 017. szBtn1, 018. szBtn2, 019. szBtn3; 020. color corP, 021. corN; 022. int HeightText; 023. bool bvT, bvD, bvP; 024. datetime TimeDevice; 025. }m_Info; 026. //+------------------------------------------------------------------+ 027. void Draw(void) 028. { 029. double v1; 030. 031. if (m_Info.bvT) 032. { 033. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 18); 034. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_TEXT, m_Info.szInfo); 035. } 036. if (m_Info.bvD) 037. { 038. v1 = NormalizeDouble((((GetInfoMouse().Position.Price - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2); 039. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1); 040. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP)); 041. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); 042. } 043. if (m_Info.bvP) 044. { 045. v1 = NormalizeDouble((((GL_PriceClose - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2); 046. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1); 047. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP)); 048. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); 049. } 050. } 051. //+------------------------------------------------------------------+ 052. inline void CreateObjInfo(EnumEvents arg) 053. { 054. switch (arg) 055. { 056. case evShowBarTime: 057. C_Mouse::CreateObjToStudy(2, 110, m_Info.szBtn1 = (def_ExpansionPrefix + (string)ObjectsTotal(0)), clrPaleTurquoise); 058. m_Info.bvT = true; 059. break; 060. case evShowDailyVar: 061. C_Mouse::CreateObjToStudy(2, 53, m_Info.szBtn2 = (def_ExpansionPrefix + (string)ObjectsTotal(0))); 062. m_Info.bvD = true; 063. break; 064. case evShowPriceVar: 065. C_Mouse::CreateObjToStudy(58, 53, m_Info.szBtn3 = (def_ExpansionPrefix + (string)ObjectsTotal(0))); 066. m_Info.bvP = true; 067. break; 068. } 069. } 070. //+------------------------------------------------------------------+ 071. inline void RemoveObjInfo(EnumEvents arg) 072. { 073. string sz; 074. 075. switch (arg) 076. { 077. case evHideBarTime: 078. sz = m_Info.szBtn1; 079. m_Info.bvT = false; 080. break; 081. case evHideDailyVar: 082. sz = m_Info.szBtn2; 083. m_Info.bvD = false; 084. break; 085. case evHidePriceVar: 086. sz = m_Info.szBtn3; 087. m_Info.bvP = false; 088. break; 089. } 090. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); 091. ObjectDelete(GetInfoTerminal().ID, sz); 092. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); 093. } 094. //+------------------------------------------------------------------+ 095. public : 096. //+------------------------------------------------------------------+ 097. C_Study(long IdParam, string szShortName, color corH, color corP, color corN) 098. :C_Mouse(IdParam, szShortName, corH, corP, corN) 099. { 100. if (_LastError != ERR_SUCCESS) return; 101. ZeroMemory(m_Info); 102. m_Info.Status = eCloseMarket; 103. m_Info.Rate.close = iClose(GetInfoTerminal().szSymbol, PERIOD_D1, ((GetInfoTerminal().szSymbol == def_SymbolReplay) || (macroGetDate(TimeCurrent()) != macroGetDate(iTime(GetInfoTerminal().szSymbol, PERIOD_D1, 0))) ? 0 : 1)); 104. m_Info.corP = corP; 105. m_Info.corN = corN; 106. CreateObjInfo(evShowBarTime); 107. CreateObjInfo(evShowDailyVar); 108. CreateObjInfo(evShowPriceVar); 109. } 110. //+------------------------------------------------------------------+ 111. void Update(const eStatusMarket arg) 112. { 113. int i0; 114. datetime dt; 115. 116. switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) 117. { 118. case eCloseMarket : 119. m_Info.szInfo = "Closed Market"; 120. break; 121. case eInReplay : 122. case eInTrading : 123. i0 = PeriodSeconds(); 124. dt = (m_Info.Status == eInReplay ? m_Info.TimeDevice + GL_TimeAdjust : TimeCurrent()); 125. m_Info.Rate.time = (m_Info.Rate.time <= dt ? (datetime)(((ulong) dt / i0) * i0) + i0 : m_Info.Rate.time); 126. m_Info.szInfo = TimeToString((datetime)m_Info.Rate.time - dt, TIME_SECONDS); 127. break; 128. case eAuction : 129. m_Info.szInfo = "Auction"; 130. break; 131. default : 132. m_Info.szInfo = "ERROR"; 133. } 134. Draw(); 135. } 136. //+------------------------------------------------------------------+ 137. virtual void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 138. { 139. C_Mouse::DispatchMessage(id, lparam, dparam, sparam); 140. switch (id) 141. { 142. case CHARTEVENT_CUSTOM + evHideBarTime: 143. RemoveObjInfo(evHideBarTime); 144. break; 145. case CHARTEVENT_CUSTOM + evShowBarTime: 146. CreateObjInfo(evShowBarTime); 147. break; 148. case CHARTEVENT_CUSTOM + evHideDailyVar: 149. RemoveObjInfo(evHideDailyVar); 150. break; 151. case CHARTEVENT_CUSTOM + evShowDailyVar: 152. CreateObjInfo(evShowDailyVar); 153. break; 154. case CHARTEVENT_CUSTOM + evHidePriceVar: 155. RemoveObjInfo(evHidePriceVar); 156. break; 157. case CHARTEVENT_CUSTOM + evShowPriceVar: 158. CreateObjInfo(evShowPriceVar); 159. break; 160. case (CHARTEVENT_CUSTOM + evSetServerTime): 161. m_Info.TimeDevice = (datetime)lparam; 162. break; 163. case CHARTEVENT_MOUSE_MOVE: 164. Draw(); 165. break; 166. } 167. ChartRedraw(GetInfoTerminal().ID); 168. } 169. //+------------------------------------------------------------------+ 170. }; 171. //+------------------------------------------------------------------+ 172. #undef def_ExpansionPrefix 173. #undef def_MousePrefixName 174. //+------------------------------------------------------------------+
Archivo de código fuente: C_Study.mqh
Muy bien. Muy bien. Presta atención, porque lo que voy a explicarte a partir de ahora es crucial para entender cómo funciona todo. Verás que el código del archivo de cabecera ha cambiado. Sin embargo, solo hay dos aspectos realmente importantes. El primero está en la línea 160, donde tratamos un evento personalizado. Fíjate en que, en la línea 161, almacenamos el valor que el evento nos pasa en una variable privada de la clase. Este es el primer punto. Ahora vayamos a la parte donde realmente ocurre la "magia". Para ello, subimos unas líneas y nos dirigimos a la línea 111, donde se encuentra el procedimiento Update. Este procedimiento generará la información que visualizaremos después. Presta atención. En la línea 123, capturamos la cantidad de segundos existentes dentro del período de tiempo del gráfico del símbolo personalizado. En la línea 124 realizamos un pequeño cálculo o utilizamos el valor que MetaTrader 5 nos proporciona. La decisión de si realizar el cálculo o usar el valor depende del estado del indicador. Cuando usamos el indicador de mouse en un símbolo de repetición, realizamos el cálculo. De lo contrario, utilizamos el valor proporcionado por MetaTrader 5.
Observa que el cálculo tiene en cuenta el valor capturado en el indicador de mouse y otro valor adicional que proporcionará la aplicación de repetición/simulación. Más adelante mostraré cómo se transmite este segundo valor al indicador de posición del mouse.
En cualquier caso, se trata de un dato que cambiará con el tiempo. Sin embargo, necesitamos otro valor, que se calcula un poco más abajo, en la línea 125, donde determinamos el momento en que se lanzará una nueva barra en el gráfico. Finalmente, en la línea 126 realizamos un último cálculo para presentar al usuario el tiempo restante para el cierre de la barra.
Todo este sistema funcionará porque el servidor de trading es el encargado de indicarnos cuánto tiempo falta para que se cree una nueva barra. En el caso de utilizar el sistema de repetición/simulación, será el servicio que gestiona la actualización de las barras y permite que MetaTrader 5 las grafique.
Muy bien. Como habrás notado, necesitamos modificar el código del servicio debido a las explicaciones anteriores. Sin embargo, no es el código del servicio en sí lo que requerirá cambios, sino el código presente en el archivo C_Replay.mqh. Antes de hacer esto, sin embargo, es necesario realizar una pequeña modificación en dos puntos del código del servicio. El primer paso es añadir algunos elementos al archivo de cabecera Macros.mqh. A continuación se muestra cómo:
1. //+------------------------------------------------------------------+ 2. #property copyright "Daniel Jose" 3. //+------------------------------------------------------------------+ 4. #define macroRemoveSec(A) (A - (A % 60)) 5. #define macroGetDate(A) (A - (A % 86400)) 6. #define macroGetSec(A) (A - (A - (A % 60))) 7. //+------------------------------------------------------------------+
Archivo de código fuente: Macros.mqh
Dado que es algo muy simple de entender, no entraré en detalles. Una vez hecho esto, modificaremos el archivo de cabecera C_FilesTick.mqh. La modificación es puntual y puede verse en el fragmento siguiente:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #include "C_FileBars.mqh" 05. #include "C_Simulation.mqh" 06. #include "..\..\Auxiliar\Macros.mqh" 07. //+------------------------------------------------------------------+ 08. //#define macroRemoveSec(A) (A - (A % 60)) 09. #define def_MaxSizeArray 16777216 // 16 Mbytes 10. //+------------------------------------------------------------------+ 11. class C_FileTicks 12. { 13. protected: 14. enum ePlotType {PRICE_EXCHANGE, PRICE_FOREX};
Fragmento del código fuente: C_FilesTick.mqh
Observa que en este caso se añadió la línea 06 y la línea 08, que aparece destacada, deberá eliminarse del código. Esto se debe a que el archivo de cabecera Macros.mqh contendrá ahora el código que antes estaba en la línea destacada. Muy bien. Muy bien, una vez realizadas estas modificaciones, podemos proceder con el archivo C_Replay.mqh. El nuevo código de este archivo se presenta a continuación en su totalidad.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "C_ConfigService.mqh" 005. #include "C_Controls.mqh" 006. //+------------------------------------------------------------------+ 007. #define def_IndicatorControl "Indicators\\Market Replay.ex5" 008. #resource "\\" + def_IndicatorControl 009. //+------------------------------------------------------------------+ 010. #define def_CheckLoopService ((!_StopFlag) && (ChartSymbol(m_Infos.IdReplay) != "")) 011. //+------------------------------------------------------------------+ 012. #define def_ShortNameIndControl "Market Replay Control" 013. #define def_MaxSlider (def_MaxPosSlider + 1) 014. //+------------------------------------------------------------------+ 015. class C_Replay : public C_ConfigService 016. { 017. private : 018. struct st00 019. { 020. C_Controls::eObjectControl Mode; 021. uCast_Double Memory; 022. ushort Position; 023. int Handle; 024. }m_IndControl; 025. struct st01 026. { 027. long IdReplay; 028. int CountReplay; 029. double PointsPerTick; 030. MqlTick tick[1]; 031. MqlRates Rate[1]; 032. }m_Infos; 033. stInfoTicks m_MemoryData; 034. //+------------------------------------------------------------------+ 035. inline bool MsgError(string sz0) { Print(sz0); return false; } 036. //+------------------------------------------------------------------+ 037. inline void UpdateIndicatorControl(void) 038. { 039. double Buff[]; 040. 041. if (m_IndControl.Handle == INVALID_HANDLE) return; 042. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 043. { 044. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 045. m_IndControl.Memory.dValue = Buff[0]; 046. if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) 047. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 048. }else 049. { 050. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 051. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 052. m_IndControl.Memory._8b[7] = 'D'; 053. m_IndControl.Memory._8b[6] = 'M'; 054. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 055. } 056. } 057. //+------------------------------------------------------------------+ 058. void SweepAndCloseChart(void) 059. { 060. long id; 061. 062. if ((id = ChartFirst()) > 0) do 063. { 064. if (ChartSymbol(id) == def_SymbolReplay) 065. ChartClose(id); 066. }while ((id = ChartNext(id)) > 0); 067. } 068. //+------------------------------------------------------------------+ 069. inline void CreateBarInReplay(bool bViewTick) 070. { 071. bool bNew; 072. double dSpread; 073. int iRand = rand(); 074. 075. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 076. { 077. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 078. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 079. { 080. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 081. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 082. { 083. m_Infos.tick[0].ask = m_Infos.tick[0].last; 084. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 085. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 086. { 087. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 088. m_Infos.tick[0].bid = m_Infos.tick[0].last; 089. } 090. } 091. if (bViewTick) 092. { 093. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 094. if (bNew) EventChartCustom(m_Infos.IdReplay, evSetServerTime, (long)m_Infos.Rate[0].time, 0, ""); 095. } 096. m_Infos.Rate[0].spread = (int)macroGetSec(m_MemoryData.Info[m_Infos.CountReplay].time); 097. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 098. } 099. m_Infos.CountReplay++; 100. } 101. //+------------------------------------------------------------------+ 102. void AdjustViewDetails(void) 103. { 104. MqlRates rate[1]; 105. 106. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 107. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 108. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE); 109. m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); 110. CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate); 111. if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE)) 112. for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++); 113. if (rate[0].close > 0) 114. { 115. if (GetInfoTicks().ModePlot == PRICE_EXCHANGE) 116. m_Infos.tick[0].last = rate[0].close; 117. else 118. { 119. m_Infos.tick[0].bid = rate[0].close; 120. m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick); 121. } 122. m_Infos.tick[0].time = rate[0].time; 123. m_Infos.tick[0].time_msc = rate[0].time * 1000; 124. }else 125. m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay]; 126. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 127. } 128. //+------------------------------------------------------------------+ 129. void AdjustPositionToReplay(void) 130. { 131. int nPos, nCount; 132. 133. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return; 134. nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); 135. for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); 136. if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1); 137. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 138. CreateBarInReplay(false); 139. } 140. //+------------------------------------------------------------------+ 141. public : 142. //+------------------------------------------------------------------+ 143. C_Replay() 144. :C_ConfigService() 145. { 146. Print("************** Market Replay Service **************"); 147. srand(GetTickCount()); 148. SymbolSelect(def_SymbolReplay, false); 149. CustomSymbolDelete(def_SymbolReplay); 150. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 151. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0); 152. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0); 153. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0); 154. CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); 155. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8); 156. SymbolSelect(def_SymbolReplay, true); 157. m_Infos.CountReplay = 0; 158. m_IndControl.Handle = INVALID_HANDLE; 159. m_IndControl.Mode = C_Controls::ePause; 160. m_IndControl.Position = 0; 161. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; 162. } 163. //+------------------------------------------------------------------+ 164. ~C_Replay() 165. { 166. SweepAndCloseChart(); 167. IndicatorRelease(m_IndControl.Handle); 168. SymbolSelect(def_SymbolReplay, false); 169. CustomSymbolDelete(def_SymbolReplay); 170. Print("Finished replay service..."); 171. } 172. //+------------------------------------------------------------------+ 173. bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate) 174. { 175. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0) 176. return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket."); 177. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0) 178. return MsgError("Asset configuration is not complete, need to declare the ticket value."); 179. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0) 180. return MsgError("Asset configuration not complete, need to declare the minimum volume."); 181. SweepAndCloseChart(); 182. m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1); 183. if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl")) 184. Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl"); 185. else 186. Print("Apply template: ", szNameTemplate, ".tpl"); 187. 188. return true; 189. } 190. //+------------------------------------------------------------------+ 191. bool InitBaseControl(const ushort wait = 1000) 192. { 193. Print("Waiting for Mouse Indicator..."); 194. Sleep(wait); 195. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200); 196. if (def_CheckLoopService) 197. { 198. AdjustViewDetails(); 199. Print("Waiting for Control Indicator..."); 200. if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 201. ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle); 202. UpdateIndicatorControl(); 203. } 204. 205. return def_CheckLoopService; 206. } 207. //+------------------------------------------------------------------+ 208. bool LoopEventOnTime(void) 209. { 210. int iPos; 211. 212. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 213. { 214. UpdateIndicatorControl(); 215. Sleep(200); 216. } 217. m_MemoryData = GetInfoTicks(); 218. AdjustPositionToReplay(); 219. EventChartCustom(m_Infos.IdReplay, evSetServerTime, (long)macroRemoveSec(m_MemoryData.Info[m_Infos.CountReplay].time), 0, ""); 220. iPos = 0; 221. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 222. { 223. if (m_IndControl.Mode == C_Controls::ePause) return true; 224. iPos += (int)(m_Infos.CountReplay < (m_MemoryData.nTicks - 1) ? m_MemoryData.Info[m_Infos.CountReplay + 1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : 0); 225. CreateBarInReplay(true); 226. while ((iPos > 200) && (def_CheckLoopService)) 227. { 228. Sleep(195); 229. iPos -= 200; 230. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 231. UpdateIndicatorControl(); 232. } 233. } 234. 235. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 236. } 237. }; 238. //+------------------------------------------------------------------+ 239. #undef def_SymbolReplay 240. #undef def_CheckLoopService 241. #undef def_MaxSlider 242. //+------------------------------------------------------------------+
Archivo de código fuente: C_Replay.mqh
Las cuestiones aquí son un poco más complejas de lo que puedas imaginar en este momento. Esto se debe a que necesitamos informar directamente al indicador del mouse en qué punto de la simulación o repetición nos encontramos. En cierto sentido, esto no sería un problema, como recordarás. Al principio de este artículo, te mostré cómo podíamos hacerlo. Sin embargo, también te diste cuenta de que ocurría algo extraño y de que sería necesario usar un enfoque diferente.
Este enfoque se logró utilizando tres nuevas líneas. Pero no te engañes, esta solución no es perfecta. Solo resuelve nuestro problema desde un punto de vista. Sin embargo, hay otro aspecto que esta solución no puede abordar, al menos por ahora. Antes de entrar en detalles sobre este aspecto no cubierto, veamos qué son estas líneas y qué hacen.
Comencemos con la línea más simple, que se ejecuta de forma casi directa: la línea 96. Si has prestado atención al fragmento del código fuente del indicador de mouse, habrás notado que estamos utilizando el valor del spread para lograr un ajuste fino en términos de segundos. Dado que no podemos pasar el valor directamente a través de los Rates, lo transmitiremos de la forma más rápida posible. Esta es la solución que encontré, ya que durante todo el periodo de simulación o repetición no estamos utilizando el spread. Por lo tanto, lo utilizaremos de una manera más interesante. Hay un pequeño problema en este punto, pero aún no nos afecta. De momento, podemos convivir con él.
Continuando con la explicación sobre las nuevas líneas Si observas un poco más arriba, en la línea 94, verás que, cada vez que el servicio de repetición/simulación identifica una nueva barra, lanzamos un evento personalizado para informar al indicador de mouse del nuevo valor que debe utilizarse. De manera similar, en la línea 219 indicamos al indicador qué valor debe emplear. En ambos casos, esto se realiza mediante eventos personalizados.
Pero ¿por qué hacerlo así? ¿No habría otra forma de lograrlo? Sí, existe otra manera, pero no fue suficientemente adecuada. No para este momento, sino para algo que aún debemos resolver. El hecho es que estos eventos personalizados solo se lanzan para indicarnos que se ha cerrado la barra de un minuto. Podríamos hacerlo de otra manera, sin necesidad de lanzar un evento personalizado en el gráfico. Para ello, tendríamos que utilizar la función de la biblioteca iTime para saber cuándo se creó la barra de un minuto. Presta atención a este detalle: NO NOS IMPORTA EL TIEMPO DE LA BARRA EN EL TIMEFRAME USADO, SINO EL TIEMPO DE LA BARRA DE UN MINUTO. Ahora podrías estar confundido, pero observa el siguiente fragmento.
110. //+------------------------------------------------------------------+ 111. void Update(const eStatusMarket arg) 112. { 113. int i0; 114. datetime dt; 115. 116. switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) 117. { 118. case eCloseMarket : 119. m_Info.szInfo = "Closed Market"; 120. break; 121. case eInReplay : 122. case eInTrading : 123. i0 = PeriodSeconds(); 124. dt = (m_Info.Status == eInReplay ? iTime(NULL, 0, 0) + GL_TimeAdjust : TimeCurrent()); 125. m_Info.Rate.time = (m_Info.Rate.time <= dt ? (datetime)(((ulong) dt / i0) * i0) + i0 : m_Info.Rate.time); 126. m_Info.szInfo = TimeToString((datetime)m_Info.Rate.time - dt, TIME_SECONDS); 127. break; 128. case eAuction : 129. m_Info.szInfo = "Auction"; 130. break; 131. default : 132. m_Info.szInfo = "ERROR"; 133. } 134. Draw(); 135. } 136. //+------------------------------------------------------------------+
Fragmento mod del archivo: C_Study.mqh
El cambio se encuentra en la línea 124. Usar esta función para capturar el tiempo de la barra genera una pérdida de tiempo totalmente innecesaria, ya que el valor obtenido sería el mismo que el de la función OnCalculate. Sin embargo, si este mismo fragmento se modificara como se muestra a continuación, las cosas serían bastante diferentes.
110. //+------------------------------------------------------------------+ 111. void Update(const eStatusMarket arg) 112. { 113. int i0; 114. datetime dt; 115. 116. switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) 117. { 118. case eCloseMarket : 119. m_Info.szInfo = "Closed Market"; 120. break; 121. case eInReplay : 122. case eInTrading : 123. i0 = PeriodSeconds(); 124. dt = (m_Info.Status == eInReplay ? iTime(NULL, PERIOD_M1, 0) + GL_TimeAdjust : TimeCurrent()); 125. m_Info.Rate.time = (m_Info.Rate.time <= dt ? (datetime)(((ulong) dt / i0) * i0) + i0 : m_Info.Rate.time); 126. m_Info.szInfo = TimeToString((datetime)m_Info.Rate.time - dt, TIME_SECONDS); 127. break; 128. case eAuction : 129. m_Info.szInfo = "Auction"; 130. break; 131. default : 132. m_Info.szInfo = "ERROR"; 133. } 134. Draw(); 135. } 136. //+------------------------------------------------------------------+
Fragmento mod del archivo: C_Study.mqh
Nota que el cambio radica en que ahora le pedimos a MetaTrader 5 que nos informe cuándo comenzó la barra de un minuto. Y esto nos libra de tener que hacer que el servicio lance un evento personalizado para proporcionarnos esta información. Pero espera un momento, ¡no entiendo! El hecho, estimado lector, es que la información que MetaTrader 5 nos proporcionará es exactamente cuándo comenzó la barra de un minuto, independientemente del marco temporal. Y precisamente eso es lo que el evento personalizado informaría. Sin embargo, al realizar este procedimiento en el servicio, garantizo que la información solo se transmitirá de manera intermitente, en lugar de realizar una llamada constante desde el indicador para obtenerla.
Consideraciones finales
Aunque no es la solución final al problema, ha demostrado ser bastante adecuada para este momento. Por lo tanto, no veo razones para no utilizarla. Sin embargo, será necesario encontrar una solución más adecuada en un futuro próximo. Mientras tanto, ya podemos determinar cuándo se generará una nueva barra.
En el siguiente video puedes ver el sistema actual en funcionamiento.
Ejecución de la versión DEMO
Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/12286
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso