Русский Español Português
preview
Developing a Replay System (Part 71): Getting the Time Right (IV)

Developing a Replay System (Part 71): Getting the Time Right (IV)

MetaTrader 5Examples |
2 101 0
Daniel Jose
Daniel Jose

Introduction

In the previous article, "Developing a Replay System (Part 70): Getting the Time Right (III)", I explained the required modifications in the mouse indicator. These changes were aimed at enabling the mouse indicator to receive order book events. This specifically refers to the case when it is being used alongside the replay/simulation application. You, dear reader, may have felt quite frustrated and confused by all those changes. I understand that many of them may not have made any sense at first. They were probably far more complex than I would like them to appear. Nevertheless, it is essential that you fully understand that content, no matter how confusing it may have seemed at first glance. I know that many of you likely struggled to understand what I was trying to convey in that article. However, without understanding that previous content (where I used a much simpler service to demonstrate how the whole mechanism works) trying to comprehend what will be explained here would be significantly more difficult. 

So, before diving into what we'll actually be doing in this article, make sure you've understood what was covered in the previous one. Especially the part about how, by adding order book events to the custom symbol, we gained the ability to use the OnCalculate function in a way that wasn't previously possible. This required us to use an iSpread call to retrieve the data that MetaTrader 5 makes available to us.

In this article, we will actually carry out the transfer (or more precisely, the transcription) of part of that code used in the test service, bringing it into the replay/simulation service. The main issue here isn't how we can do this, but rather how we should go about doing it.

Let me remind you, dear reader, that up until the previous article, where we were working on the replay/simulation service, the mouse indicator was loaded via a template. However, I will no longer be doing this. You may try to continue using the template to load the mouse indicator if you wish. But due to some practical considerations, we will start adding the mouse indicator manually to the chart of the replay/simulation symbol. So don't be surprised if, in some videos, I demonstrate things in this manner. I have my reasons, though I won't go into detail about them here. So, let's begin transcribing the code from the test service into the replay/simulation service.


Starting the Transcription

The first thing we do is modify a portion of the code in the header file C_Replay.mqh. Check out the following snippet:

197. //+------------------------------------------------------------------+
198.       bool InitBaseControl(const ushort wait = 1000)
199.          {
200.             Print("Waiting for Mouse Indicator...");
201.             Sleep(wait);
202.             while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200);
203.             if (def_CheckLoopService)
204.             {
205.                AdjustViewDetails();
206.                Print("Waiting for Control Indicator...");
207.                if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false;
208.                ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle);
209.                UpdateIndicatorControl();
210.             }
211.             
212.             return def_CheckLoopService;
213.          }
214. //+------------------------------------------------------------------+

Code from the C_Terminal.mqh file

This fragment is the original code. Now, I want you to pay attention to the following point. This code was originally designed to work when the mouse indicator was loaded via a template. However, since the mouse indicator will now be placed manually on the chart, this code is no longer appropriate. Technically, it would still work. But we can improve it to provide a more suitable configuration in terms of both execution flow and the messages presented. So here is the new code that will be used:

197. //+------------------------------------------------------------------+
198.       bool InitBaseControl(const ushort wait = 1000)
199.          {
200.             Sleep(wait);
201.             AdjustViewDetails();
202.             Print("Loading Control Indicator...");
203.             if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false;
204.             ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle);
205.             Print("Waiting for Mouse Indicator...");
206.             while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200);
207.             UpdateIndicatorControl();
208.             
209.             return def_CheckLoopService;
210.          }
211. //+------------------------------------------------------------------+

Code from the C_Replay.mqh file

Fundamentally, very little changes in the code itself. But the messages now give a clearer picture of what's happening. Also, the execution order has been reversed. Now, we will first attempt to load the control indicator, which is part of the replay/simulation application. This is because the control indicator is embedded within the replay/simulator executable. Only after that will we load the mouse indicator. Take note of the following: If the control indicator, which is included in the application, fails to load, this signals a critical issue. If it loads successfully, we can then inform the user that the mouse indicator also needs to be loaded. In my view, this is a more appropriate workflow. That said, feel free to adjust the loading order if you prefer. Either way, the control indicator won't actually function unless the mouse indicator is present on the chart.

This change is actually of an aesthetic nature. Let's now move on to the changes that will actually support order book messages. If you didn't understand the previous article, go back to it and review the material using previous code. Don't try to understand how everything works using the code that will be shown from this point on. If you try to do this, you will find yourself in complete confusion.

