Developing a Replay System — Market simulation (Part 10): Using only real data for Replay
Introduction
In the previous article, Developing a Replay System — Market simulation (Part 09): Custom events, we looked at how to trigger custom events. We also implemented a very interesting system from the point of view of the indicator interaction with the service. In this article I will emphasize the importance of maintaining traded ticks. If you haven't tried this practice yet, you should seriously consider doing it daily. Record all traded values of the asset that you need to study in detail.
It's pointless to look for a miracle solution to recover lost information. Once information is lost, it is impossible to get it back, no matter what method is used. If your goal is to really dig deep and understand a particular asset, don't put it off until later. Start storing traded ticks in a safe place as soon as possible, as they are priceless. Over time, you will understand that the bar data may not coincide with the values of the traded ticks. This discrepancy can make it difficult to understand, especially for those less familiar with the financial market.
Sometimes certain events affect the values for example because 1-minute bars do not reflect real operations. There are different events, like earnings announcements, grouping or split of an asset, the expiration of an asset (in the case of futures), issuance of bonds and so on. These are all examples of situations that can distort the current value of 1-minute bars, making them inconsistent with the reality that is being traded at that time or period.
This is due to the fact that when these events occur, the price of the asset is adjusted in a certain way. No matter how hard we try, we will not be able to make the adjustment that equates 1-minute bars to traded ticks. This is why ticks are priceless.
NOTE: Some may say that if we add up the difference, the values will be equivalent. However, this is not the question. The dilemma is that if the market perceives the price to be in a certain range, it will react in a certain way. If we perceive it in another, then the response is different.
Therefore, we have to implement adaptation in our replay/simulation system to use exclusively traded tick files. Although we have done this since the beginning of this series, we still do not actually use the data contained in traded ticks except to create replay. However, the situation will soon change. Now we will also allow these files and, therefore, the data they contain to be used to create preview bars. This will replace the current method where we only use files with 1 minute preview bar data.
Code development
Most of the code has already been implemented, so we won't have to do much work in this aspect. However, we need to think about the configuration file used during replay/simulation. As we started using this file, it became obvious that it was of critical importance to us. The problem (although not really a problem) is that this file has two structures: one structure to list the traded ticks that will be used to create the replay/simulation, and another to indicate the bars that will be used as movement previews, which will not be included in the simulation but will only be added to the asset.
This allows you to correctly separate and correct the information, but now we are faced with a small problem: if the tick file is included in the bar structure, the system will issue an error warning. We want to keep it this way so that there is no risk of creating a replay/simulation where things get out of control due to a typo when creating the config file. The same thing happens if we try to add a 1-minute bar file as if it were a traded tick file: the system will simply treat this as an error.
How then can we solve part of this dilemma and use tick files as preview bars so that the system does not perceive this as an error? Here I will offer one of several possible working solutions. The first step is to add a new definition to the C_Replay.mqh header file.
#define def_STR_FilesBar "[BARS]" #define def_STR_FilesTicks "[TICKS]" #define def_STR_TicksToBars "[TICKS->BARS]" // ... The rest of the code...
This definition will help determine when one or more tick files should be treated as bar files. And this will make the next steps easier. However, there is an additional detail: we will not limit ourselves to adding this definition. We will also allow the user to change it like other definitions, but in a more elegant way. This will potentially make everything clearer and more understandable for the user. Anyway, there will be no big changes for the system, since it will continue to interpret everything correctly.
Thus, if the user decides to enter the following definition into the configuration file:
[ TICKS -> BARS ]
This should be understood as a valid definition. At the same time, we have expanded the parameters a little to make them easier for the user to understand. The reason is that some people prefer not to group data but to separate it logically, which is perfectly acceptable and we can allow it. To provide this flexibility to allow the user to slightly "change" the provided definitions, we will need to add some details to the service code. This will make the code clearer and allow us to extend future functionality as easily as possible. To do this, we will create an enumerator. However, there is a detail that can be seen below:
class C_Replay { private : enum eTranscriptionDefine {Transcription_FAILED, Transcription_INFO, Transcription_DEFINE}; int m_ReplayCount; datetime m_dtPrevLoading; // ... The rest of the code...
This enumerator is private to the C_Replay class, meaning it is not accessible outside the class. While we enable this by placing it in a private declaration block, we also gain the ability to easily increase the complexity of the configuration file. However, we will cover this detail in future articles within this series as it is too extensive for this article.
After that, we can focus on the next item to implement. This item is actually a function that allows us to define the classification of the data contained in the configuration file. Let's take a look at how this process works. We will use the following code here:
inline eTranscriptionDefine GetDefinition(const string &In, string &Out) { string szInfo; szInfo = In; Out = ""; StringToUpper(szInfo); StringTrimLeft(szInfo); StringTrimRight(szInfo); if (StringSubstr(szInfo, 0, 1) != "[") { Out = szInfo; return Transcription_INFO; } for (int c0 = 0; c0 < StringLen(szInfo); c0++) if (StringGetCharacter(szInfo, c0) > ' ') StringAdd(Out, StringSubstr(szInfo, c0, 1)); return Transcription_DEFINE; }
Here we intend to centralize the entire process of preliminary conversion of data from the configuration file. Regardless of how the subsequent work goes, the above function will do all the preliminary checking to ensure that the received data matches a certain pattern. The first thing to do before this is to convert all characters to uppercase. After this, we remove all elements that are not needed for subsequent steps. Once this is done, we check the first character of the sequence, and if it is different from "[", we will get an indication that the sequence represents some information rather than a definition.
In this particular case we will simply return the result of the previously executed process. If this is not the case, then we will start looking for these definitions. Even if they are in a different format, the content may be appropriate and correct. When reading, we must ignore any character whose value is less than a space. Thus, even if we write: [ B A R S ], the system interprets [BARS]. In this case, the input may have slight differences in the way it is written, but as long as the content matches the expectations, we will get the corresponding result.
We now have a new system for reading and configuring based on the contents of the configuration file.
bool SetSymbolReplay(const string szFileConfig) { int file; string szInfo; bool isBars = true; if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { MessageBox("Failed to open\nconfiguration file.", "Market Replay", MB_OK); return false; } Print("Loading data for replay. Please wait...."); ArrayResize(m_Ticks.Rate, def_BarsDiary); m_Ticks.nRate = -1; m_Ticks.Rate[0].time = 0; while ((!FileIsEnding(file)) && (!_StopFlag)) switch (GetDefinition(FileReadString(file), szInfo)) { case Transcription_DEFINE: if (szInfo == def_STR_FilesBar) isBars = true; else if (szInfo == def_STR_FilesTicks) isBars = false; break; case Transcription_INFO: if (szInfo != "") if (!(isBars ? LoadPrevBars(szInfo) : LoadTicksReplay(szInfo))) { if (!_StopFlag) MessageBox(StringFormat("File %s from %s\ncould not be loaded.", szInfo, (isBars ? def_STR_FilesBar : def_STR_FilesTicks), "Market Replay", MB_OK)); FileClose(file); return false; } break; } FileClose(file); return (!_StopFlag); }
However, you shouldn’t get too excited. The only change here is the structure that was there before. The method of action remains the same. We still cannot use files containing traded ticks as 1-minute preview bars. We need to set up the above code.
If we look closely, we will see that we have acquired the ability to do what was previously impossible. Think about it: the data is sent to a procedure that centralizes all the pre-processing of the data that was read from the configuration file. The data used in the above code is already "clean". Does this mean we can include comments in the config file? The answer is YES. Now we can do this. We just need to determine the format of the comment: whether it has one or more lines. Let's start with a one-line comment. The procedure is simple and clear:
inline eTranscriptionDefine GetDefinition(const string &In, string &Out) { string szInfo; szInfo = In; Out = ""; StringToUpper(szInfo); StringTrimLeft(szInfo); StringTrimRight(szInfo); if (StringSubstr(szInfo, 0, 1) == "#") return Transcription_INFO; if (StringSubstr(szInfo, 0, 1) != "[") { Out = szInfo; return Transcription_INFO; } for (int c0 = 0; c0 < StringLen(szInfo); c0++) if (StringGetCharacter(szInfo, c0) > ' ') StringAdd(Out, StringSubstr(szInfo, c0, 1)); return Transcription_DEFINE; }
If the specified character is at the beginning of the line, it will be treated as a comment and its entire contents will be ignored. So, now we can insert comments into the configuration file. Interesting, isn't it? By simply adding a code line we now support comments. However, let's get back to the main problem. Our code does not yet consider files of traded traded as 1-minute bars. To do this we need to make some changes. Before making such changes, we need to make sure that the system continues to work as before, but with some new functionality. So we get the following code:
bool SetSymbolReplay(const string szFileConfig) { #define macroERROR(MSG) { FileClose(file); if (MSG != "") MessageBox(MSG, "Market Replay", MB_OK); return false; } int file, iLine; string szInfo; char iStage; if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { MessageBox("Failed to open\nconfiguration file.", "Market Replay", MB_OK); return false; } Print("Loading data for replay. Please wait...."); ArrayResize(m_Ticks.Rate, def_BarsDiary); m_Ticks.nRate = -1; m_Ticks.Rate[0].time = 0; iStage = 0; iLine = 1; while ((!FileIsEnding(file)) && (!_StopFlag)) { switch (GetDefinition(FileReadString(file), szInfo)) { case Transcription_DEFINE: if (szInfo == def_STR_FilesBar) iStage = 1; else if (szInfo == def_STR_FilesTicks) iStage = 2; else if (szInfo == def_STR_TicksToBars) iStage = 3; else macroERROR(StringFormat("%s is not recognized in system\nin line %d.", szInfo, iLine)); break; case Transcription_INFO: if (szInfo != "") switch (iStage) { case 0: macroERROR(StringFormat("Couldn't recognize command in line %d\nof the configuration file.", iLine)); break; case 1: if (!LoadPrevBars(szInfo)) macroERROR(StringFormat("This line is declared in line %d", iLine)); break; case 2: if (!LoadTicksReplay(szInfo)) macroERROR(StringFormat("This line is declared in line %d", iLine)); break; case 3: break; } break; }; iLine++; } FileClose(file); return (!_StopFlag); #undef macroERROR }
When we call this procedure, the first action is to initialize some parameters for our use. We are counting lines - this is necessary for the system to report exactly where the error occurred. For this reason, we have a macro that produces a general error message used in various situations. Now our system will work in steps. Therefore, we have to explicitly define what will be processed in the following lines. Skipping this step will be considered an error. In this case, the first error will always be equal to zero, since when initializing the procedure we indicated that we are at the zero stage of the analysis.
Although this structuring seems complex, it allows us to quickly and efficiently expand on any desired aspect. Additions rarely affect previous code. This approach allows us to use a configuration file with an internal format like this:
#First set initial bars, I think 3 is enough .... [Bars] WIN$N_M1_202108020900_202108021754 WIN$N_M1_202108030900_202108031754 WIN$N_M1_202108040900_202108041754 #I have a tick file but I will use it for pre-bars ... thus we have 4 files with bars [ Ticks -> Bars] WINQ21_202108050900_202108051759 #Now use the file of traded ticks to run replay ... [Ticks] WINQ21_202108060900_202108061759 #End of the configuration file...
Now you can clarify what is happening and use a more efficient format. But we have not yet implemented what we want. I'm just showing how things can be made more interesting with small changes to the code. And these changes are not as difficult as you might think.
Let's continue. We are going to make the necessary changes to use the traded tick files as preview bars. And before you start coming up with some complex code, I will let you know that the necessary code is already ready in the replay/simulation service. It's just hidden among the complexity. Now we need to extract this code and post it. This work must be carried out very carefully, since any mistake can jeopardize the entire existing system. To understand this, let's look at the following code mentioned in the previous article:
bool LoadTicksReplay(const string szFileNameCSV) { int file, old; string szInfo = ""; MqlTick tick; MqlRates rate; if ((file = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE) { ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray); ArrayResize(m_Ticks.Rate, def_BarsDiary, def_BarsDiary); old = m_Ticks.nTicks; for (int c0 = 0; c0 < 7; c0++) szInfo += FileReadString(file); if (szInfo != def_Header_Ticks) { Print("File ", szFileNameCSV, ".csv is not a file with traded ticks."); return false; } Print("Loading ticks for replay. Wait..."); while ((!FileIsEnding(file)) && (m_Ticks.nTicks < (INT_MAX - 2)) && (!_StopFlag)) { ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray); szInfo = FileReadString(file) + " " + FileReadString(file); tick.time = macroRemoveSec(StringToTime(StringSubstr(szInfo, 0, 19))); tick.time_msc = (int)StringToInteger(StringSubstr(szInfo, 20, 3)); tick.bid = StringToDouble(FileReadString(file)); tick.ask = StringToDouble(FileReadString(file)); tick.last = StringToDouble(FileReadString(file)); tick.volume_real = StringToDouble(FileReadString(file)); tick.flags = (uchar)StringToInteger(FileReadString(file)); if ((m_Ticks.Info[old].last == tick.last) && (m_Ticks.Info[old].time == tick.time) && (m_Ticks.Info[old].time_msc == tick.time_msc)) m_Ticks.Info[old].volume_real += tick.volume_real; else { m_Ticks.Info[m_Ticks.nTicks] = tick; if (tick.volume_real > 0.0) { m_Ticks.nRate += (BuiderBar1Min(rate, tick) ? 1 : 0); rate.spread = m_Ticks.nTicks; m_Ticks.Rate[m_Ticks.nRate] = rate; m_Ticks.nTicks++; } old = (m_Ticks.nTicks > 0 ? m_Ticks.nTicks - 1 : old); } } if ((!FileIsEnding(file)) && (!_StopFlag)) { Print("Too much data in the tick file.\nCannot continue..."); return false; } }else { Print("Tick file ", szFileNameCSV,".csv not found..."); return false; } return (!_StopFlag); };
Although this code is intended to read and save traded ticks for later use as replay/simulation, there is an important point to make. At some point we are going to create something equivalent to a 1 minute bar using only traded tick data.
Now let's think: Isn't this exactly what we want to do? We want to read a file of traded ticks and create a 1 minute bar and then save it in an asset used for replay/simulation. However, we save it not as a traded tick but as data that will be interpreted as the preview bar. Thus, if, instead of simply storing these ticks, we make small changes to the mentioned code, then we can make the bar created while reading the traded ticks appear to be inserted into the asset for analysis in replay/simulation as the preview bar.
It is necessary to take into account one important detail in this task. We'll cover this when discussing the implementation, because you'll only understand it once you see the code actually built and working. Let's first look at the code responsible for converting traded ticks to bars, which is shown just below:
bool SetSymbolReplay(const string szFileConfig) { #define macroERROR(MSG) { FileClose(file); MessageBox((MSG != "" ? MSG : StringFormat("Error occurred in line %d", iLine)), "Market Replay", MB_OK); return false; } int file, iLine; string szInfo; char iStage; if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE) { MessageBox("Failed to open\nconfiguration file.", "Market Replay", MB_OK); return false; } Print("Loading data for replay. Please wait...."); ArrayResize(m_Ticks.Rate, def_BarsDiary); m_Ticks.nRate = -1; m_Ticks.Rate[0].time = 0; iStage = 0; iLine = 1; while ((!FileIsEnding(file)) && (!_StopFlag)) { switch (GetDefinition(FileReadString(file), szInfo)) { case Transcription_DEFINE: if (szInfo == def_STR_FilesBar) iStage = 1; else if (szInfo == def_STR_FilesTicks) iStage = 2; else if (szInfo == def_STR_TicksToBars) iStage = 3; else macroERROR(StringFormat("%s is not recognized in system\nin line %d.", szInfo, iLine)); break; case Transcription_INFO: if (szInfo != "") switch (iStage) { case 0: macroERROR(StringFormat("Couldn't recognize command in line %d\nof the configuration file.", iLine)); break; case 1: if (!LoadPrevBars(szInfo)) macroERROR(""); break; case 2: if (!LoadTicksReplay(szInfo)) macroERROR(""); break; case 3: if (!LoadTicksReplay(szInfo, false)) macroERROR(""); break; } break; }; iLine++; } FileClose(file); return (!_StopFlag); #undef macroERROR }
Although we have made a small correction to the error reporting system in order to standardize reporting, that is not our goal here. We are really interested in the call that will generate 1-minute bars from traded ticks. Note that the call is identical to the one we used earlier, just with an additional parameter. This simple extra parameter detail is key and saves us from having to rewrite the entire function, as seen in the following code.
All the necessary logic is already present in the original replay/simulation service function to process the data from the traded tick file and convert it into 1-minute bars. What we really need to do is adapt these bars as preliminary ones without compromising overall performance. Because without the necessary modifications, errors may occur when using the replay/simulation service. So, let's look at the changes made to the code for reading traded ticks. Thus, the system will be able to use these ticks as bars.
bool LoadTicksReplay(const string szFileNameCSV, const bool ToReplay = true) { int file, old, MemNRates, MemNTicks; string szInfo = ""; MqlTick tick; MqlRates rate, RatesLocal[]; MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate); MemNTicks = m_Ticks.nTicks; if ((file = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE) { ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray); ArrayResize(m_Ticks.Rate, def_BarsDiary, def_BarsDiary); old = m_Ticks.nTicks; for (int c0 = 0; c0 < 7; c0++) szInfo += FileReadString(file); if (szInfo != def_Header_Ticks) { Print("File", szFileNameCSV, ".csv is not a file with traded ticks."); return false; } Print("Loading ticks for replay. Wait..."); while ((!FileIsEnding(file)) && (m_Ticks.nTicks < (INT_MAX - 2)) && (!_StopFlag)) { ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray); szInfo = FileReadString(file) + " " + FileReadString(file); tick.time = macroRemoveSec(StringToTime(StringSubstr(szInfo, 0, 19))); tick.time_msc = (int)StringToInteger(StringSubstr(szInfo, 20, 3)); tick.bid = StringToDouble(FileReadString(file)); tick.ask = StringToDouble(FileReadString(file)); tick.last = StringToDouble(FileReadString(file)); tick.volume_real = StringToDouble(FileReadString(file)); tick.flags = (uchar)StringToInteger(FileReadString(file)); if ((m_Ticks.Info[old].last == tick.last) && (m_Ticks.Info[old].time == tick.time) && (m_Ticks.Info[old].time_msc == tick.time_msc)) m_Ticks.Info[old].volume_real += tick.volume_real; else { m_Ticks.Info[m_Ticks.nTicks] = tick; if (tick.volume_real > 0.0) { m_Ticks.nRate += (BuiderBar1Min(rate, tick) ? 1 : 0); rate.spread = (ToReplay ? m_Ticks.nTicks : 0); m_Ticks.Rate[m_Ticks.nRate] = rate; m_Ticks.nTicks++; } old = (m_Ticks.nTicks > 0 ? m_Ticks.nTicks - 1 : old); } } if ((!FileIsEnding(file)) && (!_StopFlag)) { Print("Too much data in the tick file.\nCannot continue..."); FileClose(file); return false; } FileClose(file); }else { Print("Tick file ", szFileNameCSV,".csv not found..."); return false; } if ((!ToReplay) && (!_StopFlag)) { ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates)); ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0); CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates)); m_dtPrevLoading = m_Ticks.Rate[m_Ticks.nRate].time; m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates); m_Ticks.nTicks = MemNTicks; ArrayFree(RatesLocal); } return (!_StopFlag); };
First, we need to add some additional variables. These local variables will temporarily store important information so that you can later restore the system to its original state if necessary. Note that all code will remain identical to the original. We could add 1-minute bars as they appear, but I took a different approach. Naturally, I had to make changes in a specific place, exactly where we evaluate whether the data will be considered bars or not. If the data is used as bars, we must ensure consistent and logical presentation.
Also, we make sure that the reading is performed without interfering with the original procedure. Traded ticks are read and converted into 1-minute bars, there are no changes there. However, once the file has been fully read and everything is going as expected, we enter a new phase. And this is where the real transformation happens. If the tick file was assigned to be used as the preview bar system, and the user did not request the termination of the replay/simulation service while reading, then we have found a true state. We then take specific measures to ensure that the data is used correctly and return the system to its original state. Thus, we avoid problems and unusual situations at the play phase.
The first step here is to allocate memory for temporary storage of 1-minute bars. Once we do this, we will move the bars built while reading the file into this temporary space. This action is critical to the next step, which is to insert bars into the replay/simulation asset, ensuring its success. Without this previous action, it would be difficult to correctly position the 1-minute bars in the asset so that they would be interpreted as preview bars. Note that if we had chosen the direct method of adding bars at creation time, all this logic wouldn't be needed. However, with the chosen method, in which we first read the file and then save the bars, we need these manipulations to ensure correct operation.
By distributing and transferring data in this way, the process is simplified because there is no need to create a loop for transfer. After the translation is completed, we will correct the value of the limit position of the bars and restore the values saved at the beginning of the procedure. This way, the system will end up working as if nothing has changed. To the system, it will appear as if the reading of the bars was done directly from the 1-minute bar file, and not from the tick chart. The bars will be displayed as expected and we will finally free up some temporary memory space.
Thus, with minimal effort, we overcome a serious problem. However, the proposed solution is not the only possible one, although it seems to require the least amount of source code changes.
Removing all replay graphics
There is one interesting detail in the system at the time when it closes. Usually this happens when we close a chart that has a control indicator on it. Actually it is not quite a problem but a minor inconvenience. Many people prefer to open several charts of the same asset at the same time. This is normal and understandable, and can be useful in some situations. However, consider this: when the replay/simulation service tries to remove any traces of a used asset, it simply fails. The reason is simple: another chart with a replay asset is open.
Therefore, the system cannot remove these traces. There is one asset left in the market watch window that makes no sense. Although it can be removed manually, this is not what we need. The replay/simulation service should completely automatically remove any traces. To fix this, we need to make some changes to the code. To really understand what will be added, let's look at the source code below:
void CloseReplay(void) { ArrayFree(m_Ticks.Info); ArrayFree(m_Ticks.Rate); ChartClose(m_IdReplay); SymbolSelect(def_SymbolReplay, false); CustomSymbolDelete(def_SymbolReplay); GlobalVariableDel(def_GlobalVariableReplay); GlobalVariableDel(def_GlobalVariableIdGraphics); }
Pay attention to the behavior of this code: when calling it, only the chart that was opened by the service will be closed. If there are no other open charts that reference the replay asset, you can remove it from the market watch window. However, as already mentioned, the user could open other charts using the market replay asset. In this situation, when we try to remove the asset from the market watch window, we fail. To solve this problem, we need to change the way the service terminates. We need to adjust the corresponding lines for a more complex, but also more effective solution. The relevant code is shown below:
void CloseReplay(void) { ArrayFree(m_Ticks.Info); ArrayFree(m_Ticks.Rate); m_IdReplay = ChartFirst(); do { if (ChartSymbol(m_IdReplay) == def_SymbolReplay) ChartClose(m_IdReplay); }while ((m_IdReplay = ChartNext(m_IdReplay)) > 0); for (int c0 = 0; (c0 < 2) && (!SymbolSelect(def_SymbolReplay, false)); c0++); CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX); CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX); CustomSymbolDelete(def_SymbolReplay); GlobalVariableDel(def_GlobalVariableReplay); GlobalVariableDel(def_GlobalVariableIdGraphics); }
This code may seem strange but its function is quite simple. First we capture the ID of the first chart. It should be noted that it will not necessarily open first. Next we start a loop. In this loop, we perform a check to determine the asset the chart is referencing. If it is the replay asset, we close the graph. To determine whether to complete the loop or not, we will ask the platform to tell us the ID of the next chart in the list. If there are no other chart in the list, a value less than zero is returned and the loop is end. Otherwise, the loop will be executed again. This ensures that all windows whose asset is the one we are using as replay will be closed, regardless of their number. We then make two attempts to remove the replay asset from the market watch window. The reason why there are two attempts is because when we only have the replay/simulation service window open, then one attempt is enough to remove the asset. But if the user has other windows open, a second attempt may be required.
Usually one attempt is enough. If we can't remove the asset from the Market Watch window, we won't be able to remove the custom symbol. But regardless, we will remove any content present in the user resource as it is of no use outside of the replay/simulation service. Nothing should be left there. Even if we can't completely remove the custom asset, it won't be a big problem since there won't be anything inside it. However, the goal of this procedure is to completely remove it from the platform.
Conclusion
In the video below you can see the result of the work presented in this article. While some things may not be visible yet, watching the videos will give you a clear understanding of the replay/simulation system progress in all of these articles. Just watch the video and compare the changes from the very beginning to now.
In the next article we will continue to develop the system, since there are still some truly necessary functions to implement.
Translated from Portuguese by MetaQuotes Ltd.
Original article: https://www.mql5.com/pt/articles/10932
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use