
Developing a Replay System (Part 68): Getting the Time Right (I)
Introduction
In the article "Developing a Replay System (Part 66): Playing the service (VII)", I demonstrated how to make the mouse indicator inform us of how much time remains before the current bar ends and a new one begins plotting on the chart. While that method works very well and is quite efficient, it has a problem. Although many assume the issue lies with the method itself, it is actually related to the liquidity of the trading symbol. If you paid close attention to the explanation provided in the article, you may have noticed that the mouse indicator only updates the information when a new quote is received.
This occurs because we rely on MetaTrader 5 to trigger the OnCalculate event, which allows the indicator to receive a new value in seconds in order to calculate the remaining time of the current bar. As shown in the article, this value is set via the spread. However, because we rely on the value provided by the service in the spread, we cannot accurately report the remaining bar time in assets with (or during periods of) low liquidity. In cases where the asset enters an auction state, the problem becomes more pronounced, as the market can remain suspended for several minutes. Unfortunately, there is no reliable workaround for the auction scenario. Even for assets with good liquidity, no OnCalculate events will be triggered during an auction period. Therefore, the replay/simulation service must be updated to ensure the user is properly informed in these two scenarios. First, when liquidity is low and only a few trades occur per second. And second, when the asset enters an auction. These two issues must be addressed before we can move on to the next stage of development.
Implementing the Solution for Low Liquidity
The first scenario, where liquidity is low, is relatively simple and straightforward to solve, as it mainly requires changes to the service code, at least in general terms. It may eventually require changes elsewhere. But for now, we will attempt to resolve it by modifying only the source code of the replay/simulation service.
To efficiently determine whether this is possible, we will temporarily set aside our original source code. We will create a separate test code to verify if the service, while inactive, can still update the mouse indicator regarding the time remaining until the current bar closes.
The code we need to develop should be as simple as possible, with nothing overly complex or elaborate. The goal is to test whether it is indeed possible to send data from the server to the indicator without affecting the information displayed on the chart. The source code to be used for this purpose is shown below:
01. //+------------------------------------------------------------------+ 02. #property service 03. #property copyright "Daniel Jose" 04. #property description "Data synchronization demo service." 05. //+------------------------------------------------------------------+ 06. #include <Market Replay\Defines.mqh> 07. //+------------------------------------------------------------------+ 08. #define def_Loop ((!_StopFlag) && (ChartSymbol(id) != "")) 09. //+------------------------------------------------------------------+ 10. void OnStart() 11. { 12. long id; 13. int handle; 14. MqlRates Rate[1]; 15. 16. Print("Starting Test Service..."); 17. SymbolSelect(def_SymbolReplay, false); 18. CustomSymbolDelete(def_SymbolReplay); 19. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 20. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0.5); 21. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 5); 22. Rate[0].close = 105; 23. Rate[0].open = 100; 24. Rate[0].high = 110; 25. Rate[0].low = 95; 26. Rate[0].tick_volume = 5; 27. Rate[0].spread = 1; 28. Rate[0].real_volume = 10; 29. Rate[0].time = D'10.03.2023 09:00'; 30. CustomRatesUpdate(def_SymbolReplay, Rate, 1); 31. Rate[0].time = D'10.03.2023 09:30'; 32. CustomRatesUpdate(def_SymbolReplay, Rate, 1); 33. SymbolSelect(def_SymbolReplay, true); 34. id = ChartOpen(def_SymbolReplay, PERIOD_M30); 35. Sleep(1000); 36. if ((handle = iCustom(NULL, 0, "\\Indicators\\Mouse Study.ex5")) != INVALID_HANDLE) 37. ChartIndicatorAdd(id, 0, handle); 38. IndicatorRelease(handle); 39. Print(TimeToString(Rate[0].time, TIME_DATE | TIME_SECONDS)); 40. while (def_Loop) 41. { 42. CustomRatesUpdate(def_SymbolReplay, Rate, 1); 43. Sleep(250); 44. Rate[0].spread++; 45. if (Rate[0].spread == 60) 46. { 47. Rate[0].time += 60; 48. Rate[0].spread = 0; 49. Print(TimeToString(Rate[0].time, TIME_DATE | TIME_SECONDS)); 50. EventChartCustom(id, evSetServerTime, (long)(Rate[0].time), 0, ""); 51. } 52. } 53. ChartClose(id); 54. SymbolSelect(def_SymbolReplay, false); 55. CustomSymbolDelete(def_SymbolReplay); 56. Print("Finished Test Service..."); 57. } 58. //+------------------------------------------------------------------+
Source code of the test service
Keep in mind that, for the procedure described below to work correctly, the mouse indicator must be compiled and located in the specific directory I'll show during the explanation. Otherwise, the test will fail. So let's understand what needs to be executed and how we can later use this to our advantage, provided the test proves effective for our intended purpose.
The first four lines are declarations and property settings related to the type of executable being generated. On line 6, we include a header file, simply to avoid having to declare additional components within this script. Line 8 defines a control directive for the loop we'll implement shortly. In truth, the only truly important line among these first eight is line 2. The others could be omitted, and their respective code inserted in the appropriate places. However, line 2 is essential because it informs MetaTrader 5 that the generated executable should be a service. Otherwise, it would be interpreted as a script, which is not what we intend to test here.
From line 10 onward, things become more interesting. Between lines 12 and 14, we declare the variables we'll use. Then, from lines 17 to 21, we begin initializing the creation of the custom symbol. These are standard steps, so we can move forward. Now, between lines 22 and 28, we create a RATE structure for the bar. What really matters here is line 29, where we define the timestamp at which the bar will be created. In line 30, we specify the bar data that MetaTrader 5 should use to generate it. Now pay close attention! In line 31, we create a second bar using the same data, but this time it is offset by 30 minutes. The reason for this offset is that the chart will be opened with a 30-minute time frame. If you use any other time value when creating the bar in line 31, the mouse indicator will calculate how much time is left until that bar closes. So, pay attention to this detail. Another critical point: DO NOT include any value in seconds. The value must be defined in minutes, never in seconds. The next step is to create the bar on line 32.
In line 34, we open the chart. As discussed in the previous article in this series, before the service can add anything to the chart, whether objects or indicators, it must wait a bit. This allows the MetaTrader 5 platform enough time to properly construct the chart and render it on screen. This pause occurs on line 35, where we wait for one second before continuing execution.
Again, attention is needed on line 36. Here, we attempt to create a handle in order to place the mouse indicator on the chart. The iCustom function returns the handle required to add the indicator (if the indicator is found). If it is not found, the function returns an invalid handle, and we do not proceed with using it. Therefore, the mouse indicator must be in the specified location. Otherwise, the test will fail. If the indicator is found successfully, we add it to the chart. This is done on line 37. Since the handle returned by iCustom is no longer needed afterward, we release it in line 38.
Now comes the actual test. The part we're truly interested in, where we determine whether the approach works. If it does, we'll have a way to resolve the issue with low-liquidity scenarios. If it doesn't, we'll need to explore alternative solutions. So, let's understand how this test is conducted. It takes place in a loop that starts at line 40 and ends at line 52. The remaining code (lines 53 to 56) simply concludes the test and is not particularly relevant for this article. So let's focus on the test itself.
Understanding How the Test Works
It begins at line 40, where we enter the loop. If the chart is open and the user hasn't stopped the service, the loop will run indefinitely. Once inside the loop, we reach line 42. This line forcibly updates the bar data. However, it's important to note that no new information is actually being passed to the bar, only what we are altering for testing purposes. Here's what we do. In line 43, we add a short delay to prevent the updates from occurring too rapidly. Take note of the delay value: we're not waiting for a full second. It's only a quarter of a second, and this is important when you later observe the results in MetaTrader 5.
Then, in line 44, we increment the spread value. This gives the mouse indicator the impression that one second has passed, even though the actual delay is shorter. The purpose of this is tied directly to how the platform triggers OnCalculate events. Up to this point, the expected behavior is as follows: once the chart is opened and the mouse indicator is displayed, MetaTrader 5 should generate OnCalculate events, allowing the remaining time for the current bar to be decremented accordingly. You can check this yourself. However, I've already tested it and confirmed that it works as expected. Out of curiosity, I decided to see what would happen when the spread value exceeded 60. Why is this value significant? Because once it reaches 60, a new one-minute bar should appear, even if you're using a monthly timeframe. The actual chart timeframe is irrelevant here. For our purposes, we simply need MetaTrader 5 to be informed that a new one-minute bar has formed.
Therefore, we must ensure our code handles this properly. MetaTrader 5 cannot resolve this situation on its own. Line 45 checks if the iteration count has reached 60. When it does, line 47 adds 60 seconds (one minute) to the original bar time. This way, the next time the function in line 42 is executed, MetaTrader 5 recognizes a new one-minute bar. But that's not quite enough. In line 48, we reset the iteration counter so that a new cycle can begin. For verification, line 49 prints the value being sent to MetaTrader 5.
Here’s a critical point: up until now, only MetaTrader 5 has been informed that a new bar exists. However, as we saw in the article where we implemented the system to display the remaining time in the mouse indicator, we cannot rely on the time value provided by the OnCalculate function. At least not yet. I'm currently exploring a workaround for this limitation, which would eventually make line 50 unnecessary. Until such a workaround is implemented, line 50 must force MetaTrader 5 to generate a custom event. This notifies the mouse indicator that a new bar has formed. Thus, the indicator can properly identify the current point in time and accurately inform us how much time remains before the current bar closes and a new one begins.
If you prefer not to compile and test this service in MetaTrader 5, no problem. In the video below, I demonstrate how the service behaved and whether this test code is viable for use in the replay/simulation service. In my assessment, the test worked exceptionally well. It proved both the ability and feasibility of integrating the tested mechanism into the replay/simulator application. This would allow us to maintain accurate time tracking, even during low-liquidity periods or with symbols having low trading activity, ensuring users are informed of how much time remains before a new bar appears on the chart.
To integrate the tested mechanism into the replay/simulator application, we'll need to slightly modify a very specific section of the code. This section is located in the header file C_Replay.mqh. But before we move on to implementing those changes, there's another concept we need to understand first. To better organize the explanation, let's move on to a new topic.
Time, Time, Time – How Can We Understand It?
Although certain things may seem confusing at first, I'll need to show you something that may appear extremely complex, but it's entirely possible and logically sound. And it all revolves around the structure of the data involved.
If you look at the OnCalculate function, you can see that the spread value is provided as an int. On 64-bit processors, this means we're working with 32 bits of data. At the same time, if you check the datetime type, you'll see that it uses 64 bits. Okay. But how do these details help us here? Patience, dear reader. Patience. We need to do a bit of math now, but nothing complicated, just some basic calculations.
What we need is for the service to force MetaTrader 5 to generate some kind of event that updates the timer in the mouse indicator. This is a given, and we're already handling it in two different ways. One is by invoking the OnCalculate function, and the other is through a custom event that we trigger periodically. To see where this is being handled in the service, take a look at the code from the C_Replay.mqh header file:
068. //+------------------------------------------------------------------+ 069. inline void CreateBarInReplay(bool bViewTick) 070. { 071. bool bNew; 072. double dSpread; 073. int iRand = rand(); 074. 075. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 076. { 077. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 078. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 079. { 080. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 081. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 082. { 083. m_Infos.tick[0].ask = m_Infos.tick[0].last; 084. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 085. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 086. { 087. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 088. m_Infos.tick[0].bid = m_Infos.tick[0].last; 089. } 090. } 091. if (bViewTick) 092. { 093. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 094. if (bNew) EventChartCustom(m_Infos.IdReplay, evSetServerTime, (long)m_Infos.Rate[0].time, 0, ""); 095. } 096. m_Infos.Rate[0].spread = (int)macroGetSec(m_MemoryData.Info[m_Infos.CountReplay].time); 097. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 098. } 099. m_Infos.CountReplay++; 100. } 101. //+------------------------------------------------------------------+
Code from the C_Replay.mqh file
This was already explained in the article Developing a Replay System (Part 66): Playing the Service (VII), but here we're going to reinforce the explanation. Every time this procedure is called and a new tick is added to the bar currently being plotted on the chart, we have line 96 where the a value (in seconds) is added to the spread. This allows us to calculate the remaining time for the current bar. Alright. Additionally, whenever a new one-minute bar appears, there's a condition on line 94 that becomes true, which forces MetaTrader 5 to trigger a custom event. This event allows the mouse indicator to readjust its internal timing and correctly detect when the new bar was generated. The reason for doing it this way is to avoid adding an extra call that would be executed very intensively within the mouse indicator itself. I've explained this in more detail in the previously mentioned article.
However, we're wasting both time and space by handling it this way. We can be a bit more efficient and transmit more information using fewer resources than the current method. Now comes the calculation part. We only need to track the passing of time in one-second intervals; we don't need faster. So that gives us a clear starting point: there are 60 seconds in a minute, 60 minutes in an hour, and 24 hours in a day. That totals 86,400 seconds per day (approximately, since a day isn't exactly 24 hours, but for our purposes we'll assume it is). Alright. A 32-bit unsigned integer can hold a maximum value of 4,294,967,295. Assuming we have an unsigned value (which we do), we can store approximately 49 days' worth of seconds in a single 32-bit field. That’s an important point. Within a single spread value, which is 32 bits, we can represent roughly 49 days. However, in practice, our replay/simulator application is unlikely to be used for studies spanning more than one or two consecutive days. Therefore, we can synchronize the necessary data entirely through the spread field, eliminating the need to force MetaTrader 5 to issue a custom event just to notify the mouse indicator that a new bar has formed.
Now things are getting interesting, aren't they? To make things even more efficient, I'll go one step further and implement a more sophisticated model to avoid the 49-day limit altogether. Realistically, I doubt anyone will ever hit that ceiling, but still I'll implement something else. We will apply a bit-level control strategy that gives us more flexibility. This approach will require a few adjustments in the mouse indicator. But nothing overly complex when compared to the benefits it brings.
Still, will this really give us the kind of solution we want? To find out, we need to look at another code fragment, also from the C_Replay.mqh header file. It is shown below:
207. //+------------------------------------------------------------------+ 208. bool LoopEventOnTime(void) 209. { 210. int iPos; 211. 212. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 213. { 214. UpdateIndicatorControl(); 215. Sleep(200); 216. } 217. m_MemoryData = GetInfoTicks(); 218. AdjustPositionToReplay(); 219. EventChartCustom(m_Infos.IdReplay, evSetServerTime, (long)macroRemoveSec(m_MemoryData.Info[m_Infos.CountReplay].time), 0, ""); 220. iPos = 0; 221. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 222. { 223. if (m_IndControl.Mode == C_Controls::ePause) return true; 224. iPos += (int)(m_Infos.CountReplay < (m_MemoryData.nTicks - 1) ? m_MemoryData.Info[m_Infos.CountReplay + 1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : 0); 225. CreateBarInReplay(true); 226. while ((iPos > 200) && (def_CheckLoopService)) 227. { 228. Sleep(195); 229. iPos -= 200; 230. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 231. UpdateIndicatorControl(); 232. } 233. } 234. 235. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 236. } 237. }; 238. //+------------------------------------------------------------------+
Code from the C_Replay.mqh file
Very well, our issue arises when the asset is experiencing low liquidity. That is, ticks are not being generated every second in a continuous flow. At certain times, the liquidity may be sufficient for ticks to be generated every second, which is ideal for our purposes. However, there are also moments when this level of liquidity is not achieved. Even so, you can observe that in line 225, we are invoking the previously discussed code fragment. And even if the time between ticks exceeds one second, the call on line 225 will still, in fact, occur. So yes, using the spread to transmit time information is indeed a viable solution. However, implementing all the necessary mechanisms can be somewhat complex. Because of this, it won't be possible to cover every detail in this single article. Nonetheless, let's get started with the basics here.
Starting a New Phase
The first step we'll take is to modify the Defines.mqh header file. The changes can be seen below:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_VERSION_DEBUG 05. //+------------------------------------------------------------------+ 06. #ifdef def_VERSION_DEBUG 07. #define macro_DEBUG_MODE(A) \ 08. Print(__FILE__, " ", __LINE__, " ", __FUNCTION__ + " " + #A + " = " + (string)(A)); 09. #else 10. #define macro_DEBUG_MODE(A) 11. #endif 12. //+------------------------------------------------------------------+ 13. #define def_SymbolReplay "RePlay" 14. #define def_MaxPosSlider 400 15. #define def_MaskTimeService 0xFED00000 16. //+------------------------------------------------------------------+ 17. union uCast_Double 18. { 19. double dValue; 20. long _long; // 1 Information 21. datetime _datetime; // 1 Information 22. uint _32b[sizeof(double) / sizeof(uint)]; // 2 Informations 23. ushort _16b[sizeof(double) / sizeof(ushort)]; // 4 Informations 24. uchar _8b [sizeof(double) / sizeof(uchar)]; // 8 Informations 25. }; 26. //+------------------------------------------------------------------+ 27. enum EnumEvents { 28. evHideMouse, //Hide mouse price line 29. evShowMouse, //Show mouse price line 30. evHideBarTime, //Hide bar time 31. evShowBarTime, //Show bar time 32. evHideDailyVar, //Hide daily variation 33. evShowDailyVar, //Show daily variation 34. evHidePriceVar, //Hide instantaneous variation 35. evShowPriceVar, //Show instantaneous variation 36. evSetServerTime, //Replay/simulation system timer 37. evCtrlReplayInit, //Initialize replay control 38. }; 39. //+------------------------------------------------------------------+
Defines.mqh file source code
Note that line 36 is struck through, which means it no longer exists in the final version of the file. Similarly, we now have a new definition at line 15, which will serve as a mask, allowing us to work more comfortably. The mere removal of line 36, which was the event responsible for updating the timer, already implies that some adjustments will be needed to make the new code compatible with the updated message handling policy.
Just as we made changes to the Defines.mqh file, we have additional updates in another header file, this time Macros.mqh. The updated version of this file is shown below:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define macroRemoveSec(A) (A - (A % 60)) 05. #define macroGetDate(A) (A - (A % 86400)) 06. #define macroGetSec(A) (A - (A - (A % 60))) 07. #define macroGetTime(A) (A % 86400) 08. //+------------------------------------------------------------------+ 09. #define macroColorRGBA(A, B) ((uint)((B << 24) | (A & 0x00FF00) | ((A & 0xFF0000) >> 16) | ((A & 0x0000FF) << 16))) 10. #define macroTransparency(A) (((A > 100 ? 100 : (100 - A)) * 2.55) / 255.0) 11. //+------------------------------------------------------------------+
Source code of the Macros.mqh file
Here, we've only added line 7, which introduces a macro that enables us to extract the time value from a variable of type datetime. As this is a simple addition, we can move on to reviewing another header file. This time, the header file is C_Stuty.mqh, which can be viewed in full below. This file is part of the mouse indicator.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "..\C_Mouse.mqh" 005. //+------------------------------------------------------------------+ 006. #define def_ExpansionPrefix def_MousePrefixName + "Expansion_" 007. //+------------------------------------------------------------------+ 008. class C_Study : public C_Mouse 009. { 010. private : 011. //+------------------------------------------------------------------+ 012. struct st00 013. { 014. eStatusMarket Status; 015. MqlRates Rate; 016. string szInfo, 017. szBtn1, 018. szBtn2, 019. szBtn3; 020. color corP, 021. corN; 022. int HeightText; 023. bool bvT, bvD, bvP; 024. datetime TimeDevice; 025. }m_Info; 026. //+------------------------------------------------------------------+ 027. void Draw(void) 028. { 029. double v1; 030. 031. if (m_Info.bvT) 032. { 033. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 18); 034. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_TEXT, m_Info.szInfo); 035. } 036. if (m_Info.bvD) 037. { 038. v1 = NormalizeDouble((((GetInfoMouse().Position.Price - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2); 039. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1); 040. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP)); 041. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); 042. } 043. if (m_Info.bvP) 044. { 045. v1 = NormalizeDouble((((GL_PriceClose - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2); 046. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1); 047. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP)); 048. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); 049. } 050. } 051. //+------------------------------------------------------------------+ 052. inline void CreateObjInfo(EnumEvents arg) 053. { 054. switch (arg) 055. { 056. case evShowBarTime: 057. C_Mouse::CreateObjToStudy(2, 110, m_Info.szBtn1 = (def_ExpansionPrefix + (string)ObjectsTotal(0)), clrPaleTurquoise); 058. m_Info.bvT = true; 059. break; 060. case evShowDailyVar: 061. C_Mouse::CreateObjToStudy(2, 53, m_Info.szBtn2 = (def_ExpansionPrefix + (string)ObjectsTotal(0))); 062. m_Info.bvD = true; 063. break; 064. case evShowPriceVar: 065. C_Mouse::CreateObjToStudy(58, 53, m_Info.szBtn3 = (def_ExpansionPrefix + (string)ObjectsTotal(0))); 066. m_Info.bvP = true; 067. break; 068. } 069. } 070. //+------------------------------------------------------------------+ 071. inline void RemoveObjInfo(EnumEvents arg) 072. { 073. string sz; 074. 075. switch (arg) 076. { 077. case evHideBarTime: 078. sz = m_Info.szBtn1; 079. m_Info.bvT = false; 080. break; 081. case evHideDailyVar: 082. sz = m_Info.szBtn2; 083. m_Info.bvD = false; 084. break; 085. case evHidePriceVar: 086. sz = m_Info.szBtn3; 087. m_Info.bvP = false; 088. break; 089. } 090. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); 091. ObjectDelete(GetInfoTerminal().ID, sz); 092. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); 093. } 094. //+------------------------------------------------------------------+ 095. public : 096. //+------------------------------------------------------------------+ 097. C_Study(long IdParam, string szShortName, color corH, color corP, color corN) 098. :C_Mouse(IdParam, szShortName, corH, corP, corN) 099. { 100. if (_LastError != ERR_SUCCESS) return; 101. ZeroMemory(m_Info); 102. m_Info.Status = eCloseMarket; 103. m_Info.Rate.close = iClose(GetInfoTerminal().szSymbol, PERIOD_D1, ((GetInfoTerminal().szSymbol == def_SymbolReplay) || (macroGetDate(TimeCurrent()) != macroGetDate(iTime(GetInfoTerminal().szSymbol, PERIOD_D1, 0))) ? 0 : 1)); 104. m_Info.corP = corP; 105. m_Info.corN = corN; 106. CreateObjInfo(evShowBarTime); 107. CreateObjInfo(evShowDailyVar); 108. CreateObjInfo(evShowPriceVar); 109. } 110. //+------------------------------------------------------------------+ 111. void Update(const eStatusMarket arg) 112. { 113. int i0; 114. datetime dt; 115. 116. switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) 117. { 118. case eCloseMarket : 119. m_Info.szInfo = "Closed Market"; 120. break; 121. case eInReplay : 122. case eInTrading : 123. i0 = PeriodSeconds(); 124. dt = (m_Info.Status == eInReplay ? (datetime) m_Info.TimeDevice + GL_TimeAdjust : TimeCurrent()); 125. dt = (m_Info.Status == eInReplay ? (datetime) GL_TimeAdjust : TimeCurrent()); 126. m_Info.Rate.time = (m_Info.Rate.time <= dt ? (datetime)(((ulong) dt / i0) * i0) + i0 : m_Info.Rate.time); 127. if (dt > 0) m_Info.szInfo = TimeToString((datetime)m_Info.Rate.time - dt, TIME_SECONDS); 128. break; 129. case eAuction : 130. m_Info.szInfo = "Auction"; 131. break; 132. default : 133. m_Info.szInfo = "ERROR"; 134. } 135. Draw(); 136. } 137. //+------------------------------------------------------------------+ 138. virtual void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 139. { 140. C_Mouse::DispatchMessage(id, lparam, dparam, sparam); 141. switch (id) 142. { 143. case CHARTEVENT_CUSTOM + evHideBarTime: 144. RemoveObjInfo(evHideBarTime); 145. break; 146. case CHARTEVENT_CUSTOM + evShowBarTime: 147. CreateObjInfo(evShowBarTime); 148. break; 149. case CHARTEVENT_CUSTOM + evHideDailyVar: 150. RemoveObjInfo(evHideDailyVar); 151. break; 152. case CHARTEVENT_CUSTOM + evShowDailyVar: 153. CreateObjInfo(evShowDailyVar); 154. break; 155. case CHARTEVENT_CUSTOM + evHidePriceVar: 156. RemoveObjInfo(evHidePriceVar); 157. break; 158. case CHARTEVENT_CUSTOM + evShowPriceVar: 159. CreateObjInfo(evShowPriceVar); 160. break; 161. case (CHARTEVENT_CUSTOM + evSetServerTime): 162. m_Info.TimeDevice = (datetime)lparam; 163. break; 164. case CHARTEVENT_MOUSE_MOVE: 165. Draw(); 166. break; 167. } 168. ChartRedraw(GetInfoTerminal().ID); 169. } 170. //+------------------------------------------------------------------+ 171. }; 172. //+------------------------------------------------------------------+ 173. #undef def_ExpansionPrefix 174. #undef def_MousePrefixName 175. //+------------------------------------------------------------------+
C_Study.mqh file source code
All the struck-through lines in this file should be removed from the original file. While those lines did previously form part of the old code used to allow the custom event to keep the indicator's timer in sync, this is no longer necessary, as we are now implementing a different method for this task. However, it's important that you observe a particular detail in this header file. Look at line 124 and compare it to line 125. Although it's indicated that the entire line was replaced, what actually happened is that the variable m_Info.TimeDevice was removed. This is because it's no longer needed, it was previously used to keep the timer synchronized. Now, this brings us to a new issue, so to speak, which is present in the code of the mouse indicator itself. The full code of the indicator can be found below:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property description "This is an indicator for graphical studies using the mouse." 04. #property description "This is an integral part of the Replay / Simulator system." 05. #property description "However it can be used in the real market." 06. #property version "1.68" 07. #property icon "/Images/Market Replay/Icons/Indicators.ico" 08. #property link "https://www.mql5.com/pt/articles/" 09. #property indicator_chart_window 10. #property indicator_plots 0 11. #property indicator_buffers 1 12. #property indicator_applied_price PRICE_CLOSE 13. //+------------------------------------------------------------------+ 14. double GL_PriceClose; 15. datetime GL_TimeAdjust; 16. //+------------------------------------------------------------------+ 17. #include <Market Replay\Auxiliar\Study\C_Study.mqh> 18. //+------------------------------------------------------------------+ 19. C_Study *Study = NULL; 20. //+------------------------------------------------------------------+ 21. input color user02 = clrBlack; //Price Line 22. input color user03 = clrPaleGreen; //Positive Study 23. input color user04 = clrLightCoral; //Negative Study 24. //+------------------------------------------------------------------+ 25. C_Study::eStatusMarket m_Status; 26. int m_posBuff = 0; 27. double m_Buff[]; 28. //+------------------------------------------------------------------+ 29. int OnInit() 30. { 31. ResetLastError(); 32. Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04); 33. if (_LastError != ERR_SUCCESS) return INIT_FAILED; 34. if ((*Study).GetInfoTerminal().szSymbol != def_SymbolReplay) 35. { 36. MarketBookAdd((*Study).GetInfoTerminal().szSymbol); 37. OnBookEvent((*Study).GetInfoTerminal().szSymbol); 38. m_Status = C_Study::eCloseMarket; 39. }else 40. m_Status = C_Study::eInReplay; 41. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 42. ArrayInitialize(m_Buff, EMPTY_VALUE); 43. 44. return INIT_SUCCEEDED; 45. } 46. //+------------------------------------------------------------------+ 47. int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], 48. const double& high[], const double& low[], const double& close[], const long& tick_volume[], 49. const long& volume[], const int& spread[]) 50. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double& price[]) 51. { 52. GL_PriceClose = close[rates_total - 1]; 53. GL_PriceClose = price[rates_total - 1]; 54. GL_TimeAdjust = (spread[rates_total - 1] < 60 ? spread[rates_total - 1] : 0); 55. if (_Symbol == def_SymbolReplay) 56. GL_TimeAdjust = iSpread(NULL, PERIOD_M1, 0) & (~def_MaskTimeService); 57. m_posBuff = rates_total; 58. (*Study).Update(m_Status); 59. 60. return rates_total; 61. } 62. //+------------------------------------------------------------------+ 63. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 64. { 65. (*Study).DispatchMessage(id, lparam, dparam, sparam); 66. (*Study).SetBuffer(m_posBuff, m_Buff); 67. 68. ChartRedraw((*Study).GetInfoTerminal().ID); 69. } 70. //+------------------------------------------------------------------+ 71. void OnBookEvent(const string &symbol) 72. { 73. MqlBookInfo book[]; 74. C_Study::eStatusMarket loc = m_Status; 75. 76. if (symbol != (*Study).GetInfoTerminal().szSymbol) return; 77. MarketBookGet((*Study).GetInfoTerminal().szSymbol, book); 78. m_Status = (ArraySize(book) == 0 ? C_Study::eCloseMarket : C_Study::eInTrading); 79. for (int c0 = 0; (c0 < ArraySize(book)) && (m_Status != C_Study::eAuction); c0++) 80. if ((book[c0].type == BOOK_TYPE_BUY_MARKET) || (book[c0].type == BOOK_TYPE_SELL_MARKET)) m_Status = C_Study::eAuction; 81. if (loc != m_Status) (*Study).Update(m_Status); 82. } 83. //+------------------------------------------------------------------+ 84. void OnDeinit(const int reason) 85. { 86. if (reason != REASON_INITFAILED) 87. { 88. if ((*Study).GetInfoTerminal().szSymbol != def_SymbolReplay) 89. MarketBookRelease((*Study).GetInfoTerminal().szSymbol); 90. } 91. delete Study; 92. } 93. //+------------------------------------------------------------------+
Mouse Pointer source code
This code, at times, has been behaving in a way that's seriously frustrating me during tests. And there are other elements, which I've tested but will dive into later, that are incredibly infuriating, mainly because they make no sense. These things should work, but for some bizarre and unexplained reason, they just don't. I'll leave those topics for another article, where I can explain them more calmly. For now, I want to draw your attention, dear reader, to a few key changes that had to be made.
Let's start from the end so we can understand the beginning. Take a look at lines 47 to 49, they've been struck through and replaced by line 50. In other words, the OnCalculate event handler no longer receives all those parameters. You may be wondering why I've made this change. Especially after I previously mentioned that doing something like this wasn't a good idea. I'll explain the reason behind this tactical shift when I cover the service code, but not here. Now notice that line 52 has been replaced by line 53, and line 54 by lines 55 and 56. In addition to these, there's another change: the introduction of line 12. But before we get to line 12, let's take a closer look at what's happening in lines 55 and 56.
When the service updates the RATE (we'll see exactly how this is done later), MetaTrader 5 fires a Calculate event, which is then handled by the OnCalculate function. Alright. However, if the chart timeframe is not one minute, the rates_total value will NOT be updated until a new bar is formed. Since we'll be using the spread value only during replay/simulation, we check if the symbol matches the expected one. If that's true, we fetch the spread value using the iSpread function. Why we do this instead of just using the OnCalculate parameter, as well as the calculations in line 56, will be explained in the next article.
That said, because we're now using the iSpread function to retrieve the correct value, we no longer need to call OnCalculate with all its previous parameters. As a result, line 53 properly captures the price value. Now, here's the question: Which price? There's more than one we could use. And that's where line 12 comes in. It defines, as a property of the indicator, which price we'll actually be using. In this case, we've chosen to use the closing price of each bar. This keeps the mouse indicator compatible with the values previously displayed on the chart.
Final Thoughts
In this article, I covered some unresolved issues, which I'll address more deeply in upcoming entries. A number of things mentioned here may not make sense if you're only looking at this article. For example, why we're using iSpread instead of simply reading the spread value passed in through OnCalculate, which MetaTrader 5 already provides. There's also another topic we haven't discussed yet: what happens when the asset enters auction mode. This is a particularly odd case when working with a custom symbol. Not because the mouse indicator can't show when an asset is in auction mode. The issue lies elsewhere.
The functionality is already implemented and working. You can check it yourself by looking at the OnBookEvent function in the mouse indicator code. You'll see that we only need to report the value BOOK_TYPE_BUY_MARKET or BOOK_TYPE_SELL_MARKET for the mouse indicator to display that the asset is in auction. But I'll explain exactly how to do that in another article. That's all for now. See you in the next one.
Translated from Portuguese by MetaQuotes Ltd.
Original article: https://www.mql5.com/pt/articles/12309





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