We need to add a new line to the class constructor. This new line can be seen in the code below:

149. //+------------------------------------------------------------------+
150.       C_Replay()
151.          :C_ConfigService()
152.          {
153.             Print("************** Market Replay Service **************");
154.             srand(GetTickCount());
155.             SymbolSelect(def_SymbolReplay, false);
156.             CustomSymbolDelete(def_SymbolReplay);
157.             CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay));
158.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0);
159.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0);
160.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0);
161.             CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation");
162.             CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8);
163.             CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TICKS_BOOKDEPTH, 1);
164.             SymbolSelect(def_SymbolReplay, true);
165.             m_Infos.CountReplay = 0;
166.             m_IndControl.Handle = INVALID_HANDLE;
167.             m_IndControl.Mode = C_Controls::ePause;
168.             m_IndControl.Position = 0;
169.             m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState;
170.          }
171. //+------------------------------------------------------------------+

Code from the C_Replay.mqh file

The new line is exactly line 163. Once this is done, we can use the order book messages. Now pay attention to one thing. The important point is not in the C_Replay.mqh header file, but in the mouse indicator. Therefore, let's take an abstract from the indicator code to better understand how it works. Because it's important.

27. //+------------------------------------------------------------------+
28. int OnInit()
29. {
30.    Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04);
31.    if (_LastError >= ERR_USER_ERROR_FIRST) return INIT_FAILED;
32.    MarketBookAdd((*Study).GetInfoTerminal().szSymbol);
33.    OnBookEvent((*Study).GetInfoTerminal().szSymbol);
34.    m_Status = C_Study::eCloseMarket;
35.    SetIndexBuffer(0, m_Buff, INDICATOR_DATA);
36.    ArrayInitialize(m_Buff, EMPTY_VALUE);
37.    
38.    return INIT_SUCCEEDED;
39. }
40. //+------------------------------------------------------------------+

Mouse pointer file fragment

Note that on line 34, we're setting an initial status for the mouse indicator. This status indicates that the market is closed. But we're not dealing with the real market here. We're working within an application whose primary objective is to allow replay or simulation of potential market movements. So, the message currently displayed by the mouse indicator - when we're on the custom symbol used for replay/simulation - is incorrect. Fortunately, this is very, very easy to fix. But before we fix it, you need to understand that the moment the control indicator is placed on the chart, the replay/simulation is effectively in a paused state. This applies when the application is initialized from the beginning. And now we face a decision that will significantly impact what happens next.

Let's think this through: When we're in pause mode, should the mouse indicator show an auction message? Or should it display the time remaining in the current bar? If we decide to show the remaining time, should that information appear before the first play is triggered, or only after? This might sound a bit confusing, so let's clarify. Before the market officially opens, an auction phase takes place. This allows participants to place their orders at the best possible prices, i.e. the prices at which they truly want to buy or sell. So, as soon as the replay/simulation application causes MetaTrader 5 to load the chart and the mouse indicator becomes visible, the message we should see is an auction message. It is a fact. Now, when you enable the simulation or replay and then pause it again, what should the message say? Should it still display "auction"? Or should it now show the time remaining in the current bar? That's the question we need to think through. Regardless, the first thing we need to ensure is that when the application starts, it clearly indicates that we are in an auction phase.

Doing this is actually quite simple. See the code snippet below.

212. //+------------------------------------------------------------------+
213.       bool LoopEventOnTime(void)
214.          {         
215.             int iPos, iCycles;
216.             MqlBookInfo book[1];
217.             
218.             book[0].price = 1.0;
219.             book[0].volume = 1;
220.             book[0].type = BOOK_TYPE_BUY_MARKET;
221.             CustomBookAdd(def_SymbolReplay, book, 1);            
222.             while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay))
223.             {
224.                UpdateIndicatorControl();
225.                Sleep(200);
226.             }
227.             m_MemoryData = GetInfoTicks();
228.             AdjustPositionToReplay();
229.             iPos = iCycles = 0;
230.             while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService))
231.             {
232.                if (m_IndControl.Mode == C_Controls::ePause) return true;
233.                iPos += (int)(m_Infos.CountReplay < (m_MemoryData.nTicks - 1) ? m_MemoryData.Info[m_Infos.CountReplay + 1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : 0);
234.                CreateBarInReplay(true);
235.                while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause))
236.                {
237.                   Sleep(195);
238.                   iPos -= 200;
239.                   m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks);
240.                   UpdateIndicatorControl();
241.                   iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1);
242.                }
243.             }
244. 
245.             return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService));
246.          }
247. };
248. //+------------------------------------------------------------------+

