Русский 中文 Español Deutsch 日本語 Português
preview
Developing a Replay System (Part 35): Making Adjustments (I)

Developing a Replay System (Part 35): Making Adjustments (I)

MetaTrader 5Examples | 25 April 2024, 10:54
1 058 0
Daniel Jose
Daniel Jose

Introduction

In the previous article Developing a Replay System (Part 34): Order System (III), I mentioned that the system has several rather strange and even mysterious bugs. These bugs or failures were caused by some interaction within the system. Despite attempts to find out the cause of such failures in order to eliminate them, all these attempts were unsuccessful. Some of these cases make no sense, for example, when we use pointers or recursion in C/C++, the program crashes. One of the first steps is to verify these mechanisms. However, in MQL5 this does not occur the same way as in C/C++. After making some small edits, I managed to solve one of the flaws. Although the solution does not seem elegant, it made one of the failures completely disappear.

However, we still need to make slightly more radical changes to the code in order to completely eliminate the errors that affect the system operation. They have probably been here for a long time, because the system didn't have certain types of interaction earlier. Once these interactions began to occur, these errors became noticeable.

The existing failures do not have a negative impact on the operation of the system, but they do not allow a really correct operation. All this makes the experience with the program quite unpleasant and even unacceptable. The first of these flaws is quite easy to fix, so we will start with it.


Solving the Service Busy indication

The first of these flaws is the easiest to correct. It is responsible for the following case: when we press CTRL or SHIFT while placing orders, we get an indication that the service is busy. This means that while running, even though the system is functioning normally, there is an indication that the service is performing some other task. This task is time shift. It means that bars are being created before you can perform some kind of analysis on the replay /simulator chart. Although this error is not extremely harmful, it makes the experience of using replay/simulator quite unpleasant because it is more confusing than informing.

Some people think that it is better to remove that the service is busy. Since it's been a while since we've used the display of bar creation when scrolling through time. But that doesn't solve the problem. This will only push it into the background and sweep it under the rug, but the solution is actually quite simple and effective. Furthermore, we can preserve some things so that in the future we can come back to the visualization system where we can see the creation of bars. As it was possible before we disabled the replay/simulation service. So, to solve the problem, we need to make some changes. Let's start with the following code:

int OnInit()
{
#define macro_INIT_FAILED { ChartIndicatorDelete(m_id, 0, def_ShortName); return INIT_FAILED; }
        u_Interprocess Info;
        ulong ul = 1;

        m_id = ChartID();
        ul <<= def_BitShift;
        IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName);
        if ((_Symbol != def_SymbolReplay) || (!GlobalVariableCheck(def_GlobalVariableIdGraphics))) macro_INIT_FAILED;
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableIdGraphics);
        if (Info.u_Value.IdGraphic != m_id) macro_INIT_FAILED;
        if ((Info.u_Value.IdGraphic >> def_BitShift) == 1) macro_INIT_FAILED;
        IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName + "Device");
        Info.u_Value.IdGraphic |= ul;
        GlobalVariableSet(def_GlobalVariableIdGraphics, Info.u_Value.df_Value); 
        if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.u_Value.df_Value = 0;
        EventChartCustom(m_id, C_Controls::ev_WaitOff, 0, Info.u_Value.df_Value, "");
        EventChartCustom(m_id, C_Controls::ev_WaitOff, 1, Info.u_Value.df_Value, "");
        Control.Init(Info.s_Infos.isPlay);
        
        return INIT_SUCCEEDED;
        
#undef macro_INIT_FAILED
}

The first modification should be made to the indicator file. The exact place where the modification is made is shown above. The crossed out part was deleted and replaced with a new code. Look closely because this is the only change that really needs to be made here. It concerns the value of the lparam parameter, which needs to be passed to the EventChartCustom function. Oddly enough, such a simple change is already bearing fruit.

In the same file, but in a different function, we need to do something very similar:

int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
{
  static bool bWait = false;
  u_Interprocess Info;
        
  Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
  if (!bWait)
  {
     if (Info.s_Infos.isWait)
     {
        EventChartCustom(m_id, C_Controls::ev_WaitOn, 0, 0, "");
        EventChartCustom(m_id, C_Controls::ev_WaitOn, 1, 0, "");
        bWait = true;
     }
  }else if (!Info.s_Infos.isWait)
  {
     EventChartCustom(m_id, C_Controls::ev_WaitOff, 0, Info.u_Value.df_Value, "");
     EventChartCustom(m_id, C_Controls::ev_WaitOff, 1, Info.u_Value.df_Value, "");
     bWait = false;
  }
        
  return rates_total;
}

This is similar to the OnInit code above. The crossed out parts have been replaced with new code. The only difference is again in the lparam parameter value. So, the fix again concerns the EventChartCustom function.

Why does this happen, and does it really work? In fact, making these changes here will not solve the problem. To really solve the problem, we need to go into the C_Control class and add a small check to the message handling function. Check out the new code below:

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   u_Interprocess Info;
   static int six = -1, sps;
   int x, y, px1, px2;
                                
   switch (id)
   {
      case (CHARTEVENT_CUSTOM + C_Controls::ev_WaitOn):
         if (lparam == 0) break;
         m_bWait = true;
         CreateBtnPlayPause(true);
         break;
      case (CHARTEVENT_CUSTOM + C_Controls::ev_WaitOff):
         if (lparam == 0) break;
         m_bWait = false;
         Info.u_Value.df_Value = dparam;
         CreateBtnPlayPause(Info.s_Infos.isPlay);
         break;
//... The rest of the code ...
  }
}

I only show here the fragments that we really need for understanding the problem. Note that a small check has been added to both custom events. The fact that user interaction fires a custom event in the program forces these checks to only allow the custom event to fire. These events are triggered exactly in the control indicator code. For any other reason, the value that arrives at the handler will be different from what we expected. In this case, the value of the lparam parameter is taken into account. This kind of thing will appear again in the future, when I explain how to use quite an interesting thing in the MetaTrader 5 platform. When we come to this, I will explain in more detail the reasons for this error. However, at the moment the reason is still strange.

But wait, that doesn't make any sense! In fact, I also didn't understand why at the beginning. But somehow, when you press SHIFT or CTRL to use the order system we are developing, the platform generates an event that triggers two previous events. Specifically CHARTEVENT_CUSTOM + C_Controls::ev_WaitOn, which causes the 'busy service' image to appear on the chart. As soon as you release SHIFT or CTRL, the CHARTEVENT_CUSTOM + C_Controls::ev_WaitOff event will fire, and this puts everything back on track.

With this problem fixed, we can move on to the next one. The second, however, is considerably more difficult to solve. To avoid confusion, we need to consider it within a separate topic. So, let's move on to the next section to understand the real situation.


Making a deeper reuse of the C_Mouse class

The next flaw happens when we do some analysis. When it happens, the system may end up moving the position indicator without the user actually noticing this. The solution to this problem requires much deeper code editing. You might think that it would be enough to run a check to verify that the user is actually trying to move the slider object. This slider object is an integral part of the control indicator and serves to ensure that the user knows at what stage the replay or simulation is. But a simple check is not enough to solve this flaw. We must go beyond simple verification. In fact, we'll do something much more complex at the beginning, but it will solve the problem in a much more definitive way and also allow us to do other things later.

We will use the C_Mouse class, which is originally used in the EA, also in the control indicator. But doing this is not as easy as it seems. We will need to make significant changes in several places in the code. We did this so that the C_Mouse class could work in complete harmony both in the control indicator and in the EA. We need everything to work without any conflict. By doing this, we will also open a door that will allow us to do something else.

First, let's look at the C_Mouse class and its changes. To explain the changes, we need to look at what was actually added to the code for this class. The first thing concerns private global variables. The modification is shown below:

struct st_Mem
{
   bool     CrossHair,
            IsFull;
   datetime dt;
}m_Mem;

This variable, which has been added, will be used to perform the check within the class. Its priority is to define exactly how the class will be executed: this refers to the objects that the class should work with during its lifetime. Once the variable is declared, we need to start it in an appropriate way. For this we will use the class constructor. Now comes the first complication: how to know whether to use the system in the FULL mode or not? An important detail: when using the FULL mode, we actually use graphical objects. These objects will be used by the Expert Advisor. When we do not use the class in the EA, such objects should NOT be created. But how can we tell the class whether we are using the Expert Advisor or something else? There are several ways. We need the one that is done through the class constructor. This way avoids a lot of subsequent additional checks and risks associated with incorrect use of the class. So, we will change the constructor. It will look like below:

C_Mouse(C_Terminal *arg, color corH = clrNONE, color corP = clrNONE, color corN = clrNONE)
{
   Terminal = arg;
   if (CheckPointer(Terminal) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid);
   if (_LastError != ERR_SUCCESS) return;
   m_Mem.CrossHair = (bool)ChartGetInteger(def_InfoTerminal.ID, CHART_CROSSHAIR_TOOL);
   ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_MOUSE_MOVE, true);
   ChartSetInteger(def_InfoTerminal.ID, CHART_CROSSHAIR_TOOL, false);
   ZeroMemory(m_Info);
   m_Info.corLineH  = corH;
   m_Info.corTrendP = corP;
   m_Info.corTrendN = corN;
   m_Info.Study = eStudyNull;
   if (m_Mem.IsFull = (corP != clrNONE) && (corH != clrNONE) && (corN != clrNONE))
      def_AcessTerminal.CreateObjectGraphics(def_NameObjectLineH, OBJ_HLINE, m_Info.corLineH);
}

Now notice what's happening here. The fact that we define the color data with a default value here in the constructor declaration allows us to overload the constructor. This overload means that we don't need to create other constructor code to use the same C_Mouse class. This is necessary in order to distinguish between its use in a control indicator or in the Expert Advisor. If such an overload did not exist, we would have to create another constructor to tell the class whether it would be used in the FULL mode or NOT. At this point in the constructor code, we define the value of the variable. At the same time, we are using its value for the first time. If we are in the FULL mode, we will create a price line that will be used on the chart. Otherwise, this line will not be created.

The destructor code has also undergone minor changes. Here is the code:

~C_Mouse()
{
   if (CheckPointer(Terminal) == POINTER_INVALID) return;
   ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_OBJECT_DELETE, 0, false);
   ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_MOUSE_MOVE, false);
   ChartSetInteger(def_InfoTerminal.ID, CHART_CROSSHAIR_TOOL, m_Mem.CrossHair);
   ObjectsDeleteAll(def_InfoTerminal.ID, def_MousePrefixName);
}

If class construction fails because the pointer to the C_Terminal class was not initialized correctly, we will not be able to use that pointer in the destructor. To avoid using an invalid pointer, we perform this check in the class destructor. This was the easiest part of initial coding. Now let's have a quick look at the old class code to see what changes we need to make.

void CreateStudy(void)
{
   if (m_Mem.IsFull)
   {
      def_AcessTerminal.CreateObjectGraphics(def_NameObjectLineV, OBJ_VLINE, m_Info.corLineH);
      def_AcessTerminal.CreateObjectGraphics(def_NameObjectLineT, OBJ_TREND, m_Info.corLineH);
      ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectLineT, OBJPROP_WIDTH, 2);
      CreateObjectInfo(0, 0, def_NameObjectStudy);
   }
   m_Info.Study = eStudyCreate;
}
//+------------------------------------------------------------------+
void ExecuteStudy(const double memPrice)
{
   double v1 = GetInfoMouse().Position.Price - memPrice;
   int w, h;
                                
   if (!CheckClick(eClickLeft))
   {
      m_Info.Study = eStudyNull;
      ChartSetInteger(def_InfoTerminal.ID, CHART_MOUSE_SCROLL, true);
      if (m_Mem.IsFull) ObjectsDeleteAll(def_InfoTerminal.ID, def_MousePrefixName + "T");
   }else if (m_Mem.IsFull)
   {
      string sz1 = StringFormat(" %." + (string)def_InfoTerminal.nDigits + "f [ %d ] %02.02f%% ",
      MathAbs(v1), Bars(def_InfoTerminal.szSymbol, PERIOD_CURRENT, m_Mem.dt, GetInfoMouse().Position.dt) - 1, MathAbs((v1/ memPrice) * 100.0)));
      GetDimensionText(sz1, w, h);
      ObjectSetString(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_TEXT, sz1);                                                                                                                           
      ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corTrendN : m_Info.corTrendP));
      ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_XSIZE, w);
      ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_YSIZE, h);
      ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_XDISTANCE, GetInfoMouse().Position.X - w);
      ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y - (v1 < 0 ? 1 : h));                            
      ObjectMove(def_InfoTerminal.ID, def_NameObjectLineT, 1, GetInfoMouse().Position.dt, GetInfoMouse().Position.Price);
      ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectLineT, OBJPROP_COLOR, (memPrice > GetInfoMouse().Position.Price ? m_Info.corTrendN : m_Info.corTrendP));
   }
   m_Info.Data.ButtonStatus = eKeyNull;
}

