
Desarrollo de un sistema de repetición (Parte 71): Ajuste del tiempo (IV)
Introducción
En el artículo anterior, "Desarrollo de un sistema de repetición (Parte 70): Ajuste del tiempo (III), expliqué las modificaciones necesarias en el indicador del mouse. Dichos cambios tienen como objetivo permitir que el indicador del mouse reciba eventos de libro de órdenes. Esto ocurre cuando el indicador del mouse se utiliza junto con la aplicación de repetición/simulación. Es posible que, estimado lector, hayas sentido bastante indignación y confusión ante todas esas modificaciones. Sé que muchas de ellas no tienen sentido al principio y resultan mucho más confusas de lo que me gustaría mostrar. Sin embargo, es imprescindible que hayas comprendido ese contenido por completo, por más confuso que parezca al principio. Soy consciente de que muchos de ustedes tendrán dificultades iniciales para entender lo que intenté explicar allí. Sin embargo, sin comprender ese contenido, donde utilicé un servicio mucho más simple para mostrar cómo funciona todo, tratar de entender lo que se explicará aquí será mucho más complicado.
Por lo tanto, antes de intentar comprender lo que se hará en este artículo, asegúrate de haber entendido lo que se mostró en el artículo anterior. Especialmente la parte relacionada con el hecho de que, al agregar eventos de libro de órdenes al símbolo personalizado, adquirimos la capacidad de usar la función OnCalculate de una forma que antes no era posible. Esto nos obliga a usar una llamada iSpread para obtener los datos que MetaTrader 5 pondrá a nuestra disposición.
En este artículo, transferiremos (o, mejor dicho, transcribiremos) parte de ese código del servicio de prueba al servicio de repetición/simulación. La cuestión aquí no es tanto cómo hacerlo, sino cómo deberíamos hacerlo.
Quiero recordarte, estimado lector, que hasta el artículo anterior el indicador del mouse se cargaba mediante una plantilla mientras trabajábamos en el servicio de repetición/simulación. Sin embargo, ahora ya no lo haré de esa forma. Puedes intentar mantener el indicador del mouse cargado a través de la plantilla, pero debido a ciertas cuestiones prácticas, comenzaré a colocar el indicador del mouse manualmente en el gráfico del símbolo que se utiliza para generar la repetición/simulación. Por lo tanto, no te sorprendas si en algún vídeo muestro las cosas de esta manera. Tengo mis razones para hacerlo así, pero no entraré en detalles. Sin más preámbulos, comencemos a transcribir el código del servicio de prueba al servicio de repetición/simulación.
Iniciamos la transcripción
Lo primero que haremos será modificar una parte del código del archivo C_Replay.mqh. Presta atención al siguiente fragmento:
197. //+------------------------------------------------------------------+ 198. bool InitBaseControl(const ushort wait = 1000) 199. { 200. Print("Waiting for Mouse Indicator..."); 201. Sleep(wait); 202. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200); 203. if (def_CheckLoopService) 204. { 205. AdjustViewDetails(); 206. Print("Waiting for Control Indicator..."); 207. if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 208. ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle); 209. UpdateIndicatorControl(); 210. } 211. 212. return def_CheckLoopService; 213. } 214. //+------------------------------------------------------------------+
Fragmento de código del archivo C_Terminal.mqh
Este fragmento es el código original. Ahora quiero que observes el siguiente detalle. Este código fue diseñado para funcionar cuando el indicador del mouse estaba en una plantilla. Sin embargo, como a partir de ahora colocaré el indicador del mouse de manera manual, este código ya no es tan adecuado. De hecho, aún funcionaría, pero podemos mejorarlo para obtener una configuración más adecuada en términos de mensajes y de ejecución. Por lo tanto, el nuevo código que se utilizará se puede ver a continuación:
197. //+------------------------------------------------------------------+ 198. bool InitBaseControl(const ushort wait = 1000) 199. { 200. Sleep(wait); 201. AdjustViewDetails(); 202. Print("Loading Control Indicator..."); 203. if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 204. ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle); 205. Print("Waiting for Mouse Indicator..."); 206. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200); 207. UpdateIndicatorControl(); 208. 209. return def_CheckLoopService; 210. } 211. //+------------------------------------------------------------------+
Fragmento del archivo C_Replay.mqh
Básicamente, el código casi no ha cambiado. Sin embargo, los mensajes que se muestran ahora son más claros y el orden de ejecución ha cambiado. De esta manera, primero intentaremos cargar el indicador de control, que es un recurso del aplicativo de repetición/simulador. Esto se debe a que está incorporado en el ejecutable del repetidor/simulador. Solo después se cargará el indicador del mouse. Observa el siguiente hecho: si el indicador de control incluido en el servicio no se puede cargar, indica una incidencia grave. Pero si se carga correctamente, entonces se informará al usuario de que es necesario cargar el indicador del mouse. En mi opinión, esto resulta mucho más adecuado. Sin embargo, siéntete libre de colocar el orden que prefieras. En cualquier caso, el indicador de control no funcionará si el indicador del mouse no está presente en el gráfico.
Este cambio es prácticamente estético. Pero ahora vamos a los cambios que realmente darán soporte a los mensajes de Book. Si no has entendido el artículo anterior, vuelve a él y compréndelo con ese código. No intentes entender cómo funcionan las cosas con el código que se mostrará a partir de este punto. Si lo intentas, te confundirás completamente y no entenderás nada.
Es necesario añadir una nueva línea en el constructor de la clase. Esta nueva línea se puede ver en el fragmento de código siguiente:
149. //+------------------------------------------------------------------+ 150. C_Replay() 151. :C_ConfigService() 152. { 153. Print("************** Market Replay Service **************"); 154. srand(GetTickCount()); 155. SymbolSelect(def_SymbolReplay, false); 156. CustomSymbolDelete(def_SymbolReplay); 157. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 158. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0); 159. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0); 160. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0); 161. CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); 162. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8); 163. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TICKS_BOOKDEPTH, 1); 164. SymbolSelect(def_SymbolReplay, true); 165. m_Infos.CountReplay = 0; 166. m_IndControl.Handle = INVALID_HANDLE; 167. m_IndControl.Mode = C_Controls::ePause; 168. m_IndControl.Position = 0; 169. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; 170. } 171. //+------------------------------------------------------------------+
Fragmento del archivo C_Replay.mqh
La nueva línea es precisamente la 163. Una vez hecho esto, ya podemos hacer uso de los mensajes de Book. Ahora presta mucha atención a un detalle. En realidad, el detalle no está en el archivo de encabezado C_Replay.mqh, sino en el indicador del mouse. Por lo tanto, tomaremos prestado un fragmento del indicador para entenderlo mejor. Ya que es importante.
27. //+------------------------------------------------------------------+ 28. int OnInit() 29. { 30. Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04); 31. if (_LastError >= ERR_USER_ERROR_FIRST) return INIT_FAILED; 32. MarketBookAdd((*Study).GetInfoTerminal().szSymbol); 33. OnBookEvent((*Study).GetInfoTerminal().szSymbol); 34. m_Status = C_Study::eCloseMarket; 35. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 36. ArrayInitialize(m_Buff, EMPTY_VALUE); 37. 38. return INIT_SUCCEEDED; 39. } 40. //+------------------------------------------------------------------+
Fragmento del archivo del indicador de mouse
Observa que en la línea 34 indicamos el estado inicial del indicador del mouse. Mostrará que el mercado está cerrado. Sin embargo, no estamos gestionando realmente el mercado físico. Utilizamos una aplicación cuyo objetivo principal es permitirnos ejecutar una simulación de posibles movimientos del mercado. Por lo tanto, el mensaje que se muestra en el indicador del mouse cuando estamos en el símbolo personalizado de replay/simulación es incorrecto. Pero esto es algo muy sencillo de corregir. Antes de corregirlo, es importante que entiendas que, en el momento en que el indicador de control se coloca en el gráfico, la simulación está efectivamente en modo pausa. Esto ocurre cuando se inicia la aplicación desde el principio. Ahora debemos tomar una decisión que influirá profundamente en los eventos posteriores.
Pensemos en lo siguiente: cuando estemos en modo pausa, ¿el indicador del mouse debería mostrar un mensaje de subasta o el tiempo restante de la barra? Si debe mostrar la información de la barra, ¿queremos que aparezca antes del primer play o solo después de ejecutarlo? Quizás esto resulte un poco confuso. Así que consideremos lo siguiente: antes de que el mercado abra oficialmente, se realiza una subasta para que los participantes puedan posicionar sus órdenes al mejor precio, es decir, donde realmente desean comprar o vender. Por lo tanto, tan pronto como la aplicación de reproducción/simulación haga que MetaTrader 5 abra el gráfico y el indicador del mouse sea visible lo que deberíamos ver es un mensaje de subasta. Esto es un hecho. Ahora bien, cuando se habilite la simulación o el replay, ¿qué mensaje debería aparecer cuando se active la pausa? ¿Te gustaría ver el mensaje de subasta o el mensaje del tiempo restante de la barra actual? Esta es la cuestión que debemos considerar. En cualquier caso, lo primero que debemos garantizar es que, en el momento en que se inicie la aplicación, se muestre que estamos en subasta.
Hacer esto es algo bastante sencillo. Observa el siguiente fragmento:
212. //+------------------------------------------------------------------+ 213. bool LoopEventOnTime(void) 214. { 215. int iPos, iCycles; 216. MqlBookInfo book[1]; 217. 218. book[0].price = 1.0; 219. book[0].volume = 1; 220. book[0].type = BOOK_TYPE_BUY_MARKET; 221. CustomBookAdd(def_SymbolReplay, book, 1); 222. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 223. { 224. UpdateIndicatorControl(); 225. Sleep(200); 226. } 227. m_MemoryData = GetInfoTicks(); 228. AdjustPositionToReplay(); 229. iPos = iCycles = 0; 230. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 231. { 232. if (m_IndControl.Mode == C_Controls::ePause) return true; 233. 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); 234. CreateBarInReplay(true); 235. while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause)) 236. { 237. Sleep(195); 238. iPos -= 200; 239. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 240. UpdateIndicatorControl(); 241. iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1); 242. } 243. } 244. 245. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 246. } 247. }; 248. //+------------------------------------------------------------------+
Fragmento del archivo C_Replay.mqh
Aquí tenemos la parte fácil de lo que realmente debemos implementar. Iré realizando la implementación paso a paso para que puedas seguir claramente lo que se está haciendo. Observa que en la línea 216 hay una nueva variable. En realidad, es un array con una longitud de una unidad. Ahora llega la parte interesante.
Probablemente imaginas que los valores que se generan en un evento de libro de órdenes deben tener algún significado. Bueno, en realidad no tienen por qué significar nada. Solo tenemos que asegurarnos de que los valores que activan el evento de libro de órdenes sigan una lógica. De hecho, no tienen que significar nada, salvo si deseas simular el libro de órdenes. Pero no tengo esa intención, al menos por ahora. Quizás en el futuro.
De todos modos, las líneas 218 y 219 se utilizarán para mostrar algo en el libro en MetaTrader 5. Estos valores no tienen ningún significado especial. Solo sirven para dar soporte a lo que realmente me interesa, que es la línea 220, en la que indicamos al libro de órdenes que tenemos una posición indicativa de subasta. Si no lo has entendido, revisa nuevamente el artículo anterior para comprenderlo. En la línea 221, indicamos al MetaTrader 5 que dispare un evento personalizado del libro de órdenes. Este evento será capturado por la función OnEventBook, que en este caso se encuentra en el indicador del mouse. Resultado: cada vez que se ejecuta la función LoopEventOnTime, el indicador del mouse muestra efectivamente que estamos en subasta. Existen dos casos en los que se ejecuta la función LoopEventOnTime desde el inicio. El primer caso ocurre cuando se inicia la aplicación. El segundo caso sucede cuando el usuario interactúa con el indicador de control presionando el botón de pausa del sistema. En ese momento, se ejecuta la línea 232 y, a continuación, se vuelve a ejecutar la función LoopEventOnTime. ¿Has notado lo sencillo que es? Ahora que ya tenemos el mensaje de subasta mostrado, ¿cómo podemos mostrar la información del tiempo restante de las barras? Bueno, esta es la parte más sencilla. Para ello, debemos modificar el fragmento anterior con el siguiente:
212. //+------------------------------------------------------------------+ 213. bool LoopEventOnTime(void) 214. { 215. int iPos, iCycles; 216. MqlBookInfo book[1]; 217. 218. book[0].price = 1.0; 219. book[0].volume = 1; 220. book[0].type = BOOK_TYPE_BUY_MARKET; 221. CustomBookAdd(def_SymbolReplay, book, 1); 222. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 223. { 224. UpdateIndicatorControl(); 225. Sleep(200); 226. } 227. m_MemoryData = GetInfoTicks(); 228. AdjustPositionToReplay(); 229. iPos = iCycles = 0; 230. book[0].type = BOOK_TYPE_BUY; 231. CustomBookAdd(def_SymbolReplay, book, 1); 232. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 233. { 234. if (m_IndControl.Mode == C_Controls::ePause) return true; 235. 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); 236. CreateBarInReplay(true); 237. while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause)) 238. { 239. Sleep(195); 240. iPos -= 200; 241. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 242. UpdateIndicatorControl(); 243. iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1); 244. } 245. } 246. 247. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 248. } 249. }; 250. //+------------------------------------------------------------------+
Fragmento del archivo C_Replay.mqh
¿Has notado la diferencia? ¿Sí? ¿No? ¿Tal vez? Bueno, si no has notado la diferencia es porque estás muy distraído, ya que la diferencia radica precisamente en la inclusión de las líneas 230 y 231. Estas dos líneas simples hacen que, en el momento en que el usuario presiona el botón de play en el servicio de repetición/simulación, el indicador del mouse reciba un evento personalizado de libro de órdenes. Este evento indica que pasamos de la condición de subasta a la de símbolo en negociación. Así, el tiempo restante de la barra actual comenzará a mostrarse en el indicador del mouse. Es muy sencillo, ¿verdad? Sin embargo, ahora nos enfrentamos a una condición un poco más complicada de resolver.
En el mercado real, es decir, cuando estamos conectados al servidor de trading, pueden producirse momentos en los que un símbolo se suspenda o entre en subasta. Esto sucede debido a ciertas reglas muy específicas, como expliqué en el artículo anterior. Pero aquí haremos lo siguiente: crearemos una regla bastante simple. Si el símbolo tiene un intervalo de tiempo igual o superior a 60 segundos entre un tick y otro, haremos que el indicador del mouse indique que el símbolo ha entrado en subasta. Esta es la parte sencilla. La parte complicada es cómo hacer que el indicador del mouse vuelva a mostrar el tiempo restante de la barra.
Podrías sugerir lo siguiente: cuando el símbolo entre en subasta, enviamos la constante BOOK_TYPE_BUY_MARKET al libro de órdenes, y cuando salga, enviamos BOOK_TYPE_BUY. Sí, eso es exactamente lo que debemos hacer. Pero, ¿cómo lo hacemos? Consideremos lo siguiente: no queremos que la función LoopEventOnTime se ejecute desde el principio. Queremos mantener el sistema funcionando dentro del bucle que comienza en la línea 232 y termina en la 245, por lo que, si haces que las constantes BOOK_TYPE_BUY_MARKET y BOOK_TYPE_BUY se envíen dentro de este bucle, tendrás que tener cuidado. Esto se debe a que cada llamada a CustomBookAdd con estas constantes diferentes producirá, con toda seguridad, un efecto indeseado para quien esté observando el indicador del mouse. Este efecto sería que el indicador parpadee y alterne constantemente entre el tiempo restante y la palabra «SUBASTA».
Por esta razón, debemos ser creativos. Debemos implementar una solución que no provoque este efecto, pero que al mismo tiempo resuelva nuestro problema. La solución que propongo se muestra en el siguiente fragmento:
212. //+------------------------------------------------------------------+ 213. bool LoopEventOnTime(void) 214. { 215. int iPos, iCycles; 216. MqlBookInfo book[1]; 217. ENUM_BOOK_TYPE typeMsg; 218. 219. book[0].price = 1.0; 220. book[0].volume = 1; 221. book[0].type = BOOK_TYPE_BUY_MARKET; 222. CustomBookAdd(def_SymbolReplay, book, 1); 223. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 224. { 225. UpdateIndicatorControl(); 226. Sleep(200); 227. } 228. m_MemoryData = GetInfoTicks(); 229. AdjustPositionToReplay(); 230. iPos = iCycles = 0; 231. book[0].type = BOOK_TYPE_BUY; 232. CustomBookAdd(def_SymbolReplay, book, 1); 233. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 234. { 235. if (m_IndControl.Mode == C_Controls::ePause) return true; 236. 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); 237. if ((typeMsg = (iPos >= 60000 ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY)) != book[0].type) 238. { 239. book[0].type = typeMsg; 240. CustomBookAdd(def_SymbolReplay, book, 1); 241. } 242. CreateBarInReplay(true); 243. while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause)) 244. { 245. Sleep(195); 246. iPos -= 200; 247. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 248. UpdateIndicatorControl(); 249. iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1); 250. } 251. } 252. 253. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 254. } 255. }; 256. //+------------------------------------------------------------------+
Fragmento del archivo C_Replay.mqh
Sé que no es muy elegante, pero al menos funciona. Si lo deseas, puedes probarlo en diversas condiciones diferentes, aumentando o disminuyendo el tiempo. Eres libre de hacerlo. Pero antes de modificar nada, ¿qué te parece si entendemos qué está ocurriendo en este fragmento?
Observa lo siguiente: en la línea 217 declaro una nueva variable. Esta se utilizará para alojar una de las constantes posibles que pueden emplearse en el libro. En la línea 237 utilizamos el operador ternario para agilizar nuestro código, ya que la idea es evaluar una condición y, en función de ella, asignar un valor a la variable typeMsg. Ahora presta atención: podría haber compactado aún más este código, pero la explicación se volvería demasiado complicada. La explicación es la siguiente: después de que se asigne una de las constantes a typeMsg, verificamos si su valor es diferente del último valor lanzado como evento personalizado de libro. Si es así, en la línea 239 asignamos la nueva constante que se utilizará y, en la línea 240, hacemos una llamada a CustomBookAdd. La parte a la que realmente debes prestar atención está relacionada con el valor que se compara con la variable iPos en la línea 237. Observa que allí estoy comparando con el valor 60000 (sesenta mil). Pero ¿por qué ese valor? ¿No estábamos usando un límite de un minuto? Sí, pero quizá olvidaste que un minuto son sesenta segundos. Observa ahora que en la línea 236 el valor que se asigna a iPos se da en milisegundos y un segundo tiene mil milisegundos. ¿Entiendes ahora por qué estamos comparando iPos con el valor de sesenta mil? Esto se debe a que el valor está en milisegundos. Por lo tanto, si decides modificar este valor por otro que consideres más adecuado, simplemente asegúrate de establecer el valor correcto. De lo contrario, la información que aparece en el indicador del mouse diferirá mucho de lo que esperabas ver cuando el símbolo permaneciera cierto tiempo sin que se añadieran ticks al gráfico.
Así, finalmente podemos ver el código final del archivo C_Replay.mqh. En este momento, todo lo referente al tiempo ya está adecuadamente implementado, al menos de forma que el resultado final es el que se muestra en el vídeo siguiente:
A continuación, se muestra el código completo del archivo de encabezado C_Replay.mqh:
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 int RateUpdate(bool bCheck) 070. { 071. static int st_Spread = 0; 072. 073. st_Spread = (bCheck ? (int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time) : st_Spread + 1); 074. m_Infos.Rate[0].spread = (int)(def_MaskTimeService | st_Spread); 075. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 076. 077. return 0; 078. } 079. //+------------------------------------------------------------------+ 080. inline void CreateBarInReplay(bool bViewTick) 081. { 082. bool bNew; 083. double dSpread; 084. int iRand = rand(); 085. 086. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 087. { 088. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 089. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 090. { 091. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 092. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 093. { 094. m_Infos.tick[0].ask = m_Infos.tick[0].last; 095. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 096. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 097. { 098. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 099. m_Infos.tick[0].bid = m_Infos.tick[0].last; 100. } 101. } 102. if (bViewTick) 103. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 104. RateUpdate(true); 105. } 106. m_Infos.CountReplay++; 107. } 108. //+------------------------------------------------------------------+ 109. void AdjustViewDetails(void) 110. { 111. MqlRates rate[1]; 112. 113. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 114. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 115. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE); 116. m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); 117. CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate); 118. if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE)) 119. for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++); 120. if (rate[0].close > 0) 121. { 122. if (GetInfoTicks().ModePlot == PRICE_EXCHANGE) 123. m_Infos.tick[0].last = rate[0].close; 124. else 125. { 126. m_Infos.tick[0].bid = rate[0].close; 127. m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick); 128. } 129. m_Infos.tick[0].time = rate[0].time; 130. m_Infos.tick[0].time_msc = rate[0].time * 1000; 131. }else 132. m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay]; 133. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 134. } 135. //+------------------------------------------------------------------+ 136. void AdjustPositionToReplay(void) 137. { 138. int nPos, nCount; 139. 140. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return; 141. nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); 142. for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); 143. if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1); 144. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 145. CreateBarInReplay(false); 146. } 147. //+------------------------------------------------------------------+ 148. public : 149. //+------------------------------------------------------------------+ 150. C_Replay() 151. :C_ConfigService() 152. { 153. Print("************** Market Replay Service **************"); 154. srand(GetTickCount()); 155. SymbolSelect(def_SymbolReplay, false); 156. CustomSymbolDelete(def_SymbolReplay); 157. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 158. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0); 159. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0); 160. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0); 161. CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); 162. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8); 163. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TICKS_BOOKDEPTH, 1); 164. SymbolSelect(def_SymbolReplay, true); 165. m_Infos.CountReplay = 0; 166. m_IndControl.Handle = INVALID_HANDLE; 167. m_IndControl.Mode = C_Controls::ePause; 168. m_IndControl.Position = 0; 169. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; 170. } 171. //+------------------------------------------------------------------+ 172. ~C_Replay() 173. { 174. SweepAndCloseChart(); 175. IndicatorRelease(m_IndControl.Handle); 176. SymbolSelect(def_SymbolReplay, false); 177. CustomSymbolDelete(def_SymbolReplay); 178. Print("Finished replay service..."); 179. } 180. //+------------------------------------------------------------------+ 181. bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate) 182. { 183. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0) 184. return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket."); 185. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0) 186. return MsgError("Asset configuration is not complete, need to declare the ticket value."); 187. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0) 188. return MsgError("Asset configuration not complete, need to declare the minimum volume."); 189. SweepAndCloseChart(); 190. m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1); 191. if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl")) 192. Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl"); 193. else 194. Print("Apply template: ", szNameTemplate, ".tpl"); 195. 196. return true; 197. } 198. //+------------------------------------------------------------------+ 199. bool InitBaseControl(const ushort wait = 1000) 200. { 201. Sleep(wait); 202. AdjustViewDetails(); 203. Print("Loading Control Indicator..."); 204. if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 205. ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle); 206. Print("Waiting for Mouse Indicator..."); 207. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200); 208. UpdateIndicatorControl(); 209. 210. return def_CheckLoopService; 211. } 212. //+------------------------------------------------------------------+ 213. bool LoopEventOnTime(void) 214. { 215. int iPos, iCycles; 216. MqlBookInfo book[1]; 217. ENUM_BOOK_TYPE typeMsg; 218. 219. book[0].price = 1.0; 220. book[0].volume = 1; 221. book[0].type = BOOK_TYPE_BUY_MARKET; 222. CustomBookAdd(def_SymbolReplay, book, 1); 223. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 224. { 225. UpdateIndicatorControl(); 226. Sleep(200); 227. } 228. m_MemoryData = GetInfoTicks(); 229. AdjustPositionToReplay(); 230. iPos = iCycles = 0; 231. book[0].type = BOOK_TYPE_BUY; 232. CustomBookAdd(def_SymbolReplay, book, 1); 233. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 234. { 235. if (m_IndControl.Mode == C_Controls::ePause) return true; 236. 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); 237. if ((typeMsg = (iPos >= 60000 ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY)) != book[0].type) 238. { 239. book[0].type = typeMsg; 240. CustomBookAdd(def_SymbolReplay, book, 1); 241. } 242. CreateBarInReplay(true); 243. while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause)) 244. { 245. Sleep(195); 246. iPos -= 200; 247. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 248. UpdateIndicatorControl(); 249. iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1); 250. } 251. } 252. 253. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 254. } 255. }; 256. //+------------------------------------------------------------------+ 257. #undef def_SymbolReplay 258. #undef def_CheckLoopService 259. #undef def_MaxSlider 260. //+------------------------------------------------------------------+
Código fuente del archivo C_Replay.mqh
Se trata de una falla bastante curiosa, pero inofensiva.
Muy bien. Aunque este sistema funciona maravillosamente bien, como se puede ver y observar en la imagen, hay un problema. Incluso si en el vídeo no mostré este detalle, creo que si observas y reflexionas un poco, notarás que hay un problema. Pero antes de decirte cuál es el fallo, comprueba si has comprendido cómo funciona MetaTrader 5. Si no tienes claro cómo funciona MetaTrader 5 en diferentes tipos de escenarios, es muy probable que no hayas identificado el fallo de esta programación. El defecto radica en que no puedes CAMBIAR EL MARCO TEMPORAL DEL GRÁFICO durante todo el uso de la aplicación de repetición/simulación.
Ahora debes estar preguntándote: ¿cómo es eso? ¿Cómo no puedo cambiar el timeframe del gráfico? ¿Qué ocurre si intento modificarlo? Lo que sucederá —y esto es completamente seguro— es que se activará el error del sistema. Sin embargo, este fallo, que explicaré cómo corregir enseguida, solo ocurre en escenarios muy específicos. Solo lo activarás si el servicio de repetición/simulación está en modo de ejecución. Si está en modo pausa o si el sistema detecta que el símbolo está en subasta debido al número de transacciones que hemos definido, el fallo no se activará. Una vez más, solo se producirá si estamos en modo play y los ticks se están añadiendo al gráfico.
Ahora vuelvo a preguntarte: tú, que has estado siguiendo estos artículos sobre cómo desarrollar un sistema de repetición/simulación, ¿tienes alguna idea de por qué ocurre este fallo y cuál es exactamente? Si la respuesta es afirmativa, ¡genial! Significa que has estado estudiando y dedicándote a entender cada vez más el MQL5 y el funcionamiento de MetaTrader 5. Si la respuesta es no, significa que aún estás en una etapa inicial de aprendizaje. No te sientas mal por ello; al contrario, que esto sea una motivación para estudiar aún más.
Veamos ahora cuál es este fallo, que a pesar de existir es completamente inofensivo. Solo se activará si estás en modo de prueba y la liquidez es suficiente para que no se dispare la condición de subasta. Al cambiar el timeframe del gráfico, este fallo provoca algo curioso. El error es exactamente este: si se cumplen las condiciones especificadas anteriormente, en cuanto modifiques el timeframe del gráfico, el indicador del mouse empezará a mostrar que el mercado está cerrado. Además, no se mostrará el tiempo restante de la barra. Tendrás que activar el modo de pausa y luego volver a activar el modo de reproducción.
Puede que ahora estés completamente confundido. ¿Cómo es posible? ¿Si cambio el timeframe del gráfico, el indicador del mouse mostrará que el mercado está cerrado? Esto no tiene sentido. De hecho, debo concordar contigo, estimado lector. Pero si tienes un poco más de experiencia, sabrás que cuando cambiamos el timeframe del gráfico, MetaTrader 5 elimina todos los indicadores y otros elementos del gráfico. Luego vuelve a abrir el gráfico y traza las barras siguiendo el criterio del nuevo timeframe. Así es como funciona MetaTrader 5. Por lo tanto, cuando el indicador del mouse se vuelva a colocar en el gráfico, en realidad mostrará su configuración inicial. Para saber cuál es esta configuración, basta con observar el fragmento de código que se muestra a continuación, extraído del indicador del mouse:
27. //+------------------------------------------------------------------+ 28. int OnInit() 29. { 30. Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04); 31. if (_LastError >= ERR_USER_ERROR_FIRST) return INIT_FAILED; 32. MarketBookAdd((*Study).GetInfoTerminal().szSymbol); 33. OnBookEvent((*Study).GetInfoTerminal().szSymbol); 34. m_Status = C_Study::eCloseMarket; 35. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 36. ArrayInitialize(m_Buff, EMPTY_VALUE); 37. 38. return INIT_SUCCEEDED; 39. } 40. //+------------------------------------------------------------------+
Fragmento de indicador de mouse
Ahora observa lo siguiente: en la línea 34 de este fragmento indicamos el valor del estado. Este indicará que el mercado está cerrado. Sin embargo, siguen llegando ticks desde el servicio de repetición/simulación. Aquí es precisamente donde radica el problema. Como puedes ver, es inofensivo y no causa grandes problemas, solo es un inconveniente que, para solucionarlo, nos obliga a pausar el servicio y luego reanudarlo, momento en el cual todo regresa a la normalidad.
Podrías pensar en mil y una formas de resolverlo. Sin embargo, la solución que voy a mostrar es más inusual de lo que puedas imaginar. Pero antes de mostrarte la solución, ¿puedes decirme por qué el fallo desaparece al dar pausa y luego play? Si no lo sabes, vamos a ver por qué esta acción hace que el fallo desaparezca. Para comprenderlo, debemos consultar el código del archivo de encabezado C_Replay.mqh, específicamente la función LoopEventOnTime. Cuando ocurre el fallo, el indicador del mouse nos informa de que el mercado está cerrado. Al activar el modo de pausa, la línea 235 nos saca del bucle que comienza en la línea 233. Esto hace que la función vuelva al código principal. Dado que el retorno de la función es verdadero, el código principal del servicio volverá a llamar a la función LoopEventOnTime. En este punto, la línea 222 llama para añadir un evento personalizado al libro. Este evento utiliza el valor indicado en la línea 221, por lo que el indicador del mouse cambiará la información mostrada, pasando de «mercado cerrado» a «mercado en subasta». Cuando tú o el usuario vuelvan a habilitar el servicio dando play, la línea 232 hará una nueva llamada y añadirá otro evento personalizado al libro. Pero esta vez, el valor de la constante utilizada será el definido en la línea 231, que permitirá que el indicador del mouse vuelva a mostrar el tiempo restante de la barra actual.
Por este motivo, cuando el fallo ocurre debido al cambio de timeframe del gráfico, basta con pausar y luego reanudar el servicio para que todo vuelva a la normalidad.
Sin embargo, aunque este fallo no afecta tanto al indicador del mouse, resulta molesto en lo que respecta a otro indicador: el indicador de control. En este caso, no hay mucho que hacer. Debemos esperar a que falle la prueba realizada en la línea 42 del código de la línea de encabezado C_Replay.mqh. ¿Y por qué necesitamos que esta prueba falle en la línea 42 para que el indicador de control vuelva a ser visible en el gráfico? La razón es que solo cuando esta prueba falla, se ejecuta la línea 54. Es necesario que esta línea se ejecute para que el indicador de control pueda actualizarse. Una vez que se realiza esta actualización, el indicador de control volverá a trazarse en el gráfico, siempre que estuviera oculto por cualquier motivo.
Muy bien. Ahora debes haberte dado cuenta de que la situación se complica. Si el fallo detectado en el indicador del mouse podía resolverse simplemente presionando el botón de control para cambiar del modo de reproducción al modo de pausa y luego volver a dar play, ¿qué sucede si el indicador de control no está visible? ¿Cómo podremos solucionar el fallo del indicador del mouse? Esto se debe a que no podremos pausar la aplicación de manera que el servicio deje de enviar ticks. Pero si el servicio no deja de enviar ticks, tendremos que esperar a que falle la prueba de la línea 42. Y esto solo ocurrirá cuando se haya añadido al gráfico una cantidad determinada de ticks. Vaya, ahora tenemos un verdadero problema.
Entonces podrías pensar: «Todo lo que tengo que hacer es observar el timeframe del gráfico del servicio». Si este se modifica, haré que los indicadores de control y de mouse reciban datos adecuados para que se reinicialicen correctamente. De acuerdo. Pero ¿sabes cómo hacer que el servicio detecte que ha habido un cambio en el timeframe del gráfico? Tal vez exista alguna forma. Pero te aseguro que será mucho más complicada, laboriosa y difícil de implementar que la solución que se mostrará. No en este artículo, sino en el próximo. Quiero que intentes encontrar una solución a este problema, estimado lector.
Conclusión
En este artículo, mostré cómo puedes combinar un código utilizado en el artículo anterior para probar y verificar el funcionamiento del sistema de mensajes personalizados del libro con un servicio que se ha estado implementando durante algún tiempo. También demostré que, a menudo, los fallos de una implementación que parecen inofensivos pueden causarnos grandes dolores de cabeza.
Sin embargo, también dejé —y estoy seguro de que muchos se preguntan cómo lo resolveré— el desafío de cómo resolveré el problema del timeframe. Es decir, hacer que el servicio sea capaz de detectar que el timeframe del gráfico que está monitorizando ha cambiado de alguna manera. Mostraré este tipo de solución en el próximo artículo, ya que no quiero dejarte completamente perdido intentando entender por qué las cosas funcionan o no.
Ser programador consiste precisamente en eso: encontrar una solución cuando nadie más puede verla. Los problemas siempre existirán. Si no existieran, programar no tendría sentido ni sería divertido. Por eso, cuantos más problemas haya, mejor. Cuanto más difíciles sean, mejor, porque nos obligan a pensar fuera de lo común. Nos sacan de la rutina de hacer siempre las mismas cosas de la misma manera.
Nos vemos en el próximo artículo. Te propongo que intentes pensar en una solución para el problema del cambio de timeframe antes de leerlo.
Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/12335
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.





- 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