Code from the C_Replay.mqh file

Here's the easy part of what we actually need to implement. I'll be introducing the implementation gradually, so you can truly follow along with what's being done. Take a look at line 216 where we have a new variable. It's an array with a single element. And here comes the interesting part.

You're probably assuming that the values injected into a book event need to carry some meaningful significance. But the reality is, they don't need to mean anything at all. We just need to ensure the values used to trigger the book event follow some internal logic. But they don't have to be meaningful, unless your goal is to simulate an actual order book. That, however, is not my intent. At least not for now. Maybe in the future.

In any case, lines 218 and 219 are used to give MetaTrader 5 something to populate the book. These values don't carry any special meaning. They simply exist to support what I truly care about, which is line 220. In line 220, we inform the book that we have a position indicating an auction state. If that doesn't make sense, I recommend revisiting the previous article to better understand this behavior. Then, in line 221, we tell MetaTrader 5 to trigger a custom book event. This event is captured by the OnEventBook function, which in this case resides in the mouse indicator. Result: Every time the LoopEventOnTime function is entered, the mouse indicator will indeed display that we are in an auction. There are two scenarios in which LoopEventOnTime runs from the beginning: the first case is when the application is first initialized. The second case occurs when the user interacts with the control indicator by pressing the pause button. In that case, line 232 is executed, and immediately afterward, the LoopEventOnTime function is re-executed. Have you noticed how easy it is? Now that we already have the auction message displaying, how do we show the time remaining in the bars? This is actually quite simple. To do that, we need to modify the code as follows:

212. //+------------------------------------------------------------------+
213.       bool LoopEventOnTime(void)
214.          {         
215.             int iPos, iCycles;
216.             MqlBookInfo book[1];
217.             
218.             book[0].price = 1.0;
219.             book[0].volume = 1;
220.             book[0].type = BOOK_TYPE_BUY_MARKET;
221.             CustomBookAdd(def_SymbolReplay, book, 1);
222.             while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay))
223.             {
224.                UpdateIndicatorControl();
225.                Sleep(200);
226.             }
227.             m_MemoryData = GetInfoTicks();
228.             AdjustPositionToReplay();
229.             iPos = iCycles = 0;
230.             book[0].type = BOOK_TYPE_BUY;
231.             CustomBookAdd(def_SymbolReplay, book, 1);
232.             while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService))
233.             {
234.                if (m_IndControl.Mode == C_Controls::ePause) return true;
235.                iPos += (int)(m_Infos.CountReplay < (m_MemoryData.nTicks - 1) ? m_MemoryData.Info[m_Infos.CountReplay + 1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : 0);
236.                CreateBarInReplay(true);
237.                while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause))
238.                {
239.                   Sleep(195);
240.                   iPos -= 200;
241.                   m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks);
242.                   UpdateIndicatorControl();
243.                   iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1);
244.                }
245.             }
246. 
247.             return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService));
248.          }
249. };
250. //+------------------------------------------------------------------+

Code from the C_Replay.mqh file

Did you notice the difference? If you didn't notice, it's probably because you were too distracted - because the difference lies precisely in the inclusion of lines 230 and 231. These two lines ensure that, the moment the user presses play on the replay/simulation service, the mouse indicator receives a custom order book event. This event signals that we've exited the auction state and entered active trading. As a result, the remaining time of the current bar will begin to display on the mouse indicator. As you can see, all is pretty simple. However, we now face a slightly more complex condition to handle.

In the real market, i.e., when we're connected to the trading server, there are moments when a security may be suspended or enter an auction. This typically happens due to specific regulatory conditions. I discussed this in the previous article. But for now, we'll implement a simplified rule. If the asset, between one tick and the next, has a time gap equal to or greater than 60 seconds, the mouse indicator will display that the asset has entered an auction. This is actually quite simple. The tricky part is this: How do we get the mouse indicator to go back to showing the remaining time of the bar afterward?