We have modified these two methods to make them very simple and straightforward. They add a small check for creating and working with objects. Such manipulations will take place only if we use the class in the FULL mode. But we also have another method in the C_Mouse class. This is very important because through this method we are going to interact with the class. Its code can be found below:

virtual void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   int w = 0;
   static double memPrice = 0;
                                
   switch (id)
   {
      case (CHARTEVENT_CUSTOM + ev_HideMouse):
         if (m_Mem.IsFull) ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectLineH, OBJPROP_COLOR, clrNONE);
         break;
      case (CHARTEVENT_CUSTOM + ev_ShowMouse):
         if (m_Mem.IsFull) ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectLineH, OBJPROP_COLOR, m_Info.corLineH);
         break;
      case CHARTEVENT_MOUSE_MOVE:
         ChartXYToTimePrice(def_InfoTerminal.ID, m_Info.Data.Position.X = (int)lparam, m_Info.Data.Position.Y = (int)dparam, w, m_Info.Data.Position.dt, m_Info.Data.Position.Price);
         if (m_Mem.IsFull) ObjectMove(def_InfoTerminal.ID, def_NameObjectLineH, 0, 0, m_Info.Data.Position.Price = def_AcessTerminal.AdjustPrice(m_Info.Data.Position.Price));
         m_Info.Data.Position.dt = def_AcessTerminal.AdjustTime(m_Info.Data.Position.dt);
         ChartTimePriceToXY(def_InfoTerminal.ID, w, m_Info.Data.Position.dt, m_Info.Data.Position.Price, m_Info.Data.Position.X, m_Info.Data.Position.Y);
         if ((m_Info.Study != eStudyNull) && (m_Mem.IsFull)) ObjectMove(def_InfoTerminal.ID, def_NameObjectLineV, 0, m_Info.Data.Position.dt, 0);
         m_Info.Data.ButtonStatus = (uint) sparam;
         if (CheckClick(eClickMiddle))
            if ((!m_Mem.IsFull) || ((color)ObjectGetInteger(def_InfoTerminal.ID, def_NameObjectLineH, OBJPROP_COLOR) != clrNONE)) CreateStudy();
               if (CheckClick(eClickLeft) && (m_Info.Study == eStudyCreate))
               {
                  ChartSetInteger(def_InfoTerminal.ID, CHART_MOUSE_SCROLL, false);
                  if (m_Mem.IsFull) ObjectMove(def_InfoTerminal.ID, def_NameObjectLineT, 0, m_Mem.dt = GetInfoMouse().Position.dt, memPrice = GetInfoMouse().Position.Price);
                  m_Info.Study = eStudyExecute;
               }
         if (m_Info.Study == eStudyExecute) ExecuteStudy(memPrice);
         m_Info.Data.ExecStudy = m_Info.Study == eStudyExecute;
         break;
      case CHARTEVENT_OBJECT_DELETE:
         if ((m_Mem.IsFull) && (sparam == def_NameObjectLineH)) def_AcessTerminal.CreateObjectGraphics(def_NameObjectLineH, OBJ_HLINE, m_Info.corLineH);
         break;
   }
}

Like the previous ones, this method has also undergone modification. In fact, the only thing we did was add checks for whether objects on the chart need to be manipulated. You might think that these checks are not necessary because in some cases the objects will not be on the chart, and if they are, they will have to be manipulated in the same way. This is because the MetaTrader 5 platform will not create duplicate objects. But think about the following: when we do these manipulations with objects, we don't just do them once. The fact that the EA and the control indicator use the same class, not knowing that both are using it at the same time, forces the MetaTrader 5 platform to make double calls to object handlers. This is not really a problem, but we must remember that we are using the system for market replay or simulation. Since the beginning of this project, I have repeated several times that our main goal is to make the simulation or replay as close as possible to what happens in the real market. It's not about just creating the bars, it's about the time it takes to create them. Having MetaTrader 5 access objects twice, where this could be represented in a single call, wastes valuable time, no matter what processor or hardware you use. If we start losing milliseconds or even nanoseconds due to code duplication for such calls, then soon we will start losing seconds or delaying the creation of bars too much. So yes, we should make sure MetaTrader 5 doesn't have to do extra work to display objects on the chart.

