
Developing a Replay System (Part 73): An Unusual Communication (II)
Introduction
In the previous article "Developing a Replay System (Part 72): An Unusual Communication (I)" I began to show how you can use an indicator to transmit certain information that would otherwise be impossible to obtain. ]Although I provided a number of explanations in that article, actually implementing the code in our replay/simulator application is not quite that simple. You may be thinking that I'm just being dramatic. That implementing such functionality is quite simple and clear. That I'm just building suspense for effect.
Well, I wish I were simply building suspense around this topic. But the truth is, this really is far more complex than you might initially imagine. I've made a conscious effort to make these articles understandable to all those who really want to learn MQL5. And the current topic, at least up to the time of writing this article, is something I haven't seen anyone explore. Not because it's unthinkable, but because it's at the very least quite exotic and highly uncommon. That's why I'm trying to highlight as many details as possible about how you should proceed when tackling something for which no precedent exists.
Continuing the Implementation
Before diving into the service itself, which will undoubtedly be the most complex part of the entire implementation, let's take a look at the control indicator code. This is because in the previous article, I only introduced the header file C_Controls.mqh. And since there was already a lot of information covered there, I decided to complete the explanation about the control indicator in this article. So, let's start by reviewing its source code.
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. //+------------------------------------------------------------------+
Source code of the control indicator
If you compare this code with the previous ones, you will immediately notice one difference on line 25. Previously, the check for the _LastError variable was done against the ERR_SUCCESS constant. However, doing so was causing a few problems. That's because sometimes the indicator would be placed on the chart while the _LastError variable contained a value different from ERR_SUCCESS.
It took me some time to understand why the initialization was occasionally failing. It's odd because, even after explicitly calling the ResetLastError library function and debugging the code to eliminate any errors, the constructor of the C_Controls class would sometimes return with _LastError still holding a value.
What's even stranger is that the error was often related to another symbol or open chart. So, apparently - let me make this perfectly clear, as I'm not making any claims - there may be leakage of information between different charts. But even when only the custom symbol chart was present, there would still be an error return that had nothing to do with the application. Therefore, I decided to isolate the errors. Only errors set using the SetUserError function will cause the indicator to be removed from the chart.
But what we really need to pay close attention to is the OnCalculate function. Why is this function so important to us? Well, the OnChartEvent function gets triggered at several moments, especially due to mouse movement. And remember, we're using the mouse in our application. However, OnCalculate is called whenever a new tick or quote arrives. Okay, so even if you only use the keyboard or some interface that triggers certain events via keystrokes, you would get calls to OnChartEvent. But with OnCalculate, we can ensure things move along more quickly, or rather, we can ensure that the information in the buffer is as up-to-date as possible. That's why we use OnCalculate for this purpose.
Take a look at line 38. It's simple and performs the same task as line 46. The difference is that line 38 updates the buffer every time the indicator receives a new tick. Even if the mouse has frozen or a mouse event hasn't been triggered. So, while OnChartEvent might only run after a change, OnCalculate is triggered almost constantly.
Now you understand the role of the control indicator. But don't forget that when switching timeframes, the OnDeinit function is executed, removing the indicator from the chart, and then OnInit is called immediately afterward. So, before anything appears on the chart, OnCalculate runs and only after that OnChartEvent is executed. And we need the buffer data to be consistently up-to-date.
Does this mean we're ready to start modifying the C_Replay.mqh header file? Well, we could technically start now. However, I'm not fully confident that all aspiring developers have truly understood how this form of communication works. For that reason, dear reader, I ask for a little more of your patience. If you already know how this works, let's be patient so that those who don't yet understand can also learn what's happening here. That's why we're moving on to the next topic.
Understanding Fast Information Exchange
Most beginners and developers with limited experience in MQL5 will likely assume that exchanging information, or more precisely, reading the indicator buffer from a service is simple. All you need is a handle. And yes, that does work quite well, in fact. But there's a caveat. Or more precisely, a flaw in using a handle in this particular scenario.
At this point, you might be thinking "How can there be a flaw in using a handle? I've always done it, and it's always worked." I'm not here to argue with your logic or experience. Pointless arguments don't get us anywhere. Instead of debating or trying to prove anything through words, let's run a real test. That way, no one can argue, because once something is tested and proven. It becomes an unquestionable fact.
So, to prove that using a handle to access data from an indicator buffer is not a reliable method when accessed via a service - and let me be clear: the issue becomes UNSTABLE when you change the chart timeframe - we'll run a practical experiment. It's important to understand this. The problem lies in switching the timeframe. So here's what we'll do: we'll create two simple code examples to test this. Don't worry, they are easy to follow. Let's start with the first one: the indicator code.
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. //+------------------------------------------------------------------+
Source code of a test indicator
Note that on line 5, we're instructing the compiler that we won't actually be plotting any data. This prevents it from issuing alerts during each new compilation attempt. On line 6, we declare that we'll use a buffer. Then on line 8, we include a header file used in the replay/simulation application. This is important because our goal here is to understand how the replay/simulation system will actually access the data. On line 10, we declare our buffer. Inside the OnInit function, we initialize the buffer and define the name of the indicator. Pay attention to this, as we'll need this information later.
Now, look closely at the body of the OnCalculate function. On line 23, we write the chart timeframe value into the buffer. This is the same value discussed in the previous article, which is why understanding that previous content is essential. Alright, I believe that everyone here knows what an indicator does and how it works, at least at a basic level. So, let's now look at the second code example.
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. //+------------------------------------------------------------------+
Source code of a test service
Notice that on line 2, we inform the compiler that this script is a service. Then, on line 6, we add an input parameter, allowing the user to define which symbol the service will observe. So far, nothing unusual. Between lines 10 and 12, we declare a few variables. Now comes the important part: for the service to function, at least one chart must be open. Otherwise, we'll run into problems. But if there's at least one chart open, line 14 will successfully capture its ID. We then iterate through the open charts, looking for the one that matches the symbol defined in line 6. Once we find it, we retrieve its correct chart ID.
With that chart ID in hand, we request MetaTrader 5 to give us the handle for the indicator. Note the indicator name used here. It must match the one defined in line 16 of the indicator source code. From there, we enter a loop. Since we now have a valid handle, we can use it to read the indicator buffer. That's exactly what we do on line 21. On line 22, we print the buffer data and the result of the read operation to the terminal. This loop runs until the conditions on line 24 are no longer met, in other words, until the service is stopped or the buffer read fails. Once this happens, line 25 releases the handle.
So, we all agree that this code does indeed read the indicator buffer. And the value it prints will match the expected one thanks to the tests we discussed in the previous article. So let's run this in MetaTrader 5. For your convenience, you can watch the results in the video below:
Wait, what just happened here? You might be thinking I'm trying to trick you. That's fine. You're free to believe whatever you want. In fact, you don't need to take my word for it. Run your own tests. But be sure to keep the core idea intact: using a handle to read an indicator buffer from a service. What you saw in the video will happen. But why?
The key lies in the handle. As soon as MetaTrader 5 changes the chart timeframe, the handle ID changes. However, your code is still referencing the old handle. But that handle is no longer valid as MetaTrader 5 has re-created the indicator internally, and the new buffer is now linked to a different handle. When you're working with an Expert Advisor or another indicator, those are automatically reattached to the chart after a timeframe switch. This causes all function calls to be reissued and the handle to be updated. That's because the Expert Advisor or indicator is reloaded onto the chart. At this point, the handle is updated, allowing the buffer to be read correctly.
But a service runs outside the chart context. As a result, the handle isn't updated. That's why the value read from the buffer remains the same. You might still think this doesn't apply to you. That somehow, your service operates in a different way. Fine. Let's test that hypothesis. Here’s the new version of the service script, just below: This time, we'll modify only the service code. The code is shown below:
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. //+------------------------------------------------------------------+
Source code of a test service
Note that the code underwent small modifications - changes that are almost imperceptible at first glance. But here's the key point: line 20 is where the critical change occurs. Now, the handle is no longer static - it has become dynamic. This means that even if the service does not know the chart's current timeframe, or what modifications are happening to it, it can still find and interact with the correct instance of the indicator, and read its buffer properly. To make things easier, you can see this behavior in the video below:
Now you're probably thinking that I am messing up with you. How is this possible? Take it easy, dear reader. As I mentioned earlier, I didn't want to show you the changes in the C_Replay.mqh file before first presenting this concept.
See how something that seems simple but is entirely valid and reasonable can drastically change how your code behaves? The key is this: you must always test things in the simplest environment possible. I often see people trying to write overly elaborate code without having a solid, well-founded base. In the end, they give up because they can't figure out where the subtle issues are hiding.
But the truth is, you must always weigh computational cost against implementation complexity. There's no use in having working code if it'sslow. Likewise, it's no good having fast code that simply doesn't work.
By making the handle dynamic, as we did in this second version of the service, we introduce a slight performance cost. Because now, there's a handle lookup occurring on each execution. The first version had the handle already stored, so it was much faster to simply reference a preloaded variable.
This is the trade-off you must always evaluate. That's why I believe that you should build and test things within minimal, simple code. But how will this logic actually be integrated and implemented in the C_Replay.mqh header file, which is what controls the replay/simulator service? To answer that, let's move on to a new section.
Modifying the C_Replay.mqh File
Here I faced a small dilemma: should I show the already modified code, or walk you through the modification process step by step? This kind of decision often slows down the article more than I'd like. Writing and testing code is quick. But explaining what changed and why takes time.
Still, my goal is for you, dear reader, to actually understand and learn how to do this. I've been through this. Years of programming tend to leave marks. That said, I truly believe showing the step-by-step transformation is the best path forward. Even if it means you'll have to do a bit of cleanup on your end to remove deprecated sections. So, let's now look at the entire C_Replay.mqh header file.
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. //+------------------------------------------------------------------+
Source code of the C_Replay.mqh file
Note that every struck-through line must be removed from the code. But let's take a closer look at why so many lines were removed.
The first important point is in 23. As demonstrated in the previous section, we should not use a static handle. For this reason, the 'handle' variable has become obsolete. Consequently, all portions of the code that referenced it were removed. Also, an entire procedure was removed. It is UpdateIndicatorControl, previously located between lines 75 and 93. Accordingly, every reference to it has been removed as well. This means we now need to find a way to replace the functionality previously covered by UpdateIndicatorControl.
But we'll get to that in just a moment. Before we dive into that, let's take a quick look at two specific functions. The first is InitBaseControl, which begins at line 243. You'll notice some small but meaningful changes to this function. These adjustments aim to both improve user experience and standardize the initialization of indicators. Let's see what's happening.
Between lines 253 and 255, we attempt to load the control indicator. Here's a crucial detail: indicator loading does not happen instantly. There is a slight latency in execution. Therefore, before executing line 257, we need to ensure that the control indicator has been properly loaded on the chart. This check is performed in line 253. Pay attention to the indicator names in lines 256 and 258 - these are the specific indicators we expect to be loaded. These calls are triggered at line 185. From this point onward, things start to get interesting. So pay close attention.
The loop at line 188 will wait for the specified indicator to load on the chart. When line 256 requests that we wait for the control indicator to load, the check in line 187 prevents us from early check. However, when line 255 asks to wait for the mouse indicator to load, line 190 checks the control indicator's buffer. Why does this happen? Because the user may change the chart timeframe. To better understand this, let's jump to line 52.
At line 52, we find the procedure responsible for reading the control indicator buffer. And now the most interesting part. There are two static variables declared in lines 54 and 55. But at this moment, we care about the one in line 54. Note that it is initialized with a value of 0. Now, if the attempt to read from the buffer in line 58 returns a value less than zero, it means the control indicator was removed from the chart. Since the user cannot manually reattach it and we require its presence, the chart is closed. This will terminate the application. For this reason, we must not check the control indicator while it's still in the loading phase.
In line 60, we check whether the value of the control indicator is greater than the position being analyzed in the service. If so, it means we must fast-forward execution once the user hits play. However, the truly critical condition is the one in line 64. This check is the Achilles' heel of the timeframe lock - the mechanism discussed in the previous section. If the user has not changed the chart timeframe, this condition will return true, and no further action is taken.
If the check returns false, then line 69 stores the new timeframe, and line 70 retrieves the last known status of the indicator. This is necessary because, at line 72, another procedure will be called. Let's jump to line 37. Here's where things get really interesting. This is precisely where we instruct MetaTrader 5 to trigger events on the chart. The reason for separating this from the buffer validation is that, at times, we simply want to trigger chart events, while in other cases, we need to confirm whether any meaningful changes occurred in the value of the control indicator.
Note that all the logic of SendEventCustom already existed in the previous code version. Therefore, there's no need to explain it again. The code is straightforward and self-explanatory. I believe I’ve explained the core structural changes. However, we still need to discuss the modifications made to the LoopEventOnTime function. Although these changes are not dramatic or structural, they highlight the reason why UpdateIndicatorControl was split into two new procedures. Let's go through this quickly.
In line 275, we ask MetaTrader 5 to send custom events to us. This ensures both the mouse and control indicators are correctly configured before beginning simulation or replay. Although line 275 could be skipped during the first execution, it must be run after the first "play" command to ensure data is revalidated. This line is re-executed once the system enters paused mode.
At line 279, we don't need to trigger any events. We simply observe the control indicator. Once the user presses play, the loop at line 276 ends, and simulation begins.
In line 285, we again request custom events. This time we need to allow the mouse indicator to display the remaining time on the bar.
Another subtle change occurs in lines 293 and 294. It's a simple logic block that updates the mouse indicator's status. This status allows us to detect whether the asset has entered or exited auction mode.
The final change is shown in line 306, where we check if timeframe has changed. If a change is detected, indicators are reloaded, as discussed at the beginning of this section.
With this, all necessary code adjustments have been made. The video below demonstrates how the system now behaves when the chart timeframe is changed.
Conclusion
In these last two articles, I've shown how important it is to experiment and push your programming language to its limits. Even if some readers might feel there was little direct knowledge gained, that's not the case. I am sure that many believed what I explained here was either not possible or at the very least, not practical to implement or maintain. However, before modifying the main code, I explained the importance of first forming a hypothesis and then building a simple prototype to test it.
But the most important lesson is this: Never give up on the first attempt. If something doesn't work, adjust your approach, but always remain focused on testing your hypothesis. That's exactly what I did. My goal was to push data into the indicator buffer to clearly and reliably detect timeframe changes. Doing this directly on the chart was easy. So the question became: Could the service detect this change too? The first implementation attempt failed. However, we were still able to read the timeframe value. The problem was, that value represented the state at the time the indicator was first attached to the chart.
This is where the concept truly emerged. If I had simply given up, rather than rethink the architecture to dynamically retrieve the handle during each call, I would have missed the opportunity to access real-time updated data from the indicator. By rethinking the design, we opened new possibilities, proving that we can go far beyond what many consider possible.
That's how real systems are built: You start with a hypothesis, test it, and even if the initial approach only partially works, you iterate always guided by the same core idea.
In the attachment, you'll find the necessary executable files for using the replay/simulator. In the next article, we'll start exploring additional features we need to integrate into the replay/simulator application. See you there.
Translated from Portuguese by MetaQuotes Ltd.
Original article: https://www.mql5.com/pt/articles/12363





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use