You might say: "When the asset enters auction mode, we send the BOOK_TYPE_BUY_MARKET constant to the book; and when it exits auction, we send BOOK_TYPE_BUY." Exactly, that's what we need to do. But how do we do that properly? Let's think this through: We don't want the LoopEventOnTime function to restart from the beginning. We want the system to keep running within the loop that starts at line 232 and ends at line 245. Now, if you send both BOOK_TYPE_BUY_MARKET and BOOK_TYPE_BUY from within that loop, you'll run into problems. That's because each call to CustomBookAdd() with those differing constants will definitely produce an unpleasant visual result for the user watching the mouse indicator. It will flicker and alternate rapidly between the remaining time and the word AUCTION.

So, because of this, we need to get creative. We must implement a solution that avoids that flickering effect, while still solving the problem effectively. The solution I propose is shown below:

212. //+------------------------------------------------------------------+
213.       bool LoopEventOnTime(void)
214.          {         
215.             int iPos, iCycles;
216.             MqlBookInfo book[1];
217.             ENUM_BOOK_TYPE typeMsg;
218.             
219.             book[0].price = 1.0;
220.             book[0].volume = 1;
221.             book[0].type = BOOK_TYPE_BUY_MARKET;
222.             CustomBookAdd(def_SymbolReplay, book, 1);
223.             while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay))
224.             {
225.                UpdateIndicatorControl();
226.                Sleep(200);
227.             }
228.             m_MemoryData = GetInfoTicks();
229.             AdjustPositionToReplay();
230.             iPos = iCycles = 0;
231.             book[0].type = BOOK_TYPE_BUY;
232.             CustomBookAdd(def_SymbolReplay, book, 1);
233.             while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService))
234.             {
235.                if (m_IndControl.Mode == C_Controls::ePause) return true;
236.                iPos += (int)(m_Infos.CountReplay < (m_MemoryData.nTicks - 1) ? m_MemoryData.Info[m_Infos.CountReplay + 1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : 0);
237.                if ((typeMsg = (iPos >= 60000 ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY)) != book[0].type)
238.                {
239.                   book[0].type = typeMsg;
240.                   CustomBookAdd(def_SymbolReplay, book, 1);                  
241.                }
242.                CreateBarInReplay(true);
243.                while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause))
244.                {
245.                   Sleep(195);
246.                   iPos -= 200;
247.                   m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks);
248.                   UpdateIndicatorControl();
249.                   iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1);
250.                }
251.             }
252. 
253.             return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService));
254.          }
255. };
256. //+------------------------------------------------------------------+

Code from the C_Replay.mqh file

I know it's not exactly elegant, but at least it works. And if you want, you're free to test it under different conditions by increasing or decreasing the timing thresholds. That's entirely up to you. But before jumping in and changing things, how about we take a moment to understand what's happening in this code snippet?

First, take note of the following: at line 217, I declare a new variable. This variable is used to hold one of the possible constants accepted by the order book. Then at line 237, I use the ternary operator to streamline the logic, since the idea is to evaluate a condition and, based on that, assign a value to the typeMsg variable. Now pay attention: I could have condensed this code even more, but that would have made the explanation unnecessarily complex. Here's how it works. After a constant is assigned to typeMsg, we check whether its value is different from the last value sent as a custom book event. If it is different, then at line 239, we assign the new constant to be used, and at line 240, we call CustomBookAdd. The part you really need to focus on is the value being compared to the iPos variable, specifically at line 237. Notice that we're comparing it to the value 60000 (sixty thousand). But why this value? Weren't we using a one-minute threshold? Yes, but you might be forgetting this simple fact - one minute equals 60 seconds. Now, look at line 236 - the value assigned to iPos is in milliseconds. And there are 1,000 milliseconds in a single second. That's why we're comparing iPos with 60,000. It reflects a 60-second interval, expressed in milliseconds. So, if you decide to change this threshold to something else you feel is more appropriate, just be sure to convert it properly into milliseconds. Otherwise, the mouse indicator might show unexpected behavior, especially when the asset goes for a while without ticks being added to the chart.

With that, we can finally view the final version of the C_Replay.mqh code file. At this stage, all aspects related to timing are fully implemented, at least to a degree where the end result behaves as demonstrated in the video shown below.


The full source code of the C_Replay.mqh header file is presented below:

001. //+------------------------------------------------------------------+
002. #property copyright "Daniel Jose"
003. //+------------------------------------------------------------------+
004. #include "C_ConfigService.mqh"
005. #include "C_Controls.mqh"
006. //+------------------------------------------------------------------+
007. #define def_IndicatorControl   "Indicators\\Market Replay.ex5"
008. #resource "\\" + def_IndicatorControl
009. //+------------------------------------------------------------------+
010. #define def_CheckLoopService ((!_StopFlag) && (ChartSymbol(m_Infos.IdReplay) != ""))
011. //+------------------------------------------------------------------+
012. #define def_ShortNameIndControl   "Market Replay Control"
013. #define def_MaxSlider             (def_MaxPosSlider + 1)
014. //+------------------------------------------------------------------+
015. class C_Replay : public C_ConfigService
016. {
017.    private   :
018.       struct st00
019.       {
020.          C_Controls::eObjectControl Mode;
021.            uCast_Double             Memory;
022.          ushort                     Position;
023.          int                        Handle;
024.       }m_IndControl;
025.       struct st01
026.       {
027.          long     IdReplay;
028.          int      CountReplay;
029.          double   PointsPerTick;
030.          MqlTick  tick[1];
031.          MqlRates Rate[1];
032.       }m_Infos;
033.       stInfoTicks m_MemoryData;
034. //+------------------------------------------------------------------+
035. inline bool MsgError(string sz0) { Print(sz0); return false; }
036. //+------------------------------------------------------------------+
037. inline void UpdateIndicatorControl(void)
038.          {
039.             double Buff[];
040.                                  
041.             if (m_IndControl.Handle == INVALID_HANDLE) return;
042.             if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position)
043.             {
044.                if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1)
045.                   m_IndControl.Memory.dValue = Buff[0];
046.                if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay)
047.                   m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition];
048.             }else
049.             {
050.                m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position;
051.                m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode;
052.                m_IndControl.Memory._8b[7] = 'D';
053.                m_IndControl.Memory._8b[6] = 'M';
054.                EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, "");
055.             }
056.          }
057. //+------------------------------------------------------------------+
058.       void SweepAndCloseChart(void)
059.          {
060.             long id;
061.             
062.             if ((id = ChartFirst()) > 0) do
063.             {
064.                if (ChartSymbol(id) == def_SymbolReplay)
065.                   ChartClose(id);
066.             }while ((id = ChartNext(id)) > 0);
067.          }
068. //+------------------------------------------------------------------+
069. inline int RateUpdate(bool bCheck)
070.          {
071.             static int st_Spread = 0;
072. 
073.             st_Spread = (bCheck ? (int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time) : st_Spread + 1);
074.             m_Infos.Rate[0].spread = (int)(def_MaskTimeService | st_Spread);
075.             CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate);
076.             
077.             return 0;
078.          }
079. //+------------------------------------------------------------------+
080. inline void CreateBarInReplay(bool bViewTick)
081.          {
082.             bool   bNew;
083.             double dSpread;
084.             int    iRand = rand();
085. 
086.             if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew))
087.             {
088.                m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay];
089.                if (m_MemoryData.ModePlot == PRICE_EXCHANGE)
090.                {                  
091.                   dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 );
092.                   if (m_Infos.tick[0].last > m_Infos.tick[0].ask)
093.                   {
094.                      m_Infos.tick[0].ask = m_Infos.tick[0].last;
095.                      m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread;
096.                   }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid)
097.                   {
098.                      m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread;
099.                      m_Infos.tick[0].bid = m_Infos.tick[0].last;
100.                   }
101.                }
102.                if (bViewTick)
103.                   CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
104.                RateUpdate(true);
105.             }
106.             m_Infos.CountReplay++;
107.          }
108. //+------------------------------------------------------------------+
109.       void AdjustViewDetails(void)
110.          {
111.             MqlRates rate[1];
112. 
113.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX);
114.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX);
115.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE);
116.             m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
117.             CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate);
118.             if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE))
119.                for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++);
120.             if (rate[0].close > 0)
121.             {
122.                if (GetInfoTicks().ModePlot == PRICE_EXCHANGE)
123.                   m_Infos.tick[0].last = rate[0].close;
124.                else
125.                {
126.                   m_Infos.tick[0].bid = rate[0].close;
127.                   m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick);
128.                }               
129.                m_Infos.tick[0].time = rate[0].time;
130.                m_Infos.tick[0].time_msc = rate[0].time * 1000;
131.             }else
132.                m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay];
133.             CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
134.          }
135. //+------------------------------------------------------------------+
136.       void AdjustPositionToReplay(void)
137.          {
138.             int nPos, nCount;
139.             
140.             if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return;
141.             nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider);
142.             for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread);
143.             if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1);
144.             while ((nPos > m_Infos.CountReplay) && def_CheckLoopService)
145.                CreateBarInReplay(false);
146.          }
147. //+------------------------------------------------------------------+
148.    public   :
149. //+------------------------------------------------------------------+
150.       C_Replay()
151.          :C_ConfigService()
152.          {
153.             Print("************** Market Replay Service **************");
154.             srand(GetTickCount());
155.             SymbolSelect(def_SymbolReplay, false);
156.             CustomSymbolDelete(def_SymbolReplay);
157.             CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay));
158.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0);
159.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0);
160.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0);
161.             CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation");
162.             CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8);
163.             CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TICKS_BOOKDEPTH, 1);
164.             SymbolSelect(def_SymbolReplay, true);
165.             m_Infos.CountReplay = 0;
166.             m_IndControl.Handle = INVALID_HANDLE;
167.             m_IndControl.Mode = C_Controls::ePause;
168.             m_IndControl.Position = 0;
169.             m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState;
170.          }
171. //+------------------------------------------------------------------+
172.       ~C_Replay()
173.          {
174.             SweepAndCloseChart();
175.             IndicatorRelease(m_IndControl.Handle);
176.             SymbolSelect(def_SymbolReplay, false);
177.             CustomSymbolDelete(def_SymbolReplay);
178.             Print("Finished replay service...");
179.          }
180. //+------------------------------------------------------------------+
181.       bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate)
182.          {
183.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0)
184.                return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket.");
185.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0)
186.                return MsgError("Asset configuration is not complete, need to declare the ticket value.");
187.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0)
188.                return MsgError("Asset configuration not complete, need to declare the minimum volume.");
189.             SweepAndCloseChart();
190.             m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1);
191.             if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl"))
192.                Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl");
193.             else
194.                Print("Apply template: ", szNameTemplate, ".tpl");
195. 
196.             return true;
197.          }
198. //+------------------------------------------------------------------+
199.       bool InitBaseControl(const ushort wait = 1000)
200.          {
201.             Sleep(wait);
202.             AdjustViewDetails();
203.             Print("Loading Control Indicator...");
204.             if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false;
205.             ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle);
206.             Print("Waiting for Mouse Indicator...");
207.             while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200);
208.             UpdateIndicatorControl();
209.             
210.             return def_CheckLoopService;
211.          }
212. //+------------------------------------------------------------------+
213.       bool LoopEventOnTime(void)
214.          {         
215.             int iPos, iCycles;
216.             MqlBookInfo book[1];
217.             ENUM_BOOK_TYPE typeMsg;
218.             
219.             book[0].price = 1.0;
220.             book[0].volume = 1;
221.             book[0].type = BOOK_TYPE_BUY_MARKET;
222.             CustomBookAdd(def_SymbolReplay, book, 1);
223.             while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay))
224.             {
225.                UpdateIndicatorControl();
226.                Sleep(200);
227.             }
228.             m_MemoryData = GetInfoTicks();
229.             AdjustPositionToReplay();
230.             iPos = iCycles = 0;
231.             book[0].type = BOOK_TYPE_BUY;
232.             CustomBookAdd(def_SymbolReplay, book, 1);
233.             while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService))
234.             {
235.                if (m_IndControl.Mode == C_Controls::ePause) return true;
236.                iPos += (int)(m_Infos.CountReplay < (m_MemoryData.nTicks - 1) ? m_MemoryData.Info[m_Infos.CountReplay + 1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : 0);
237.                if ((typeMsg = (iPos >= 60000 ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY)) != book[0].type)
238.                {
239.                   book[0].type = typeMsg;
240.                   CustomBookAdd(def_SymbolReplay, book, 1);                  
241.                }
242.                CreateBarInReplay(true);
243.                while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause))
244.                {
245.                   Sleep(195);
246.                   iPos -= 200;
247.                   m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks);
248.                   UpdateIndicatorControl();
249.                   iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1);
250.                }
251.             }
252. 
253.             return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService));
254.          }
255. };
256. //+------------------------------------------------------------------+
257. #undef def_SymbolReplay
258. #undef def_CheckLoopService
259. #undef def_MaxSlider
260. //+------------------------------------------------------------------+