We have completed the modification of the C_Mouse class code, We do not need to worry about the EA code. The directory structure has changed, but this only affects the code of include. So, I don't think it's necessary to dwell on this. It's a small relief that the EA code hasn't undergone any implementation changes. But it is temporary since the control indicator code which is used to control replay/simulation will undergo much deeper changes that deserve a detailed and calm explanation. So, let's move on to the next topic.


Modifying the control indicator

To make this modification to the control indicator, we'll start by working on the class it uses. I mean the C_Control class. Let's see the first code modifications:

#include "..\Auxiliar\C_Terminal.mqh"
#include "..\Auxiliar\C_Mouse.mqh"
//+------------------------------------------------------------------+
#define def_AcessTerminal (*Terminal)
#define def_InfoTerminal def_AcessTerminal.GetInfoTerminal()
//+------------------------------------------------------------------+
class C_Controls : protected C_Mouse
{
        protected:
                enum EventCustom {ev_WaitOn, ev_WaitOff};
        private :
//+------------------------------------------------------------------+
                string  m_szBtnPlay;
                bool    m_bWait;
                struct st_00
                {
                        string  szBtnLeft,
                                szBtnRight,
                                szBtnPin,
                                szBarSlider,
                                szBarSliderBlock;
                        int     posPinSlider,
                                posY,
                                Minimal;
                }m_Slider;
                C_Terminal *Terminal;

You can immediately see that the code now has two calls to include files, both the C_Terminal class and the C_Mouse class. But why not use the C_Study class, which inherits from the C_Mouse class? The reason is that we will not make the control indicator do or manage analysis. This is the job for the Expert Advisor, at least at this stage of implementation, and not for the control indicator. Therefore, we will use the C_Mouse class. Note that the C_Control class inherits from the C_Mouse class. Despite this inheritance, the control indicator will not benefit from it, at least not directly. For this reason, such an inheritance could even be made private, but I usually treat this type of inheritance as protected. The last thing to note about these declarations is the pointer that the C_Terminal class will use.

NOTE: In the source code of the C_Control class there was a private global variable that provided access to the chart window index. This index will no longer exist in this version. The reason is that we are going to use the C_Terminal class to do this work, which was previously done through the internal index of the C_Control class.

Because of this detail, all points where the index was referenced have been removed. Where it was really needed, the reference was replaced with a definition that allows us to access the index through the C_Terminal class. Let's now look at the class constructor code. It is shown below:

C_Controls(C_Terminal *arg)
          :C_Mouse(arg),
           m_bWait(false)
   {
      if (CheckPointer(Terminal = arg) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid);
      m_szBtnPlay             = NULL;
      m_Slider.szBarSlider    = NULL;
      m_Slider.szBtnPin       = NULL;
      m_Slider.szBtnLeft      = NULL;
      m_Slider.szBtnRight     = NULL;
   }

The constructor will now take one parameter, which is a pointer to the C_Terminal class. It is passed to the C_Mouse class. But note that now C_Mouse will NOT be used in the FULL mode. Then objects of the C_Mouse class will not be created. This is because it will only serve us as support for code reuse. Anyway, we check here whether this pointer is valid. This is important to avoid using something that points to an invalid or unknown memory location. We also have a class destructor, which we have modified a bit:

~C_Controls()
{
   if (CheckPointer(Terminal) == POINTER_INVALID) return;
   ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_OBJECT_DELETE, false);
   ObjectsDeleteAll(def_InfoTerminal.ID, def_PrefixObjectName);
}

This check prevents an attempt to use an invalid pointer because the constructor may have failed, but the destructor does not know about it. By doing this check, we ensure that the destructor also knows why the constructor has failed. Because it may be that the destructor is called simply to finish using the C_Controls class. Since we are now using the system, some elements are initialized outside of this class, so we can make an additional change to the source code.

