
Desarrollo de un sistema de repetición (Parte 65): Presionando play en el servicio (VI)
Introducción
En el artículo anterior, "Desarrollo de un sistema de repetición (Parte 64): Presionando play en el servicio (V)", resolvimos dos fallos de la aplicación de repetición/simulador. Sin embargo, no lo solucionamos todo. Así que no podíamos avanzar hacia nuevas metas a partir de este artículo. Todavía tenemos que resolver algunos pequeños problemas que persisten. Dichos problemas ya no existían cuando se utilizaban las variables globales del terminal. Sin embargo, como dejamos de usarlas y optamos por otras técnicas y medios para que la aplicación de repetición/simulación funcionara, debemos adaptar y construir una nueva implementación. No obstante, como tú, estimado lector, debes haber notado, no hemos comenzado desde cero. De hecho, estamos adaptando y ajustando el código para que no se pierda el trabajo realizado durante el uso de las variables globales del terminal.
Con esto, ya estamos casi en el mismo punto de funcionalidad que antes. Sin embargo, para llegar al mismo nivel, debemos resolver algunos detalles más. Intentaré hacerlo en este artículo, ya que estas cuestiones son relativamente sencillas. Esto es diferente de lo que fue corregir el fallo de volcado de memoria, donde el problema era bastante complejo y fue necesario entrar en detalles. Por esta razón, expliqué por qué se producía el fallo, incluso cuando el código parecía absolutamente correcto. Si en lugar de explicar simplemente hubiera mostrado la línea que necesitaba ser añadida, muchos se habrían quedado desconcertados. Otra opción habría sido corregir el código sin explicar nada. Eso sí que sería muy molesto, ya que nunca te habrías encontrado con estos problemas y tendrías una falsa sensación de seguridad de que no existen. Y, cuando ocurrieran, te frustrarías por no tener a quién preguntar o pensarías que eres el peor profesional. No quiero que nadie piense así. Porque incluso los profesionales más expertos cometen errores. Aunque no los veas, siempre ocurren. Pero el profesional los detecta rápidamente y los corrige cuanto antes. Por ello, deseo y anhelo que todos los aspirantes se conviertan realmente en verdaderos profesionales. Y no simples profesionales, sino grandes y excelentes profesionales en su campo. Dicho esto, comencemos resolviendo el primer problema pendiente.
Añadimos la función de avance rápido (Modelo básico)
Esta función ya existía en el pasado y se implementó durante la fase en que se usaban las variables globales del terminal. Pero como ya no utilizamos dichas variables, necesitamos adaptar el código para que el avance rápido funcione correctamente. Mantendré la misma filosofía de avance rápido que se utilizó anteriormente. De este modo, podrás comprender mucho más fácilmente cómo se adaptará el código antiguo para que la nueva implementación pueda aprovechar esta funcionalidad.
Para empezar, debemos realizar una pequeña modificación en el código mostrado en el artículo anterior. El objetivo de esta modificación es asegurar que el indicador de control funcione correctamente. Puedes verla en el siguiente fragmento:
35. //+------------------------------------------------------------------+ 36. inline void UpdateIndicatorControl(void) 37. { 38. double Buff[]; 39. 40. if (m_IndControl.Handle == INVALID_HANDLE) return; 41. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 42. { 43. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 44. m_IndControl.Memory.dValue = Buff[0]; 45. if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState) 46. if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) 47. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 48. }else if (m_IndControl.Mode == C_Controls::ePause) 49. { 50. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 51. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 52. m_IndControl.Memory._8b[7] = 'D'; 53. m_IndControl.Memory._8b[6] = 'M'; 54. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 55. } 56. } 57. //+------------------------------------------------------------------+
Fragmento de código del archivo C_Replay.mqh
El cambio, o mejor dicho, la adición, se realizó precisamente en la línea 48. ¿Y por qué fue necesario? La razón está en el código de la función LoopEventOnTime. Bueno, pero ahora parece confuso. ¿Por qué realizar el cambio en el procedimiento UpdateIndicatorControl si la razón para dicho cambio está en lo que ocurre dentro de la función LoopEventOnTime? No tiene sentido. De hecho, no tendría sentido si no fuera por el hecho de que la función LoopEventOnTime lee y escribe mensajes para acceder y modificar el indicador de control. Sin embargo, si la prueba que aparece en la línea 48 no existe, ocurrirá algo extraño cuando intentes avanzar en el tiempo y luego reproducir. Esto ocurre antes de implementar el procedimiento de avance.
Si avanzas en el tiempo y reproduces, te resultará imposible enviar un comando de pausa al servicio. ¿Qué locura es esta? Bastaría con presionar el botón de pausa y el servicio recibirá la actualización. Sí, efectivamente, el sistema se pausará. Sin embargo, esto no tendrá un efecto inmediato. ¿Y por qué? Por la línea 41. El problema es que el búfer del indicador de control estará en una posición, mientras que la memoria del búfer estará en otra. Así, si inicias el servicio, luego presionas reproducir y después intentas avanzar en el tiempo, no ocurrirá nada incorrecto. Pero si pausas el servicio, intentas avanzar en el tiempo y no existe la prueba presente en la línea 48, la barra de bloqueo del indicador de control seguirá el desplazamiento que estés haciendo. Esto impedirá que el usuario ajuste manualmente la posición.
Si inicias el servicio de repetición/simulación, avanzas una única posición y luego presionas "reproducir", no podrás detener el servicio mediante el botón de pausa. Este solo se detendrá realmente cuando la prueba en la línea 41 sea verdadera y el indicador indique que el modo de pausa está seleccionado. Esto podría tardar un buen rato. Tal vez la explicación parezca un poco confusa, pero es necesario entender que hay tres situaciones diferentes que manejar. Cada una tiene sus problemas debido a que la función LoopEventOnTime está constantemente leyendo y enviando mensajes al indicador de control durante el tiempo normal de ejecución de la aplicación de repetición/simulación.
Sin embargo, al añadir una simple prueba implementada en la línea 48, como puedes observar en el fragmento, todo se resuelve y se eliminan los problemas relacionados con la función LoopEventOnTime. De este modo, podemos concentrarnos en implementar el avance rápido.
Implementar el avance rápido no es una tarea compleja. En realidad, podemos hacerlo simplemente añadiendo los siguientes fragmentos de código al archivo 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. //+------------------------------------------------------------------+ 014. class C_Replay : public C_ConfigService 015. { 016. private : ... 035. //+------------------------------------------------------------------+ 036. inline void UpdateIndicatorControl(void) 037. { 038. double Buff[]; 039. 040. if (m_IndControl.Handle == INVALID_HANDLE) return; 041. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 042. { 043. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 044. m_IndControl.Memory.dValue = Buff[0]; 045. if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState) 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 if (m_IndControl.Mode == C_Controls::ePause) 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. //+------------------------------------------------------------------+ ... 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) CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 092. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 093. } 094. m_Infos.CountReplay++; 095. } 096. //+------------------------------------------------------------------+ ... 123. //+------------------------------------------------------------------+ 124. void AdjustPositionToReplay(void) 125. { 126. int nPos; 127. 128. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxPosSlider * 1.0) / m_MemoryData.nTicks)) return; 129. nPos = (int)(m_MemoryData.nTicks * ((m_IndControl.Position * 1.0) / (def_MaxPosSlider + 1))); 130. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 131. CreateBarInReplay(false); 132. } 133. //+------------------------------------------------------------------+ 134. public : 135. //+------------------------------------------------------------------+ ... 200. //+------------------------------------------------------------------+ 201. bool LoopEventOnTime(void) 202. { 203. int iPos; 204. 205. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 206. { 207. UpdateIndicatorControl(); 208. Sleep(200); 209. } 210. m_MemoryData = GetInfoTicks(); 211. AdjustPositionToReplay(); 212. iPos = 0; 213. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 214. { 215. if (m_IndControl.Mode == C_Controls::ePause) return true; 216. 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); 217. CreateBarInReplay(true); 218. while ((iPos > 200) && (def_CheckLoopService)) 219. { 220. Sleep(195); 221. iPos -= 200; 222. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxPosSlider) / m_MemoryData.nTicks); 223. UpdateIndicatorControl(); 224. } 225. } 226. 227. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 228. } 229. //+------------------------------------------------------------------+ 230. }; 231. //+------------------------------------------------------------------+ 232. #undef macroRemoveSec 233. #undef def_SymbolReplay 234. #undef def_CheckLoopService 235. //+------------------------------------------------------------------+
Fragmentos de código de la clase C_Replay.mqh
Observa que en el código mostrado arriba, con fragmentos del archivo C_Replay.mqh, se incluyen todos los pasos necesarios para implementar un avance rápido básico. Hago hincapié en "básico" porque hay pequeños detalles que necesitaré explicar. Sin embargo, es importante que comprendas este modelo básico, ya que es la base para la implementación completa que realizaremos pronto. Observa cómo se refleja aquí lo explicado previamente en la línea 48. Fíjate en la línea 207. Esta línea es precisamente la que causa problemas en el indicador de control si la prueba de la línea 48 no existiera. Puedes comprobar los problemas deshabilitando la prueba de la línea 48 y también la de la línea 211. Pero, como todo ya está funcionando en el indicador de control, lo dejaremos tal cual. Ahora veremos cómo se consigue el avance rápido en esta versión básica.
Cuando el código del servicio llama a la función LoopEventOnTime, el modo de pausa se activa. Por tanto, el bucle que comienza en la línea 205 y termina en la línea 209 se ejecutará de forma indefinida, lo que permitirá al usuario ajustar la posición en el indicador de control. Está bien. Cuando el usuario pulse el botón de reproducción del servicio de repetición/simulación, capturaremos los datos del símbolo en la línea 210 para acceder a ellos más rápidamente. A continuación, en la línea 211, hacemos una llamada al procedimiento de la línea 124, que avanzará rápidamente desde la posición actual hasta la posición requerida por el usuario.
En la línea 128 verificamos si la posición indicada por el usuario es la que estamos utilizando actualmente. Si es el caso, el procedimiento de avance termina y se ejecuta el bucle entre las líneas 213 y 225. Pero si la posición requerida por el usuario está más adelante, necesitamos determinar su ubicación, para lo cual hacemos un pequeño cálculo en la línea 129. Hasta aquí, nada extraordinario. Pero en la línea 130 ocurre algo significativo, ya que entramos en un bucle que ejecuta la línea 131, y observa que estamos pasando un parámetro false al procedimiento de creación de barras. Este procedimiento se ejecutará lo más rápido posible hasta que el contador de posición sea igual al indicador de desplazamiento calculado. Sin embargo, como podrás observar fácilmente, el procedimiento llamado en la línea 131 ejecutará el código entre las líneas 69 y 95. Observa que estamos pasando un parámetro false al procedimiento de creación de barras. Por esta razón, la prueba presente en la línea 91 evitará que se ejecute la llamada a CustomTicksAdd. De este modo, se impedirá el envío de ticks al símbolo. Sin embargo, la línea 92 trazará las barras en el gráfico a medida que se vayan construyendo, una por una.
Todo el proceso funciona maravillosamente bien, excepto por dos puntos que provocan un retraso considerable en la ejecución del avance rápido. Si el usuario solicita un avance lo suficientemente grande, podrá observar la creación de las barras directamente en el gráfico. Las líneas 75 y 92, además de la llamada al procedimiento, son las responsables de este retraso. Sin embargo, este último no tiene un impacto significativo, por lo que podemos ignorar su coste. Las otras dos líneas, especialmente la 75, son las que causan un mayor retraso.
Muy bien, esta es la forma básica de hacerlo. No obstante, hay una manera considerablemente más rápida de lograr lo mismo. Sin embargo, si quieres ver cómo se crean las barras, el método más sencillo es el que acabamos de implementar. Si modificas el código del archivo C_Replay.mqh añadiendo lo que acabamos de ver, el avance rápido ya funcionará. Por lo tanto, queda a tu criterio si usas este método básico o uno más elaborado, aunque este último es notablemente más rápido en la ejecución del avance rápido. Muy bien, para diferenciar, pasemos a una nueva sección.
Añadimos la función de avance rápido (Modelo ágil)
Si observas con atención el código, verás que en la clase C_FileTicks se están construyendo las barras de un minuto durante la carga de los ticks. Entonces, ¿por qué rehacemos esto? Simplemente usaremos las barras ya creadas para aproximarnos lo máximo posible al punto ideal y después, si lo deseamos, avanzaremos hasta alcanzar el punto perfecto calculado con precisión. Esto hará que el procedimiento de avance rápido sea mucho más veloz, dándonos la impresión de que es instantáneo.
Sin embargo, no todo es magnífico y perfecto desde el principio. Para lograr la agilidad que buscamos, será necesario añadir un tipo de índice o enlace que permita realizar una búsqueda rápida. Para ello, utilizaremos una parte de la estructura de datos que, en el contexto de repetición/simulador, no es muy útil: el spread dentro de la estructura MqlRates. Para entender las modificaciones necesarias, consulta el fragmento siguiente:
126. //+------------------------------------------------------------------+ 127. bool BarsToTicks(const string szFileNameCSV, int MaxTickVolume) 128. { 129. C_FileBars *pFileBars; 130. C_Simulation *pSimulator = NULL; 131. int iMem = m_Ticks.nTicks, 132. iRet = -1; 133. MqlRates rate[1]; 134. MqlTick local[]; 135. bool bInit = false; 136. 137. pFileBars = new C_FileBars(szFileNameCSV); 138. ArrayResize(local, def_MaxSizeArray); 139. Print("Converting bars to ticks. Please wait..."); 140. while ((*pFileBars).ReadBar(rate) && (!_StopFlag)) 141. { 142. if (!bInit) 143. { 144. m_Ticks.ModePlot = (rate[0].real_volume > 0 ? PRICE_EXCHANGE : PRICE_FOREX); 145. pSimulator = new C_Simulation(SetSymbolInfos()); 146. bInit = true; 147. } 148. ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary); 149. m_Ticks.Rate[++m_Ticks.nRate] = rate[0]; 150. if (pSimulator == NULL) iRet = -1; else iRet = (*pSimulator).Simulation(rate[0], local, MaxTickVolume); 151. if (iRet < 0) break; 152. rate[0].spread = m_Ticks.nTicks; 153. for (int c0 = 0; c0 <= iRet; c0++) 154. { 155. ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray); 156. m_Ticks.Info[m_Ticks.nTicks++] = local[c0]; 157. } 158. m_Ticks.Rate[++m_Ticks.nRate] = rate[0]; 159. } 160. ArrayFree(local); 161. delete pFileBars; 162. delete pSimulator; 163. m_Ticks.bTickReal = false; 164. 165. return ((!_StopFlag) && (iMem != m_Ticks.nTicks) && (iRet > 0)); 166. } 167. //+------------------------------------------------------------------+ 168. datetime LoadTicks(const string szFileNameCSV, const bool ToReplay, const int MaxTickVolume) 169. { 170. int MemNRates, 171. MemNTicks, 172. nDigits, 173. nShift; 174. datetime dtRet = TimeCurrent(); 175. MqlRates RatesLocal[], 176. rate; 177. MqlTick TicksLocal[]; 178. bool bNew; 179. 180. MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate); 181. nShift = MemNTicks = m_Ticks.nTicks; 182. if (!Open(szFileNameCSV)) return 0; 183. if (!ReadAllsTicks()) return 0; 184. rate.time = 0; 185. nDigits = SetSymbolInfos(); 186. m_Ticks.bTickReal = true; 187. for (int c0 = MemNTicks, c1, MemShift = nShift; c0 < m_Ticks.nTicks; c0++, nShift++) 188. { 189. if (nShift != c0) m_Ticks.Info[nShift] = m_Ticks.Info[c0]; 190. if (!BuildBar1Min(c0, rate, bNew)) continue; 191. if (bNew) 192. { 193. if ((m_Ticks.nRate >= 0) && (ToReplay)) if (m_Ticks.Rate[m_Ticks.nRate].tick_volume > MaxTickVolume) 194. { 195. nShift = MemShift; 196. ArrayResize(TicksLocal, def_MaxSizeArray); 197. C_Simulation *pSimulator = new C_Simulation(nDigits); 198. if ((c1 = (*pSimulator).Simulation(m_Ticks.Rate[m_Ticks.nRate], TicksLocal, MaxTickVolume)) > 0) 199. nShift += ArrayCopy(m_Ticks.Info, TicksLocal, nShift, 0, c1); 200. delete pSimulator; 201. ArrayFree(TicksLocal); 202. if (c1 < 0) return 0; 203. } 204. rate.spread = MemShift; 205. MemShift = nShift; 206. ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary); 207. }; 208. m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate; 209. } 210. if (!ToReplay) 211. { 212. ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates)); 213. ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0); 214. CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates)); 215. dtRet = m_Ticks.Rate[m_Ticks.nRate].time; 216. m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates); 217. m_Ticks.nTicks = MemNTicks; 218. ArrayFree(RatesLocal); 219. }else m_Ticks.nTicks = nShift; 220. 221. return dtRet; 222. }; 223. //+------------------------------------------------------------------+
Fragmento de código del archivo C_FileTicks.mqh
Este fragmento muestra exactamente las líneas que deben modificarse en el archivo C_FileTicks.mqh. Observa que la línea 149 deberá eliminarse y moverse a una nueva posición, concretamente la línea 158. ¿Por qué hacer esto? Tranquilo, estimado lector, todo se explicará a su debido tiempo. Ahora observa que se ha añadido una nueva línea, la 152. Presta atención al siguiente hecho: la función aquí está convirtiendo barras en ticks mediante la simulación. Por lo tanto, en la línea 152 se almacena el valor de índice donde comenzará una nueva barra. Este valor se guarda en el spread de la barra.
Ahora pasemos a la siguiente función, donde haremos algo muy similar. Observa que solo se ha añadido la línea 204, donde se leerán los ticks. Pero debes recordar lo siguiente: aunque estemos leyendo los ticks desde un archivo, podría ser necesario descartarlos y, en su lugar, simular los que se van a utilizar. Esto lo vimos en los artículos anteriores, donde expliqué las razones para hacer algo así. Por esta razón, el valor que realmente nos interesa es el valor de memoria, es decir, MemShift, que nos indica dónde comienza una nueva barra. Al igual que en la función anterior, aquí almacenamos este valor en el spread de la estructura de rates.
Muy bien, pero ¿por qué lo hacemos? ¿Cuál es la verdadera utilidad? Pues bien, ahora entendamos algo. Durante la simulación de las barras en ticks, que se realiza en la función anterior, sabemos exactamente cuándo y dónde comenzará cada una. Esto es posible porque contamos con un índice que apunta precisamente a esta posición inicial de la barra antes de que se simule. De la misma forma, en la función que carga los ticks desde un archivo, transformamos los ticks en una barra en la línea 190, tal como se haría en la clase C_Replay. Por esta razón, con cada nueva barra creada aquí, sabemos exactamente dónde comenzará. Por tanto, no necesitamos dejar esta tarea a la clase C_Replay, que tendría que buscar el punto de inicio de la barra. Y dado que el valor del spread no tiene ninguna utilidad en este momento para la aplicación de repetición/simulación, utilizamos este valor, que en principio parece inútil, para almacenar algo valioso para nosotros: el punto exacto donde comienza cada barra.
Si observas el procedimiento de avance rápido descrito en el tema anterior, verás que en la línea 129 se está realizando un cálculo. Este cálculo nos indica exactamente el índice al que debemos saltar para avanzar rápidamente en la repetición o simulación. ¿Empiezas a entender la importancia de este valor, que se genera durante la carga de los ticks? Este valor es muy importante y te permitirá avanzar más rápido. Esto se debe a que no será necesario reconstruir barra por barra. Podemos ir directamente al punto correcto y luego solicitar a MetaTrader 5 que muestre y actualice las barras entre ambos puntos. Es decir, ahora tenemos una nueva forma de hacer las cosas.
Para utilizar estos datos de forma útil, deberemos realizar algunos cambios en el código de la clase C_Replay. Sin embargo, estas modificaciones serán mínimas en comparación con lo tratado en el tema anterior. Por tanto, deberás añadir los siguientes fragmentos al archivo C_Replay.mqh, que puedes ver a continuación.
013. #define def_MaxSlider (def_MaxPosSlider + 1) ... 124. //+------------------------------------------------------------------+ 125. void AdjustPositionToReplay(void) 126. { 127. int nPos, nCount; 128. 129. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return; 130. nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); 131. for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); 132. if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1); 133. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 134. CreateBarInReplay(false); 135. } 136. //+------------------------------------------------------------------+
Fragmento de código del archivo C_Replay.mqh
Como puedes ver, fue necesario añadir una nueva línea al archivo de cabecera C_Replay.mqh. Esta es la línea 13, donde realizamos un pequeño ajuste en el cálculo del desplazamiento general. Como siempre, todo tiene su razón de ser. Si no realizas este ajuste, aparecerá otro problema o tendrás que hacer las cosas de otra manera. Para evitar rehacer gran parte del modelado de los datos, prefiero ajustar simplemente esto aquí. ¿Por qué es importante este ajuste en la línea 13? La razón es que, sin él, estarás a merced de un desplazamiento en el que la última posición del control deslizante del indicador de control finalizará la simulación o repetición en curso. El simple hecho de añadir una posición aquí permite que tengamos unos pocos ticks adicionales que pueden aplicarse al gráfico. Esto es muy beneficioso y garantiza un cierre ordenado del simulador o repetición en uso.
Sin embargo, lo que realmente nos importa se encuentra en las líneas 131 y 132. Como puedes observar, con solo añadir estas dos líneas se consigue avanzar mucho más rápido que antes. Aun así, es posible que queden algunos ticks que no estén en la posición deseada y estos deberán añadirse como se hacía antes. Estos ticks utilizarán el bucle presente en la línea 133, pero como en la mayoría de los casos serán muy pocos, el proceso será bastante rápido.
En cualquier caso, lo que hacemos aquí es lo siguiente: en la línea 131, buscamos el índice de la barra cuyo valor sea inmediatamente inferior al punto donde queremos posicionarnos. Esto se realiza completamente dentro del bucle for. Aunque esta construcción pueda parecer poco convencional para algunos, es perfectamente funcional. El hecho de que parezca inusual se debe a que incluyo dentro de la declaración del bucle la asignación del valor que se colocará en el contador de repetición, CountReplay. Sin embargo, no hay ninguna razón que impida realizar esta asignación fuera del bucle.
En la línea 132, realmente necesitamos comprobar el valor de nCount. Esto es porque no quiero correr el riesgo de que la llamada a la biblioteca de MQL5 CustomRatesUpdate no interprete correctamente los datos o barras que se van a utilizar. El resto de la función ya se explicó en el tema anterior. Lo interesante de estos cambios es que, debido a ellos, el código final del archivo C_Replay.mqh tuvo que modificarse nuevamente. Dado que las modificaciones son sencillas y no necesitan mayor explicación, mostraré el código final, al menos hasta este momento. Por lo tanto, el código completo puede verse a continuación.
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) CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 092. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 093. } 094. m_Infos.CountReplay++; 095. } 096. //+------------------------------------------------------------------+ 097. void AdjustViewDetails(void) 098. { 099. MqlRates rate[1]; 100. 101. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 102. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 103. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE); 104. m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); 105. CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate); 106. if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE)) 107. for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++); 108. if (rate[0].close > 0) 109. { 110. if (GetInfoTicks().ModePlot == PRICE_EXCHANGE) 111. m_Infos.tick[0].last = rate[0].close; 112. else 113. { 114. m_Infos.tick[0].bid = rate[0].close; 115. m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick); 116. } 117. m_Infos.tick[0].time = rate[0].time; 118. m_Infos.tick[0].time_msc = rate[0].time * 1000; 119. }else 120. m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay]; 121. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 122. } 123. //+------------------------------------------------------------------+ 124. void AdjustPositionToReplay(void) 125. { 126. int nPos, nCount; 127. 128. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return; 129. nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); 130. for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); 131. if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1); 132. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 133. CreateBarInReplay(false); 134. } 135. //+------------------------------------------------------------------+ 136. public : 137. //+------------------------------------------------------------------+ 138. C_Replay() 139. :C_ConfigService() 140. { 141. Print("************** Market Replay Service **************"); 142. srand(GetTickCount()); 143. SymbolSelect(def_SymbolReplay, false); 144. CustomSymbolDelete(def_SymbolReplay); 145. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 146. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0); 147. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0); 148. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0); 149. CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); 150. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8); 151. SymbolSelect(def_SymbolReplay, true); 152. m_Infos.CountReplay = 0; 153. m_IndControl.Handle = INVALID_HANDLE; 154. m_IndControl.Mode = C_Controls::ePause; 155. m_IndControl.Position = 0; 156. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; 157. } 158. //+------------------------------------------------------------------+ 159. ~C_Replay() 160. { 161. SweepAndCloseChart(); 162. IndicatorRelease(m_IndControl.Handle); 163. SymbolSelect(def_SymbolReplay, false); 164. CustomSymbolDelete(def_SymbolReplay); 165. Print("Finished replay service..."); 166. } 167. //+------------------------------------------------------------------+ 168. bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate) 169. { 170. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0) 171. return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket."); 172. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0) 173. return MsgError("Asset configuration is not complete, need to declare the ticket value."); 174. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0) 175. return MsgError("Asset configuration not complete, need to declare the minimum volume."); 176. SweepAndCloseChart(); 177. m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1); 178. if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl")) 179. Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl"); 180. else 181. Print("Apply template: ", szNameTemplate, ".tpl"); 182. 183. return true; 184. } 185. //+------------------------------------------------------------------+ 186. bool InitBaseControl(const ushort wait = 1000) 187. { 188. Print("Waiting for Mouse Indicator..."); 189. Sleep(wait); 190. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200); 191. if (def_CheckLoopService) 192. { 193. AdjustViewDetails(); 194. Print("Waiting for Control Indicator..."); 195. if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 196. ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle); 197. UpdateIndicatorControl(); 198. } 199. 200. return def_CheckLoopService; 201. } 202. //+------------------------------------------------------------------+ 203. bool LoopEventOnTime(void) 204. { 205. int iPos; 206. 207. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 208. { 209. UpdateIndicatorControl(); 210. Sleep(200); 211. } 212. m_MemoryData = GetInfoTicks(); 213. AdjustPositionToReplay(); 214. iPos = 0; 215. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 216. { 217. if (m_IndControl.Mode == C_Controls::ePause) return true; 218. 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); 219. CreateBarInReplay(true); 220. while ((iPos > 200) && (def_CheckLoopService)) 221. { 222. Sleep(195); 223. iPos -= 200; 224. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 225. UpdateIndicatorControl(); 226. } 227. } 228. 229. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 230. } 231. }; 232. //+------------------------------------------------------------------+ 233. #undef macroRemoveSec 234. #undef def_SymbolReplay 235. #undef def_CheckLoopService 236. #undef def_MaxSlider 237. //+------------------------------------------------------------------+
Código fuente final del archivo C_Replay.mqh
Actualizamos el tiempo de barra y el porcentaje de la cotización
Este problema es relativamente sencillo de resolver. Solo es necesario enviar mensajes al indicador de mouse para que pueda interpretar y presentar esta información de manera adecuada. El problema ahora es lograr que el indicador nos diga cuánto tiempo falta para que se inicie una nueva barra y cuál es la variación porcentual entre el cierre del día anterior y el valor actual de cotización.
Para simplificar, primero ajustaremos el porcentaje. Si lo deseas, puedes crear tu propio método basándote en lo que mostraré aquí. Siéntete libre de hacerlo como mejor te convenga. Empecemos entonces a entender el problema del porcentaje. Si observas el indicador de posición del mouse, verás que el porcentaje de variación entre el cierre y la cotización actual no muestra valores correctos. Sin embargo, el valor basado en la posición del mouse sí será correcto. ¿Por qué ocurre esta diferencia? Podrías pensar que el problema se debe a que el indicador de mouse no puede identificar dónde terminan los datos previos y dónde comienza la simulación o repetición. Pero no es exactamente eso lo que sucede. El problema es otro. El indicador de mouse sí puede leer e interpretar los datos, como se evidencia en las variaciones mostradas al moverlo. Sin embargo, ocurre algo que confunde al indicador y hace que presente valores erróneos. De vez en cuando, muestra el valor correcto. Este es el problema que debemos resolver.Para solucionarlo, bastará con hacer algo muy simple. Sin embargo, quiero advertirte de que debes evitar en la medida de lo posible utilizar lo que mostraré aquí. El motivo es que, si no tienes cuidado, podrías perder el control del proyecto. Veamos cómo se solucionó el problema.
Lo primero que hay que hacer es modificar el código del indicador de mouse, como se muestra en el siguiente fragmento.09. #property indicator_chart_window 10. #property indicator_plots 0 11. #property indicator_buffers 1 12. //+------------------------------------------------------------------+ 13. double GL_PriceClose; 14. //+------------------------------------------------------------------+ 15. #include <Market Replay\Auxiliar\Study\C_Study.mqh> 16. //+------------------------------------------------------------------+ 17. C_Study *Study = NULL; 18. //+------------------------------------------------------------------+ 19. input color user02 = clrBlack; //Price Line 20. input color user03 = clrPaleGreen; //Positive Study 21. input color user04 = clrLightCoral; //Negative Study 22. //+------------------------------------------------------------------+ 23. C_Study::eStatusMarket m_Status; 24. int m_posBuff = 0; 25. double m_Buff[]; 26. //+------------------------------------------------------------------+ 27. int OnInit() 28. { 29. ResetLastError(); 30. Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04); 31. if (_LastError != ERR_SUCCESS) return INIT_FAILED; 32. if ((*Study).GetInfoTerminal().szSymbol != def_SymbolReplay) 33. { 34. MarketBookAdd((*Study).GetInfoTerminal().szSymbol); 35. OnBookEvent((*Study).GetInfoTerminal().szSymbol); 36. m_Status = C_Study::eCloseMarket; 37. }else 38. m_Status = C_Study::eInReplay; 39. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 40. ArrayInitialize(m_Buff, EMPTY_VALUE); 41. 42. return INIT_SUCCEEDED; 43. } 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. //int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 49. { 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 de código del Indicador de Mouse
Observa que en la línea 13 he añadido una variable. Esta variable es global, no solo porque no está dentro de un procedimiento o función, sino también porque es global por su ubicación y punto de declaración. Esta variable no realiza ninguna acción especialmente compleja. Sin embargo, en la línea 50, recibirá el valor que nos estará proporcionando MetaTrader 5. Nota también que la línea 48, que correspondía a la declaración anterior de la función OnCalculate, ha sido reemplazada por otra. Esto es importante. Ahora podemos utilizar esta variable declarada en la línea 13 para resolver el problema del porcentaje. La próxima modificación debe realizarse en el archivo de cabecera C_Study.mqh. Puedes verla a continuación:
41. //+------------------------------------------------------------------+ 42. void Draw(void) 43. { 44. double v1; 45. 46. if (m_Info.bvT) 47. { 48. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 18); 49. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_TEXT, m_Info.szInfo); 50. } 51. if (m_Info.bvD) 52. { 53. v1 = NormalizeDouble((((GetInfoMouse().Position.Price - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2); 54. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1); 55. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP)); 56. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); 57. } 58. if (m_Info.bvP) 59. { 60. v1 = NormalizeDouble((((GL_PriceClose - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2); 61. v1 = NormalizeDouble((((iClose(GetInfoTerminal().szSymbol, PERIOD_D1, 0) - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2); 62. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1); 63. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP)); 64. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); 65. } 66. } 67. //+------------------------------------------------------------------+
Fragmento de código del archivo C_Study.mqh
Observa que la línea 61, que correspondía al código anterior del indicador, fue modificada por un nuevo código en la línea 60. Nota que aquella variable declarada en el código del indicador se está utilizando aquí. ¿Cómo es posible? Esto se debe a que la hemos declarado como global. Más específicamente, la hemos declarado en el cuerpo del código, de modo que es accesible desde cualquier punto dentro del código que estamos construyendo. Este tipo de práctica suele causar problemas. Por esta razón, debes extremar el cuidado al declarar variables como globales.
Cuando necesito hacer esto por algún motivo, lo hago sabiendo que la variable puede ocasionarme problemas y que debo tener cuidado con ella. Por lo general, la defino antes de los archivos de inclusión y le añado el prefijo GL_. Aunque en la línea 25 hay otra variable con un alcance global, no me preocupa demasiado porque tiene un propósito muy específico y rara vez necesitaré modificarla. Sin embargo, la variable de la línea 13 es algo a lo que debo prestar atención, ya que podrías modificarla inadvertidamente.
De esta manera tan sencilla, resolvemos el problema de los porcentajes que a veces se mostraban de manera errónea. Además, conseguimos ejecutar el código con mayor agilidad. Esto se debe a que ya no necesitamos capturar el valor de cierre mediante la llamada iClose. El propio MetaTrader 5 nos proporciona esta información, por lo que nos ahorramos el esfuerzo de buscarla manualmente.
Conclusión
Aunque nos ha quedado pendiente explicar cómo solucionar el problema relacionado con el tiempo de cierre de la barra en el caso de que estemos usando el simulador o la repetición, hemos logrado grandes avances en este artículo. La aplicación se ha acercado bastante a lo que era cuando utilizábamos variables globales del terminal. Sin embargo, creo que muchos de ustedes se sorprenderán por la cantidad de cosas que desconocían que era posible hacer con MQL5 puro. Pero esto es solo el principio. Aún queda mucho por hacer y cada nueva tarea supondrá un nuevo desafío y aprendizaje.
Aunque todavía no he explicado cómo hacer que el MetaTrader 5 nos informe del tiempo restante de una barra cuando usamos la aplicación de repetición/simulador, lo abordaré al inicio del próximo artículo. No te lo pierdas, porque también empezaremos a ajustar otro aspecto que necesita mejoras para funcionar adecuadamente con lo que ya estamos utilizando.
Lamentablemente, este artículo aún no permite que los usuarios sin conocimientos de programación utilicen la aplicación. Esto se debe a que falta resolver un detalle sobre el tiempo de cierre de la barra. Pero si sabes programar y has podido seguir las explicaciones y hacer las modificaciones que he mostrado, verás que la aplicación de repetición/simulador se comportará como se muestra en el video a continuación. Nos vemos en el próximo artículo de esta serie.
Video de demostración
Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/12265





- 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