Source code of the C_Replay.mqh file


This is a rather interesting but harmless bug.

Alright. Alright, although the system we've just built and demonstrated works beautifully, there is still an issue. Even though I didn't show this in the video, I believe that if you think things through carefully, you'll notice a flaw in the current implementation. But before I explain what the problem is, let's take a moment to check how well you understand how MetaTrader 5 works. If you don't have a solid understanding of how MetaTrader 5 operates under different conditions, chances are you haven't noticed this issue. That's because the flaw lies in something quite specific: you CANNOT CHANGE THE CHART TIMEFRAME while using the replay/simulation system.

Now you're probably asking: "What do you mean I can't change the chart timeframe? What happens if I try to?" Well, here's exactly what happens - and yes, it will happen 100% of the time - the flaw in our system will be triggered. However, this flaw only appears under very specific circumstances. You'll only trigger it if the replay/simulation system is running in play mode. If it is in pause mode or if the system detects that the symbol is on auction based on the number of trades, the error will not be raised. Again, this will only happen if we are in play mode and ticks are being added to the chart.

Let me ask again, especially for those who've been following this article series on building a replay/simulator system in MQL5: Do you have any idea what the flaw might be? If your answer is yes - great! That's a sign you've been studying and digging into how both MQL5 and MetaTrader 5 work. If your answer is no - no worries! That just means you're still at an early stage of your learning journey. Let this be motivation to continue studying.