void Init(const bool state)
{
   if (m_szBtnPlay != NULL) return;
   ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_MOUSE_MOVE, true);
   ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_OBJECT_DELETE, true);
   CreateBtnPlayPause(state);
   GlobalVariableTemp(def_GlobalVariableReplay);
   if (!state) CreteCtrlSlider();
   ChartRedraw();
}

The lines above have been removed because what they do has already been implemented at other points. This is because the C_Mouse class triggers mouse movement, and the C_Terminal class tells the MetaTrader 5 platform that we want to be notified that an object has been removed from the chart. For these reasons, these lines were removed, since if they remained in the code, we could see strange behavior in some situations. Remember that we should never duplicate code. This is considered an error because it makes the code difficult to execute correctly in some scenarios and very difficult to maintain over time.

The next code we need to change is the class message handler. It is shown below:

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   u_Interprocess Info;
   static int six = -1, sps;
   int x, y, px1, px2;
                                
   C_Mouse::DispatchMessage(id, lparam, dparam, sparam);
   switch (id)
   {
      case (CHARTEVENT_CUSTOM + C_Controls::ev_WaitOn):
         if (lparam == 0) break;
         m_bWait = true;
         CreateBtnPlayPause(true);
         break;
      case (CHARTEVENT_CUSTOM + C_Controls::ev_WaitOff):
         if (lparam == 0) break;
         m_bWait = false;
         Info.u_Value.df_Value = dparam;
         CreateBtnPlayPause(Info.s_Infos.isPlay);
         break;
      case CHARTEVENT_OBJECT_DELETE:
         if (StringSubstr(sparam, 0, StringLen(def_PrefixObjectName)) == def_PrefixObjectName)
         {
            if (StringSubstr(sparam, 0, StringLen(def_NameObjectsSlider)) == def_NameObjectsSlider)
            {
               RemoveCtrlSlider();
               CreteCtrlSlider();
            }else
            {
               Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
               CreateBtnPlayPause(Info.s_Infos.isPlay);
            }
            ChartRedraw();
         }
         break;
      case CHARTEVENT_OBJECT_CLICK:
         if (m_bWait) break;
         if (sparam == m_szBtnPlay)
         {
            Info.s_Infos.isPlay = (bool) ObjectGetInteger(def_InfoTerminal.ID, m_szBtnPlay, OBJPROP_STATE);
            if (!Info.s_Infos.isPlay) CreteCtrlSlider(); else
            {
               RemoveCtrlSlider();
               m_Slider.szBtnPin = NULL;
            }
            Info.s_Infos.iPosShift = (ushort) m_Slider.posPinSlider;
            GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
            ChartRedraw();
         }else   if (sparam == m_Slider.szBtnLeft) PositionPinSlider(m_Slider.posPinSlider - 1);
         else if (sparam == m_Slider.szBtnRight) PositionPinSlider(m_Slider.posPinSlider + 1);
         break;
      case CHARTEVENT_MOUSE_MOVE:
         if (GetInfoMouse().ExecStudy) return;
         if ((CheckClick(C_Mouse::eClickLeft)) && (m_Slider.szBtnPin != NULL))
         {
            x = GetInfoMouse().Position.X;
            y = GetInfoMouse().Position.Y;
            px1 = m_Slider.posPinSlider + def_PosXObjects + 86;
            px2 = m_Slider.posPinSlider + def_PosXObjects + 114;
            if ((y >= (m_Slider.posY - 14)) && (y <= (m_Slider.posY + 14)) && (x >= px1) && (x <= px2) && (six == -1))
            {
               six = x;
               sps = m_Slider.posPinSlider;
               ChartSetInteger(def_InfoTerminal.ID, CHART_MOUSE_SCROLL, false);
            }
            if (six > 0) PositionPinSlider(sps + x - six);
         }else if (six > 0)
         {
            six = -1;
            ChartSetInteger(def_InfoTerminal.ID, CHART_MOUSE_SCROLL, true);
         }
         break;
   }
}

