
Desarrollo de un sistema de repetición (Parte 73): Una comunicación inusual (II)
Introducción
En el artículo anterior, "Desarrollo de un sistema de repetición (Parte 72): Una comunicación inusual (I)", comencé a mostrar cómo puedes usar un indicador para transmitir un tipo de información que, de otra manera, sería imposible de obtener. Muy bien, pero implementar realmente el código en nuestra aplicación de repetición/simulador no es tan sencillo como parece después de leer el artículo. Quizás estés pensando que estoy exagerando, que implementar algo así es bastante simple y directo, y que solo estoy creando suspenso.
Me gustaría que estuvieras generando suspenso con este tema. Pero la situación es mucho más complicada de lo que podrías estar imaginando. Me he esforzado por enfocar los artículos para explicarles a todos los aspirantes que realmente desean aprender a hacer las cosas en MQL5. Y el tema actual es algo que, hasta el momento en que escribo este artículo, nadie ha explorado. No es que sea algo inimaginable, pero sí es, como mínimo, bastante exótico y muy poco común. Por esta razón, estoy tratando de mostrar con todo detalle cómo debes proceder cuando tengas que hacer algo para lo cual no existen precedentes.
Continuamos con la implementación
Antes de entrar en el tema del servicio propiamente dicho, que sin duda será la parte más complicada de toda la implementación, veamos cómo es el código del indicador de control. Esto es así porque, en el artículo anterior, solo presenté el código del archivo de cabecera C_Controls.mqh. Dado que ya había mucha información, decidí terminar la explicación sobre el indicador de control en este artículo. Así que empecemos por ver el código fuente.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property icon "/Images/Market Replay/Icons/Replay - Device.ico" 04. #property description "Control indicator for the Replay-Simulator service." 05. #property description "This one doesn't work without the service loaded." 06. #property version "1.73" 07. #property link "https://www.mql5.com/pt/articles/12363" 08. #property indicator_chart_window 09. #property indicator_plots 0 10. #property indicator_buffers 1 11. //+------------------------------------------------------------------+ 12. #include <Market Replay\Service Graphics\C_Controls.mqh> 13. //+------------------------------------------------------------------+ 14. C_Controls *control = NULL; 15. //+------------------------------------------------------------------+ 16. input long user00 = 0; //ID 17. //+------------------------------------------------------------------+ 18. double m_Buff[]; 19. int m_RatesTotal = 0; 20. //+------------------------------------------------------------------+ 21. int OnInit() 22. { 23. if (CheckPointer(control = new C_Controls(user00, "Market Replay Control", new C_Mouse(user00, "Indicator Mouse Study"))) == POINTER_INVALID) 24. SetUserError(C_Terminal::ERR_PointerInvalid); 25. if ((_LastError >= ERR_USER_ERROR_FIRST) || (user00 == 0)) 26. { 27. Print("Control indicator failed on initialization."); 28. return INIT_FAILED; 29. } 30. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 31. ArrayInitialize(m_Buff, EMPTY_VALUE); 32. 33. return INIT_SUCCEEDED; 34. } 35. //+------------------------------------------------------------------+ 36. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 37. { 38. (*control).SetBuffer(m_RatesTotal = rates_total, m_Buff); 39. 40. return rates_total; 41. } 42. //+------------------------------------------------------------------+ 43. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 44. { 45. (*control).DispatchMessage(id, lparam, dparam, sparam); 46. (*control).SetBuffer(m_RatesTotal, m_Buff); 47. } 48. //+------------------------------------------------------------------+ 49. void OnDeinit(const int reason) 50. { 51. switch (reason) 52. { 53. case REASON_TEMPLATE: 54. Print("Modified template. Replay // simulation system shutting down."); 55. case REASON_INITFAILED: 56. case REASON_PARAMETERS: 57. case REASON_REMOVE: 58. case REASON_CHARTCLOSE: 59. ChartClose(user00); 60. break; 61. } 62. delete control; 63. } 64. //+------------------------------------------------------------------+
Código fuente del Indicador de control
Si comparas este código con los anteriores, te darás cuenta de inmediato de que hay una diferencia en la línea 25. Antes, la comprobación de la variable _LastError se hacía sobre la constante ERR_SUCCESS. Sin embargo, esto estaba causando algunos problemas. Esto se debe a que, en ocasiones, el indicador se colocaba en el gráfico cuando la variable _LastError contenía algún valor distinto a ERR_SUCCESS.
Me llevó un tiempo comprender por qué, a veces, la inicialización fallaba. Es extraño, ya que, incluso después de usar la llamada a la biblioteca ResetLastError y depurar el código para eliminar cualquier error, a veces _LastError aún tenía algún valor asignado cuando el constructor de la clase C_Controls retornaba.
Lo más extraño de esta historia es que, en muchas ocasiones, el error estaba relacionado con algún otro símbolo o gráfico abierto. Entonces, aparentemente —y quiero que esto quede bien claro, porque no estoy afirmando nada aquí— existe algún tipo de filtración de información entre gráficos diferentes. Pero incluso cuando solo estaba presente el gráfico del símbolo personalizado, se producía un retorno de error que no tenía nada que ver con la aplicación. Por esta razón, decidí aislar los errores. Solo los errores definidos mediante la llamada a la biblioteca SetUserError provocarán que el indicador se cierre y no permanezca en el gráfico.
Sin embargo, lo que realmente necesitamos observar en este código es la función OnCalculate. ¿Por qué es tan importante? El hecho es que la función OnChartEvent se dispara en diversos momentos, principalmente debido a los movimientos del mouse. Recuerda que estamos utilizando el mouse en nuestra aplicación. Sin embargo, la función OnCalculate se dispara siempre que un nuevo tick o cotización llega al gráfico del símbolo. Está bien. Es decir, incluso si solo usas el teclado o alguna interfaz donde la pulsación de teclas produzca ciertos eventos, se producirá una llamada a OnChartEvent. Pero podemos garantizar que la actualización sea más rápida. O, mejor dicho, podemos asegurarnos de que la información del búfer sea lo más actualizada posible. Para garantizar esto, usamos la función OnCalculate.
Observa entonces la línea 38. Es bastante simple y directa, y realiza la misma tarea que la línea 46. Sin embargo, a diferencia de esta última, en la línea 38 se actualizará el búfer con cada nueva cotización que el indicador reciba. Incluso si el mouse se ha quedado inmóvil o no se ha producido el evento del mouse, OnCalculate se ejecutará casi constantemente, lo que asegura que OnChartEvent solo se active después de los cambios.
Ahora ya sabes cómo funciona nuestro indicador de control. Pero no olvides que, al cambiar el timeframe, se ejecuta la función OnDeInit, que elimina el indicador del gráfico, y luego se llama a la función OnInit. Así que, antes de que se dibuje cualquier cosa en el gráfico, se ejecutan OnCalculate y OnChartEvent. Necesitamos que los datos del búfer estén siempre actualizados.
Muy bien, entonces, ¿podemos proceder a modificar el archivo de cabecera C_Replay.mqh? Bueno, podríamos hacerlo ahora. Sin embargo, no estoy seguro de que los aspirantes hayan comprendido realmente cómo se llevará a cabo esta comunicación. Por esta razón, te pido, estimado lector, que tengas un poco más de paciencia. Si ya sabes cómo sucederá o debería suceder, ten paciencia para que quienes aún no lo saben también puedan aprender y comprender cómo ocurre. Por eso, veremos este tema en una próxima lección.
Entendemos el intercambio de información rápida
La mayoría de las personas con menos experiencia en MQL5 probablemente pensarán que intercambiar información, o mejor dicho, leer el búfer del indicador desde el servicio, es algo sencillo. Todo lo que necesitas hacer es usar un handle, también conocido como identificador. De hecho, esto funciona muy bien. Pero hay un detalle. O, para ser más precisos, un problema al usar un handle en estos casos.
Quizás no te sientas tan experimentado o pienses que estoy exagerando, pero ¿cómo es posible que usar un handle tenga un problema? Yo siempre lo he hecho y siempre ha funcionado. De acuerdo, no discutiré tu lógica ni tu experiencia. Las discusiones infundadas no llevan a ninguna parte. En lugar de quedarnos debatiendo y tratando de demostrar algo, ¿qué tal si lo comprobamos? Así nadie podrá argumentar en contra, porque algo que ha sido probado y comprobado no puede rebatirse. Se convierte en una verdad, y punto.
Para demostrar que usar un handle o identificador para acceder a los datos de un búfer —cuando este forma parte de un indicador— no es sostenible cuando el acceso se realiza a través de un servicio, aclarémoslo: la situación NO ES SOSTENIBLE cuando se modifica el timeframe. Es escencial que tengas esto presente. El problema está en el cambio de timeframe. Por lo tanto, hagamos lo siguiente: crearemos dos códigos para probarlo. No te preocupes, serán códigos bastante sencillos y cortos. A continuación, tenemos el primero de ellos: el código del indicador.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property version "1.00" 04. #property indicator_chart_window 05. #property indicator_plots 0 06. #property indicator_buffers 1 07. //+------------------------------------------------------------------+ 08. #include <Market Replay\Defines.mqh> 09. //+------------------------------------------------------------------+ 10. double m_Buff[]; 11. //+------------------------------------------------------------------+ 12. int OnInit() 13. { 14. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 15. ArrayInitialize(m_Buff, EMPTY_VALUE); 16. IndicatorSetString(INDICATOR_SHORTNAME, "TEST"); 17. 18. return INIT_SUCCEEDED; 19. } 20. //+------------------------------------------------------------------+ 21. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 22. { 23. m_Buff[rates_total - 1] = 1.0 * def_IndicatorTimeFrame; 24. 25. return rates_total; 26. } 27. //+------------------------------------------------------------------+
Código fuente del indicador de prueba
Observa que en la línea cinco le indicamos al compilador que no queremos trazar ninguna información. De esta forma, evitaremos que emita alertas con cada nueva compilación. En la línea seis, declaramos que usaremos un búfer. En la línea ocho, incluimos un archivo de cabecera que se utiliza en la aplicación de repetición/simulación. Esto es importante, ya que la idea es comprender cómo accederán los datos al sistema de repetición/simulación. En la línea 10 se encuentra nuestro búfer. En el cuerpo del código OnInit, inicializamos el búfer y declaramos el nombre del indicador. Presta atención a este detalle, porque necesitaremos esta información más adelante.
Ahora presta atención al cuerpo de la función OnCalculate. En la línea 23, colocaremos en el búfer el valor del timeframe. Este valor es el mismo que se mostró en el artículo anterior. Por esta razón, es importante haber comprendido el artículo anterior. Bien. Creo que todos aquí, con más o menos experiencia, saben exactamente qué hace un indicador y cómo funciona, al menos en un nivel básico. Entonces, veamos el segundo código.
01. //+------------------------------------------------------------------+ 02. #property service 03. #property copyright "Daniel Jose" 04. #property description "Data synchronization demo service." 05. //+------------------------------------------------------------------+ 06. input string user01 = "IBOV"; //Accompanying Symbol 07. //+------------------------------------------------------------------+ 08. void OnStart() 09. { 10. int ret, handle; 11. long id; 12. double Buff[1]; 13. 14. if ((id = ChartFirst()) > 0) do 15. { 16. if (ChartSymbol(id) == user01) break; 17. }while ((id = ChartNext(id)) > 0); 18. handle = ChartIndicatorGet(id, 0, "TEST"); 19. do 20. { 21. ret = CopyBuffer(handle, 0, 0, 1, Buff); 22. PrintFormat("CopyBuffer: [ %d ] Value: [ %f ]", ret, Buff[0]); 23. Sleep(250); 24. } while ((!_StopFlag) && (ret > 0)); 25. IndicatorRelease(handle); 26. } 27. //+------------------------------------------------------------------+
Código fuente del servicio de prueba
En la línea dos, indicamos al compilador que el código será un servicio. En la línea seis, añadimos una opción para que el usuario pueda configurar qué símbolo observará el servicio. Hasta aquí, todo va bien. Entre las líneas 10 y 12 declaramos las variables. Ahora viene un detalle: para que el servicio funcione, es necesario que al menos haya un gráfico abierto. De lo contrario, tendremos problemas, ya que no hemos realizado pruebas. Sin embargo, si hay al menos un gráfico abierto, la línea 14 capturará la ID de este gráfico y realizaremos una búsqueda para localizar el gráfico del símbolo indicado en la línea 6. Una vez que lo encontremos, obtendremos la ID correcta.
Con la ID del gráfico, pedimos a MetaTrader 5 que nos proporcione el handle o identificador del indicador. Fíjate en el nombre que se utiliza aquí. Este debe ser el mismo que se especifica en la línea 16 del código fuente del indicador. Ahora entramos en un bucle; dado que tenemos el valor del handle, podemos usarlo para leer el búfer del indicador. Esto se hace en la línea 21. En la línea 22, imprimimos en el terminal la información capturada del búfer, así como el valor retornado en la lectura del mismo. Este bucle continuará ejecutándose mientras se cumplan las condiciones indicadas en la línea 24, es decir, mientras el servicio no se detenga o la lectura no sea negativa. En cuanto esto ocurra, en la línea 25 se libera el handle.
Bien, todos estamos de acuerdo en que este código leerá efectivamente el búfer del indicador y que el valor que se imprimirá corresponderá al valor esperado según las pruebas realizadas en el artículo anterior. Entonces, pongamos esto en práctica en MetaTrader 5. Para verlo más fácilmente, puedes ver el resultado en el video siguiente.
Pero ¿qué ha pasado aquí? Me estás mintiendo. No me creo este video. Estás tratando de engañarme. Muy bien, eres libre de creer lo que quieras. De hecho, no necesitas creerme. Realiza las pruebas que desees, pero manteniendo la idea de usar un handle o identificador para leer el búfer del indicador.
Lo que has visto en el video ocurrirá de verdad. Pero ¿por qué? El secreto está en el handle o identificador. Cuando MetaTrader 5 modifica el timeframe, el valor de identificación del handle cambia. Sin embargo, el código sigue operando con un identificador que ya no es válido porque MetaTrader 5 ha reemplazado el gráfico con un nuevo handle. Cuando utilizas un EA o un indicador para capturar el handle y, a partir de ahí, leer el búfer del indicador, el handle se actualiza, aunque su valor permanezca igual. Esto ocurre porque tanto el EA como cualquier otro indicador se reinsertan en el gráfico, lo que fuerza la actualización de las llamadas. En ese momento, el handle se actualiza y permite leer el búfer correcto.
Sin embargo, el servicio se ejecuta fuera del gráfico. Por lo tanto, el valor del handle no se habrá actualizado. Por esta razón, el valor leído siempre es el mismo. Pero quizá pienses que esto no te afecta, que el servicio opera de alguna manera fuera del «mundo real». Bien. Probemos esta hipótesis. Así obtendremos una prueba concreta de lo que mencioné sobre el handle. Para ello, modificaremos únicamente el código del servicio. El nuevo código se muestra a continuación:
01. //+------------------------------------------------------------------+ 02. #property service 03. #property copyright "Daniel Jose" 04. #property description "Data synchronization demo service." 05. //+------------------------------------------------------------------+ 06. input string user01 = "IBOV"; //Accompanying Symbol 07. //+------------------------------------------------------------------+ 08. void OnStart() 09. { 10. int ret; 11. long id; 12. double Buff[1]; 13. 14. if ((id = ChartFirst()) > 0) do 15. { 16. if (ChartSymbol(id) == user01) break; 17. }while ((id = ChartNext(id)) > 0); 18. do 19. { 20. ret = CopyBuffer(ChartIndicatorGet(id, 0, "TEST"), 0, 0, 1, Buff); 21. PrintFormat("CopyBuffer: [ %d ] Value: [ %f ]", ret, Buff[0]); 22. Sleep(250); 23. } while ((!_StopFlag) && (ret > 0)); 24. } 25. //+------------------------------------------------------------------+
Código fuente del servicio de prueba
Observa que el código ha sufrido pequeñas modificaciones, casi imperceptibles. Pero el gran detalle —y quiero hacer hincapié en esto— está en la línea 20. Ahora, el handle o identificador dejó de ser estático y pasó a ser dinámico. Es decir, aunque el servicio no sepa cuál es el timeframe o qué tipo de modificación está ocurriendo en el gráfico, podrá identificar el indicador correcto y leer su búfer. Para facilitar la comprensión, puedes ver el resultado de la ejecución en el video inmediatamente a continuación:
Ahora sí que piensas que esto es una broma. ¿Cómo es posible? Tranquilo, estimado lector. Como mencioné antes, no quería mostrar los cambios en el archivo C_Replay.mqh sin antes explicártelo.
¿Te das cuenta de cómo algo aparentemente sencillo, pero perfectamente aceptable, puede cambiar por completo lo que ocurre? Como dije al principio del artículo, siempre debemos probar las cosas en el contexto más simple posible. Veo a muchas personas que intentan crear códigos complejos sin tener una base sólida y bien establecida. Al final, lo abandonan porque no logran comprender estos pequeños detalles.
La verdad es que siempre debes equilibrar el coste computacional con el de implementación o ejecución. De nada sirve tener un código que funcione si es lento. Lo mismo ocurre en el sentido opuesto: tener un código rápido que no funciona tampoco es útil.
El hecho de que el handle o identificador sea dinámico en este segundo código hace que su ejecución sea ligeramente más lenta que en la primera versión del mismo servicio. Esto se debe a que en esta segunda versión se realiza una llamada adicional en cada ejecución. En la primera versión, al conocer de antemano el handle, simplemente se consulta su valor en una variable, lo que es mucho más rápido.
Este es el tipo de coste que siempre debes tener en cuenta. Por eso, es bueno probar las cosas en un código más sencillo. Pero entonces, ¿cómo se implementará esto en el archivo de cabecera C_Replay.mqh, que es el encargado de controlar el servicio de repetición/simulador? Para responder a esta pregunta, avancemos al siguiente tema.
Modificamos el archivo C_Replay.mqh
Muy bien. Aquí me enfrenté a un pequeño dilema: ¿mostrar el código ya modificado o mostrar el código mientras se modifica? Este tipo de dilema hace que, a veces, escribir el artículo sea un poco más lento de lo que me gustaría. Crear y probar el código siempre es rápido. Pero explicar lo que ocurrió lleva bastante tiempo.
Sin embargo, mi intención es que el lector entienda y aprenda a hacer las cosas. Yo ya tengo bastante experiencia y he adquirido profundas cicatrices a lo largo de años de programación. De todos modos, creo que mostrar el código mientras se modifica es la mejor opción, aunque esto implique hacer un pequeño esfuerzo y eliminar las partes obsoletas. Veamos, a continuación, el archivo de cabecera C_Replay.mqh completo.
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 SendEventCustom(const ENUM_BOOK_TYPE Arg1 = BOOK_TYPE_BUY_MARKET) 038. { 039. MqlBookInfo book[1]; 040. 041. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 042. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 043. m_IndControl.Memory._8b[7] = 'D'; 044. m_IndControl.Memory._8b[6] = 'M'; 045. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 046. book[0].price = 1.0; 047. book[0].volume = 1; 048. book[0].type = Arg1; 049. CustomBookAdd(def_SymbolReplay, book, 1); 050. } 051. //+------------------------------------------------------------------+ 052. inline void CheckIndicatorControl(void) 053. { 054. static uchar memTimeFrame = 0; 055. static C_Controls::eObjectControl memMode = m_IndControl.Mode; 056. double Buff[]; 057. 058. if (CopyBuffer(ChartIndicatorGet(m_Infos.IdReplay, 0, "Market Replay Control"), 0, 0, 1, Buff) < 0) ChartClose(m_Infos.IdReplay); 059. m_IndControl.Memory.dValue = Buff[0]; 060. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] >= m_IndControl.Position) 061. { 062. if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) 063. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 064. if (m_IndControl.Memory._8b[def_IndexTimeFrame] == memTimeFrame) 065. { 066. memMode = m_IndControl.Mode; 067. return; 068. } 069. memTimeFrame = m_IndControl.Memory._8b[def_IndexTimeFrame]; 070. m_IndControl.Mode = memMode; 071. } 072. SendEventCustom(m_IndControl.Mode != C_Controls::ePlay ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY); 073. } 074. //+------------------------------------------------------------------+ 075. inline void UpdateIndicatorControl(void) 076. { 077. double Buff[]; 078. 079. if (m_IndControl.Handle == INVALID_HANDLE) return; 080. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 081. { 082. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 083. m_IndControl.Memory.dValue = Buff[0]; 084. if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) 085. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 086. { 087. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 088. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 089. m_IndControl.Memory._8b[7] = 'D'; 090. m_IndControl.Memory._8b[6] = 'M'; 091. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 092. } 093. } 094. //+------------------------------------------------------------------+ 095. void SweepAndCloseChart(void) 096. { 097. long id; 098. 099. if ((id = ChartFirst()) > 0) do 100. { 101. if (ChartSymbol(id) == def_SymbolReplay) 102. ChartClose(id); 103. }while ((id = ChartNext(id)) > 0); 104. } 105. //+------------------------------------------------------------------+ 106. inline int RateUpdate(bool bCheck) 107. { 108. static int st_Spread = 0; 109. 110. st_Spread = (bCheck ? (int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time) : st_Spread + 1); 111. m_Infos.Rate[0].spread = (int)(def_MaskTimeService | st_Spread); 112. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 113. 114. return 0; 115. } 116. //+------------------------------------------------------------------+ 117. inline void CreateBarInReplay(bool bViewTick) 118. { 119. bool bNew; 120. double dSpread; 121. int iRand = rand(); 122. 123. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 124. { 125. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 126. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 127. { 128. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 129. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 130. { 131. m_Infos.tick[0].ask = m_Infos.tick[0].last; 132. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 133. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 134. { 135. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 136. m_Infos.tick[0].bid = m_Infos.tick[0].last; 137. } 138. } 139. if (bViewTick) 140. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 141. RateUpdate(true); 142. } 143. m_Infos.CountReplay++; 144. } 145. //+------------------------------------------------------------------+ 146. void AdjustViewDetails(void) 147. { 148. MqlRates rate[1]; 149. 150. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 151. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 152. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE); 153. m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); 154. CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate); 155. if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE)) 156. for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++); 157. if (rate[0].close > 0) 158. { 159. if (GetInfoTicks().ModePlot == PRICE_EXCHANGE) 160. m_Infos.tick[0].last = rate[0].close; 161. else 162. { 163. m_Infos.tick[0].bid = rate[0].close; 164. m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick); 165. } 166. m_Infos.tick[0].time = rate[0].time; 167. m_Infos.tick[0].time_msc = rate[0].time * 1000; 168. }else 169. m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay]; 170. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 171. } 172. //+------------------------------------------------------------------+ 173. void AdjustPositionToReplay(void) 174. { 175. int nPos, nCount; 176. 177. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return; 178. nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); 179. for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); 180. if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1); 181. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 182. CreateBarInReplay(false); 183. } 184. //+------------------------------------------------------------------+ 185. void WaitIndicatorLoad(const string szArg, const bool ViewCtrl = true) 186. { 187. Print("Waiting for ", szArg); 188. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, szArg) == INVALID_HANDLE)) 189. { 190. if (ViewCtrl) CheckIndicatorControl(); 191. Sleep(100); 192. } 193. } 194. //+------------------------------------------------------------------+ 195. public : 196. //+------------------------------------------------------------------+ 197. C_Replay() 198. :C_ConfigService() 199. { 200. Print("************** Market Replay Service **************"); 201. srand(GetTickCount()); 202. SymbolSelect(def_SymbolReplay, false); 203. CustomSymbolDelete(def_SymbolReplay); 204. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 205. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0); 206. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0); 207. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0); 208. CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); 209. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8); 210. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TICKS_BOOKDEPTH, 1); 211. SymbolSelect(def_SymbolReplay, true); 212. m_Infos.CountReplay = 0; 213. m_IndControl.Handle = INVALID_HANDLE; 214. m_IndControl.Mode = C_Controls::ePause; 215. m_IndControl.Position = 0; 216. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; 217. } 218. //+------------------------------------------------------------------+ 219. ~C_Replay() 220. { 221. SweepAndCloseChart(); 222. IndicatorRelease(m_IndControl.Handle); 223. SymbolSelect(def_SymbolReplay, false); 224. CustomSymbolDelete(def_SymbolReplay); 225. Print("Finished replay service..."); 226. } 227. //+------------------------------------------------------------------+ 228. bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate) 229. { 230. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0) 231. return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket."); 232. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0) 233. return MsgError("Asset configuration is not complete, need to declare the ticket value."); 234. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0) 235. return MsgError("Asset configuration not complete, need to declare the minimum volume."); 236. SweepAndCloseChart(); 237. m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1); 238. if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl")) 239. Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl"); 240. else 241. Print("Apply template: ", szNameTemplate, ".tpl"); 242. 243. return true; 244. } 245. //+------------------------------------------------------------------+ 246. bool InitBaseControl(const ushort wait = 1000) 247. { 248. int handle; 249. 250. Sleep(wait); 251. AdjustViewDetails(); 252. Print("Loading Control Indicator..."); 253. if ((handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 254. ChartIndicatorAdd(m_Infos.IdReplay, 0, handle); 255. IndicatorRelease(handle); 256. WaitIndicatorLoad("Market Replay Control", false); 257. SendEventCustom(); 258. WaitIndicatorLoad("Indicator Mouse Study"); 259. UpdateIndicatorControl(); 260. SendEventCustom(); 261. 262. return def_CheckLoopService; 263. } 264. //+------------------------------------------------------------------+ 265. bool LoopEventOnTime(void) 266. { 267. int iPos, iCycles; 268. MqlBookInfo book[1]; 269. ENUM_BOOK_TYPE typeMsg, memBook; 270. 271. book[0].price = 1.0; 272. book[0].volume = 1; 273. book[0].type = BOOK_TYPE_BUY_MARKET; 274. CustomBookAdd(def_SymbolReplay, book, 1); 275. SendEventCustom(); 276. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 277. { 278. UpdateIndicatorControl(); 279. CheckIndicatorControl(); 280. Sleep(200); 281. } 282. m_MemoryData = GetInfoTicks(); 283. AdjustPositionToReplay(); 284. iPos = iCycles = 0; 285. SendEventCustom(memBook = BOOK_TYPE_BUY); 286. book[0].type = BOOK_TYPE_BUY; 287. CustomBookAdd(def_SymbolReplay, book, 1); 288. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 289. { 290. if (m_IndControl.Mode == C_Controls::ePause) return true; 291. 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); 292. if ((typeMsg = (iPos >= 60000 ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY)) != book[0].type) 293. if ((typeMsg = (iPos >= 60000 ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY)) != memBook) 294. SendEventCustom(memBook = typeMsg); 295. { 296. book[0].type = typeMsg; 297. CustomBookAdd(def_SymbolReplay, book, 1); 298. } 299. CreateBarInReplay(true); 300. while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause)) 301. { 302. Sleep(195); 303. iPos -= 200; 304. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 305. UpdateIndicatorControl(); 306. CheckIndicatorControl(); 307. iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1); 308. } 309. } 310. 311. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 312. } 313. }; 314. //+------------------------------------------------------------------+ 315. #undef def_SymbolReplay 316. #undef def_CheckLoopService 317. #undef def_MaxSlider 318. //+------------------------------------------------------------------+
Código fuente del archivo C_Replay.mqh
Observa que todas y cada una de las líneas que aparecen destacadas deben eliminarse del código. Pero primero veamos por qué se han eliminado tantas líneas.
El primer punto que debemos observar es la línea 23. Como se mencionó en el tema anterior, NO DEBEMOS USAR UN HANDLE ESTÁTICO. Por esta razón, este valor de handle se ha vuelto obsoleto. Por tanto, se eliminaron diversas partes del código que hacían referencia a él. Además, se eliminó un procedimiento completo del código. Me refiero a UpdateIndicatorControl, que se encontraba entre las líneas 75 y 93, por lo tanto, cualquier referencia a este procedimiento también se eliminó. Esto significa que, de alguna manera, debemos reemplazar lo que antes se ejecutaba mediante UpdateIndicatorControl. Pero llegaremos a ese punto muy pronto.
Antes de continuar, veamos rápidamente dos funciones. La primera es InitBaseControl, que comienza en la línea 243. Observa que ahora tiene algunos pequeños cambios. Estas modificaciones tienen como objetivo mejorar la experiencia del usuario al usar la aplicación y estandarizar la inicialización de los indicadores. Veamos qué ocurre aquí.
Entre las líneas 253 y 255 intentamos cargar el indicador de control. Ahora presta atención a este hecho: la carga del indicador no se produce al instante. Hay una pequeña latencia en su ejecución. Por este motivo, antes de ejecutar la línea 257, debemos asegurarnos de que el indicador de control ya se encuentra en el gráfico. Esto se logra con la llamada a la línea 253. Nota que los nombres presentes en las líneas 256 y 258 son los nombres de los indicadores que esperamos cargar. Estas llamadas se ejecutan en la línea 185, momento a partir del cual la situación comienza a ponerse interesante. Así que presta mucha atención o podrías perderte detalles críticos.
El bucle de la línea 188 esperará a que se haya cargado en el gráfico el indicador especificado. Cuando la línea 256 solicita esperar por el indicador de control, la prueba en la línea 187 evita que revisemos dicho indicador. Sin embargo, cuando la línea 255 solicita esperar por el indicador del mouse, la prueba en la línea 190 revisará el búfer del indicador de control. ¿Por qué sucede esto? El motivo es permitir que el usuario pueda cambiar el timeframe. Para entenderlo mejor, veamos la línea 52.
En esta línea 52 se encuentra el procedimiento que realiza la lectura del búfer del indicador de control. Ahora viene la parte interesante. Aquí tenemos dos variables estáticas declaradas en las líneas 54 y 55, pero en este momento solo nos interesa la de la línea 54. Observa que la inicializamos a cero. Si en la línea 58, donde intentamos leer el búfer del indicador de control, el valor retornado es menor que cero, indica que el indicador ha sido eliminado del gráfico. Dado que el usuario no puede volver a colocarlo en el gráfico y es necesario, cerramos este. Al hacerlo, la aplicación se cierra automáticamente. Por esta razón, no podemos verificar el indicador de control mientras se está cargando.
Entonces, en la línea 60, comprobamos si el valor del indicador de control es mayor que el de la posición analizada en el servicio. Si este es el caso, indica que debemos realizar un avance rápido cuando el usuario pulse play. Pero lo que realmente nos interesa es la prueba que se realiza en la línea 64. Esta prueba es el talón de Aquiles del bloqueo de tiempo, que expliqué en el tema anterior. Si el usuario no ha modificado el timeframe, esta prueba será verdadera. En este caso, no hacemos nada.
Si esta prueba es falsa, en la línea 69 almacenamos el nuevo timeframe y, en la línea 70, recuperamos el último estado del indicador. Esto se debe a que, en la línea 72, llamaremos a otro procedimiento. Por tanto, ahora nos dirigimos a la línea 37. Aquí es donde las cosas se ponen realmente interesantes, ya que es precisamente aquí donde pedimos a MetaTrader 5 que dispare los eventos en el gráfico. La separación de estas dos acciones se debe a que, en algunos momentos, solo deseamos disparar eventos, mientras que en otros realmente queremos verificar y confirmar si ha habido algún cambio en los valores del indicador de control.
Observa que todo el contenido del procedimiento SendEventCustom ya existía en el código de la versión anterior. Por este motivo, no considero necesario explicar lo que sucede aquí. Además, el código es bastante sencillo y directo. Con esto, creo haber explicado con claridad qué hemos comenzado a implementar en el código. Sin embargo, aún queda por hablar sobre los cambios realizados en la función LoopEventOnTime. Aunque los cambios no son realmente drásticos ni alteran completamente la forma en que se ejecuta la función, hacen más evidente la razón por la que se separó el antiguo procedimiento UpdateIndicatorControl en dos procedimientos distintos. Pasemos, pues, a la explicación, que será breve.
En la línea 275, pedimos a MetaTrader 5 que envíe los eventos personalizados. De este modo, se ajustan correctamente tanto el indicador de mouse como el de control para poder empezar a utilizar el sistema. Aunque esta línea 275 podría descartarse en la primera ejecución, después de presionar play por primera vez, necesitamos que se ejecute nuevamente para revisar los datos, ya que volverá a ejecutarse cuando entremos en el modo pausado.
En la línea 279 no enviamos ningún tipo de evento, pero necesitamos observar lo que ocurre en el indicador de control. Tan pronto como el usuario pulse play, el bucle de la línea 276 se cerrará y se iniciará el proceso de simulación y repetición.
En la línea 285 se realiza una nueva solicitud de envío de eventos personalizados. En esta ocasión, el objetivo es permitir que el indicador de mouse muestre el tiempo restante de la barra.
Otra pequeña diferencia ocurre en las líneas 293 y 294, que es bastante sencilla de entender, ya que su propósito es cambiar el estado del indicador de mouse. Esto nos informa de si el símbolo ha entrado o salido de subasta.
La última diferencia está en la línea 306, donde verificamos si se ha modificado el timeframe. Si esto ocurre, los indicadores se reinstalarán, tal como expliqué al inicio de este tema.
Con esto, tenemos todas las modificaciones necesarias. En el siguiente video se puede observar el comportamiento del sistema cuando se realiza un cambio en el timeframe.
Conclusión
En estos dos últimos artículos, mostré y expliqué la importancia de practicar y llevar la programación al límite. Aunque algunos puedan pensar que en estos dos artículos no se aprendió realmente nada, no deberías verlo así. Estoy seguro de que muchos pensaban que lo que expliqué aquí no era posible o viable. Sin embargo, antes de intentar modificar el código principal, demostré que era fundamental pensar primero en una solución hipotética y luego construir un código lo más sencillo posible para probar esa hipótesis.
El principal aprendizaje es no rendirse nunca en el primer intento. Si algo no funciona la primera vez, cambia la forma en que estás haciendo las cosas, pero sigue probando la misma hipótesis. Eso es precisamente lo que ocurrió aquí. Quería colocar en el búfer del indicador algún tipo de información que me indicara de manera clara y fiable cuándo se había cambiado el timeframe. Hacerlo dentro del gráfico era sencillo. La hipótesis era la siguiente: ¿es posible lograr que el servicio detecte este cambio? La primera implementación falló, pero aún así podíamos leer el valor que indicaba el timeframe. Sin embargo, ese valor solo representaba el timeframe en el momento en que el indicador se colocaba por primera vez en el gráfico.
En este punto, surge el concepto. Si hubiera desistido, en lugar de intentar un nuevo enfoque en el que el handle o identificador se obtuviera en cada llamada, habría perdido la posibilidad de acceder a los datos reales que el indicador estaba modificando en cada iteración. Pero al cambiar de enfoque, se abre una nueva puerta que muestra que podemos ir mucho más allá de lo que muchos creen posible. Así es como se construyen las cosas: se plantea una hipótesis, se prueba y, aunque los resultados iniciales no sean los esperados, pero funcionen parcialmente, se ajusta el enfoque para obtener los datos de una manera diferente, siempre manteniendo la hipótesis original. En el anexo encontrarás los ejecutables necesarios para usar el sistema de repetición/simulador.
En el próximo artículo, empezaremos a explorar otras características que necesitamos añadir a la aplicación de repetición/simulador. ¡Nos vemos allí!
Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/12363





- 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