Let's take a look at what this bug actually is. It is minor, harmless, and only shows up if you're in play mode, where ticks are actively being processed. As soon as you change the chart timeframe, something interesting happens. If all the conditions mentioned above are met, and you change the timeframe, the mouse indicator suddenly shows that the market is closed. You'll also stop seeing the time remaining in the current bar. To fix it, you'll have to pause and then resume the simulation.


Now you might be thinking: “Why does changing the timeframe make the indicator say the market is closed? That doesn't make sense." And I would agree with you. But if you have worked with MetaTrader 5 long enough, you probably already know the reason. MetaTrader 5 unloads all indicators and other elements from the chart. Then it refreshes the chart data based on the new timeframe and reloads the indicators (and other elements) from scratch. That's how MetaTrader 5 operates. So, when the mouse indicator is reloaded, it starts from its default state. To see exactly what I mean, take a look at the following code snippet taken directly from the mouse indicator:

27. //+------------------------------------------------------------------+
28. int OnInit()
29. {
30.    Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04);
31.    if (_LastError >= ERR_USER_ERROR_FIRST) return INIT_FAILED;
32.    MarketBookAdd((*Study).GetInfoTerminal().szSymbol);
33.    OnBookEvent((*Study).GetInfoTerminal().szSymbol);
34.    m_Status = C_Study::eCloseMarket;
35.    SetIndexBuffer(0, m_Buff, INDICATOR_DATA);
36.    ArrayInitialize(m_Buff, EMPTY_VALUE);
37.    
38.    return INIT_SUCCEEDED;
39. }
40. //+------------------------------------------------------------------+

Mouse pointer file fragment

Now observe the following: in line 34 of this snippet, we initialize the value of the status variable This value effectively indicates that the market is closed. However, ticks continue to arrive from the replay/simulation service. And this is exactly where the flaw lies. As you can see, it's harmless and doesn't cause more serious issues. It's merely an inconvenience that can be resolved simply by pausing and then resuming the service. Once you do that, everything returns to normal.

You might imagine a thousand ways to fix this. But the solution I'll show is far more unusual than you might expect. Before we get to that, let me ask: Do you know why simply pausing and then resuming the simulation causes the issue to disappear? If not, let's walk through why this resolves the issue. To understand it, we need to revisit the code in the C_Replay.mqh header file, specifically its LoopEventOnTime function. When the bug occurs, the mouse indicator shows that the market is closed. Once you trigger the pause mode, line 235 breaks the loop that begins at line 233. This forces the function to return to the main body of the code. Since the return value is true, the main routine of the service immediately calls LoopEventOnTime again. At this point, line 222 triggers a custom order book event, using the value defined at line 221. As a result, the mouse indicator updates its status from "market closed" to "auction mode". Then, when the user resumes the simulation by hitting play again, line 232 triggers another custom order book event. But now it will use the constant from line 231. This constant allows the mouse indicator to once again display the remaining time in the current bar.

That's why, when the bug is triggered due to a change in chart timeframe, simply pausing and then resuming is enough to restore normal functionality.