This code has undergone some changes compared to what was before. What you see is the final version. This way it will be easier to explain what's going on here. For the control indicator, you need to know the mouse behavior. Using only the CHARTEVENT_MOUSE_MOVE event for this will not be enough. We need to make the C_Mouse class to do its job. Remember that although the EA, like the indicator, uses the C_Mouse class, they do not exchange information about what the mouse is doing.

I thought about implementing this type of exchange, which is in fact possible. But we have some problems in doing this.

  1. If we exchange mouse data between the EA and the control indicator using any feature of the MetaTrader 5 platform, we may have problems using the EA outside of the replay/simulation system. We may also have difficulty getting the control indicator to understand what the mouse is doing.
  2. Another possibility would be to use shared memory via a DLL, but this would create a dependency on the platform or replay/simulation service. Frankly, I'm not interested in creating such a dependency at this point. Thus, using shared memory so that MetaTrader 5 does not have to manage the C_Mouse class the way it does itself is no longer relevant.

These reasons may not seem very compelling, but I want to take full advantage of the MQL5 language and the MetaTrader 5 platform to build the replay/simulation system. This will demonstrate that we can do much more than many people think is possible in the platform or language. Therefore, we need the C_Mouse class to also update data related to the mouse. This is done by making a call from here. Since we are going to handle mouse events via CHARTEVENT_MOUSE_MOVE, we can proceed to this specific event.

The first thing we do is check if the user is doing analysis on the chart. In this case, everything related to mouse event handling in the C_Control class should be ignored. From now on, we won't be handling mouse events locally. Instead, we'll ask the C_Mouse class what happened, and then take appropriate decisions and actions to fulfill the user's desire. Then we check if the user has pressed the left mouse button and if the slider object is present on the chart (pause mode). If true, we check the position where the click occurred. If it was on the slider object, it will react accordingly as long as the left button is pressed. This way the user can drag the slider object from one side to the other as before.

So, all we had to do was add a way for the C_Control class to know whether the user was doing analysis or not. Similar elements could be done differently, but as stated at the beginning of the article, this gives us some advantages, even if they are not so obvious now.


Conclusion

In this article, we saw how you can use classes or methods that were originally developed for use in an Expert Advisor in another program, in this case an indicator. All the work done here will allow us to have a better experience when using the replay/simulation system. Thus, a user developing a technique or wishing to test a particular way of working can do so using the system developed in this sequence. There is another issue that is currently being solved. We will talk about it in the next article. Because in order to correct the situation, we will have to change some things and add others. And, like all the material presented in this article, it can already be very difficult for those who are just starting to learn programming. I'm not going to increase the difficulty level for no reason.

In the attachment, you will find the complete system code. Also, there are three sets of files at the end of the article so you can test the system in different scenarios. By regularly testing it, you can learn a little more about how it has been developed.


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

Attached files |
Files_-_BOLSA.zip (1358.24 KB)
Files_-_FOREX.zip (3743.96 KB)
Files_-_FUTUROS.zip (11397.51 KB)
Population optimization algorithms: Micro Artificial immune system (Micro-AIS) Population optimization algorithms: Micro Artificial immune system (Micro-AIS)
The article considers an optimization method based on the principles of the body's immune system - Micro Artificial Immune System (Micro-AIS) - a modification of AIS. Micro-AIS uses a simpler model of the immune system and simple immune information processing operations. The article also discusses the advantages and disadvantages of Micro-AIS compared to conventional AIS.
Building A Candlestick Trend Constraint Model (Part 1): For EAs And Technical Indicators Building A Candlestick Trend Constraint Model (Part 1): For EAs And Technical Indicators
This article is aimed at beginners and pro-MQL5 developers. It provides a piece of code to define and constrain signal-generating indicators to trends in higher timeframes. In this way, traders can enhance their strategies by incorporating a broader market perspective, leading to potentially more robust and reliable trading signals.
How to build and optimize a volatility-based trading system (Chaikin Volatility - CHV) How to build and optimize a volatility-based trading system (Chaikin Volatility - CHV)
In this article, we will provide another volatility-based indicator named Chaikin Volatility. We will understand how to build a custom indicator after identifying how it can be used and constructed. We will share some simple strategies that can be used and then test them to understand which one can be better.
Creating a market making algorithm in MQL5 Creating a market making algorithm in MQL5
How do market makers work? Let's consider this issue and create a primitive market-making algorithm.