However, while this issue doesn't significantly affect the mouse indicator, it does cause problems for another component: the control indicator. In this case, there's no simple workaround. We have to wait for the condition checked on line 42 of the C_Replay.mqh header file to fail. Why is that? Why do we need this condition to fail in order for the control indicator to reappear on the chart? Because only when that check fails, line 54 gets executed. And we need line 54 to run in order for the control indicator to be updated. Once updated, the control indicator will be plotted again, assuming it was previously hidden for any reason.

Alright. Now you probably realize that the situation is getting more and more complicated. While the mouse indicator glitch can be resolved simply by toggling from play to pause and back, if the control indicator isn't visible, we can't even do that. How do we fix this? Without the control indicator, we can't pause the application, and the service will continue to push ticks to the chart. And unless the service stops adding ticks, we won't reach a condition where the test in line 42 fails. That will only happen once a certain number of ticks have been added to the chart. Now this is a serious problem.

You might think, "I just need to monitor the chart timeframe in the service. If it changes, I'll trigger a reinitialization of the mouse and control indicators." Alright. But do you know how to detect a change in chart timeframe within the service? Perhaps it's possible. But I assure you, it would be far more complicated, than the solution I'll share. Not in this article, but in the next one. Because I want to give you, dear reader, a chance to try solving this yourself first.


Conclusion

In this article, I showed you how to repurpose the code from the previous article, which was used to test the functionality of custom order book messages, and integrate it into a service that we've been developing for some time. I also demonstrated how small, seemingly harmless bugs in your implementation can lead to big headaches. 

However, I hope you are curious about how to fix the chart timeframe issue. Specifically, how to make the service detect when the chart timeframe has changed. I will show the solution in the next article.

Being a programmer means finding solutions when others see no path forward. Problems will always exist. Without them, programming wouldn't be that fun or rewarding. So, the more problems, the better. And the more complex they are, the more fulfilling it is to solve them. They push us to think outside the box. They break us free from the monotony of doing things the same way over and over again. 

I'll see you in the next article. And before you read that next article, try to come up with a solution for the chart timeframe change issue.

Translated from Portuguese by MetaQuotes Ltd.
Original article: https://www.mql5.com/pt/articles/12335

Attached files |
Anexo.zip (420.65 KB)
MQL5 Wizard Techniques you should know (Part 68):  Using Patterns of TRIX and the Williams Percent Range with a Cosine Kernel Network MQL5 Wizard Techniques you should know (Part 68): Using Patterns of TRIX and the Williams Percent Range with a Cosine Kernel Network
We follow up our last article, where we introduced the indicator pair of TRIX and Williams Percent Range, by considering how this indicator pairing could be extended with Machine Learning. TRIX and William’s Percent are a trend and support/ resistance complimentary pairing. Our machine learning approach uses a convolution neural network that engages the cosine kernel in its architecture when fine-tuning the forecasts of this indicator pairing. As always, this is done in a custom signal class file that works with the MQL5 wizard to assemble an Expert Advisor.
Data Science and ML (Part 42): Forex Time series Forecasting using ARIMA in Python, Everything you need to Know Data Science and ML (Part 42): Forex Time series Forecasting using ARIMA in Python, Everything you need to Know
ARIMA, short for Auto Regressive Integrated Moving Average, is a powerful traditional time series forecasting model. With the ability to detect spikes and fluctuations in a time series data, this model can make accurate predictions on the next values. In this article, we are going to understand what is it, how it operates, what you can do with it when it comes to predicting the next prices in the market with high accuracy and much more.
Automating Trading Strategies in MQL5 (Part 19): Envelopes Trend Bounce Scalping — Trade Execution and Risk Management (Part II) Automating Trading Strategies in MQL5 (Part 19): Envelopes Trend Bounce Scalping — Trade Execution and Risk Management (Part II)
In this article, we implement trade execution and risk management for the Envelopes Trend Bounce Scalping Strategy in MQL5. We implement order placement and risk controls like stop-loss and position sizing. We conclude with backtesting and optimization, building on Part 18’s foundation.
Introduction to MQL5 (Part 17): Building Expert Advisors for Trend Reversals Introduction to MQL5 (Part 17): Building Expert Advisors for Trend Reversals
This article teaches beginners how to build an Expert Advisor (EA) in MQL5 that trades based on chart pattern recognition using trend line breakouts and reversals. By learning how to retrieve trend line values dynamically and compare them with price action, readers will be able to develop EAs capable of identifying and trading chart patterns such as ascending and descending trend lines, channels, wedges, triangles, and more.