English Русский Español 日本語 Português
preview
Entwicklung eines Replay-Systems (Teil 69): Das richtige Bestimmen der Zeit (II)

Entwicklung eines Replay-Systems (Teil 69): Das richtige Bestimmen der Zeit (II)

MetaTrader 5Beispiele | 26 Mai 2025, 12:50
57 0
Daniel Jose
Daniel Jose

Einführung

Im vorigen Artikel, „Entwicklung eines Wiedergabesystems (Teil 68): Das richtige Bestimmen der Zeit (I)“, habe ich den Teil des Codes erklärt, der sich auf den Mausindikator bezieht. Dieser Code ist jedoch nur von geringem Wert, wenn Sie nicht auch den Code für den Dienst der Wiedergabe/Simulation untersuchen. Wenn Sie den vorherigen Artikel noch nicht gelesen haben, sollten Sie dies auf jeden Fall tun, bevor Sie versuchen, diesen Artikel zu verstehen. Der Grund dafür ist, dass das eine das andere wirklich ergänzt.

Der Schwerpunkt liegt hier vorerst auf der Bereitstellung von Informationen über die verbleibende Zeit auf dem Balken, wenn der Vermögenswert eine geringe Liquidität aufweist. Dies kann darauf zurückzuführen sein, dass während dieser Zeiträume keine traditionellen OnCalculate-Ereignisse erzeugt werden. Infolgedessen erhält die Mausanzeige nicht die korrekten Werte, die den verstrichenen Sekunden entsprechen. Ausgehend von dem, was im vorigen Artikel behandelt wurde, können wir jedoch die erforderlichen Werte übergeben, damit der Indikator die verbleibenden Sekunden berechnen kann.

In diesem Stadium konzentrieren wir uns in erster Linie auf den Dienst der Wiedergabe/Simulation. Genauer gesagt, werden wir uns mit der Datei C_Replay.mqh beschäftigen. Beginnen wir also damit, zu überprüfen, was wir ändern oder dem Code hinzufügen müssen.


Anpassen der Datei C_Replay.mqh

Es gibt nicht viele Änderungen, die vorgenommen werden müssen. Sie geben jedoch den richtigen Kontext für den im vorherigen Artikel besprochenen Code, insbesondere den Abschnitt, der die Verwendung der Bibliotheksfunktion iSpread innerhalb des OnCalculate-Ereignisses beinhaltet. Sie haben sich vielleicht gefragt, warum ich die Funktion iSpread verwendet habe, zumal es einfacher wäre, den Spread-Wert direkt aus dem Array zu lesen, das an die Funktion OnCalculate übergeben wird.

Das ist in der Tat ein sehr interessanter Punkt. Aber um die Gründe dafür zu verstehen, müssen wir wissen, wie die Dinge unter der Haube tatsächlich funktionieren. Dazu müssen wir untersuchen und verstehen, wie der Code des Dienstes der Wiedergabe/Simulation funktioniert. Und natürlich müssen wir auch verstehen, wie MetaTrader 5 diese Informationen verarbeitet.

Beginnen wir mit dem einfachsten Teil: dem Verständnis des Codes in der Datei C_Replay.mqh. Diese Datei ist für die Generierung der auf dem Chart angezeigten Informationen verantwortlich. Der vollständige geänderte Code ist unten zu sehen:

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 void CreateBarInReplay(bool bViewTick)
070.          {
071.             bool    bNew;
072.             double dSpread;
073.             int    iRand = rand();
074.             static int st_Spread = 0;
075. 
076.             if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew))
077.             {
078.                m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay];
079.                if (m_MemoryData.ModePlot == PRICE_EXCHANGE)
080.                {                  
081.                   dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 );
082.                   if (m_Infos.tick[0].last > m_Infos.tick[0].ask)
083.                   {
084.                      m_Infos.tick[0].ask = m_Infos.tick[0].last;
085.                      m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread;
086.                   }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid)
087.                   {
088.                      m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread;
089.                      m_Infos.tick[0].bid = m_Infos.tick[0].last;
090.                   }
091.                }
092.                if (bViewTick)
093.                {
094.                   CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
095.                   if (bNew) EventChartCustom(m_Infos.IdReplay, evSetServerTime, (long)m_Infos.Rate[0].time, 0, "");
096.                }
097.                st_Spread = (int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time);
098.                m_Infos.Rate[0].spread = (int)macroGetSec(m_MemoryData.Info[m_Infos.CountReplay].time);
099.                CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate);
100.             }
101.             m_Infos.Rate[0].spread = (int)(def_MaskTimeService | st_Spread);
102.             CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate);
103.             m_Infos.CountReplay++;
104.          }
105. //+------------------------------------------------------------------+
106.       void AdjustViewDetails(void)
107.          {
108.             MqlRates rate[1];
109. 
110.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX);
111.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX);
112.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE);
113.             m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
114.             CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate);
115.             if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE))
116.                for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++);
117.             if (rate[0].close > 0)
118.             {
119.                if (GetInfoTicks().ModePlot == PRICE_EXCHANGE)
120.                   m_Infos.tick[0].last = rate[0].close;
121.                else
122.                {
123.                   m_Infos.tick[0].bid = rate[0].close;
124.                   m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick);
125.                }               
126.                m_Infos.tick[0].time = rate[0].time;
127.                m_Infos.tick[0].time_msc = rate[0].time * 1000;
128.             }else
129.                m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay];
130.             CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
131.          }
132. //+------------------------------------------------------------------+
133.       void AdjustPositionToReplay(void)
134.          {
135.             int nPos, nCount;
136.             
137.             if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return;
138.             nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider);
139.             for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread);
140.             if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1);
141.             while ((nPos > m_Infos.CountReplay) && def_CheckLoopService)
142.                CreateBarInReplay(false);
143.          }
144. //+------------------------------------------------------------------+
145.    public   :
146. //+------------------------------------------------------------------+
147.       C_Replay()
148.          :C_ConfigService()
149.          {
150.             Print("************** Market Replay Service **************");
151.             srand(GetTickCount());
152.             SymbolSelect(def_SymbolReplay, false);
153.             CustomSymbolDelete(def_SymbolReplay);
154.             CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay));
155.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0);
156.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0);
157.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0);
158.             CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation");
159.             CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8);
160.             SymbolSelect(def_SymbolReplay, true);
161.             m_Infos.CountReplay = 0;
162.             m_IndControl.Handle = INVALID_HANDLE;
163.             m_IndControl.Mode = C_Controls::ePause;
164.             m_IndControl.Position = 0;
165.             m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState;
166.          }
167. //+------------------------------------------------------------------+
168.       ~C_Replay()
169.          {
170.             SweepAndCloseChart();
171.             IndicatorRelease(m_IndControl.Handle);
172.             SymbolSelect(def_SymbolReplay, false);
173.             CustomSymbolDelete(def_SymbolReplay);
174.             Print("Finished replay service...");
175.          }
176. //+------------------------------------------------------------------+
177.       bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate)
178.          {
179.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0)
180.                return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket.");
181.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0)
182.                return MsgError("Asset configuration is not complete, need to declare the ticket value.");
183.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0)
184.                return MsgError("Asset configuration not complete, need to declare the minimum volume.");
185.             SweepAndCloseChart();
186.             m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1);
187.             if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl"))
188.                Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl");
189.             else
190.                Print("Apply template: ", szNameTemplate, ".tpl");
191. 
192.             return true;
193.          }
194. //+------------------------------------------------------------------+
195.       bool InitBaseControl(const ushort wait = 1000)
196.          {
197.             Print("Waiting for Mouse Indicator...");
198.             Sleep(wait);
199.             while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200);
200.             if (def_CheckLoopService)
201.             {
202.                AdjustViewDetails();
203.                Print("Waiting for 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.                UpdateIndicatorControl();
207.             }
208.             
209.             return def_CheckLoopService;
210.          }
211. //+------------------------------------------------------------------+
212.       bool LoopEventOnTime(void)
213.          {         
214.             int iPos;
215. 
216.             while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay))
217.             {
218.                UpdateIndicatorControl();
219.                Sleep(200);
220.             }
221.             m_MemoryData = GetInfoTicks();
222.             AdjustPositionToReplay();
223.             EventChartCustom(m_Infos.IdReplay, evSetServerTime, (long)macroRemoveSec(m_MemoryData.Info[m_Infos.CountReplay].time), 0, "");
224.             iPos = 0;
225.             while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService))
226.             {
227.                if (m_IndControl.Mode == C_Controls::ePause) return true;
228.                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);
229.                CreateBarInReplay(true);
230.                while ((iPos > 200) && (def_CheckLoopService))
231.                {
232.                   Sleep(195);
233.                   iPos -= 200;
234.                   m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks);
235.                   UpdateIndicatorControl();
236.                }
237.             }
238. 
239.             return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService));
240.          }
241. };
242. //+------------------------------------------------------------------+
243. #undef def_SymbolReplay
244. #undef def_CheckLoopService
245. #undef def_MaxSlider
246. //+------------------------------------------------------------------+

Der Quellcode der Datei C_Replay.mqh

Im obigen Code haben Sie vielleicht bemerkt, dass mehrere Zeilen durchgestrichen sind. Diese Zeilen sollten aus der Version des Codes, die vor diesem Artikel existierte, entfernt werden. Es sind nicht viele Zeilen zu streichen, aber die Auswirkungen ihrer Entfernung werden erheblich sein.

Zunächst einmal ist festzustellen, dass in Zeile 74 eine neue Variable eingeführt wurde. Der Zweck dieser Variablen ist einfach: Sie soll die Sekunden zählen, in denen die Liquidität abnimmt oder sehr niedrig wird. Obwohl diese Logik in diesem Moment nicht ausgeführt wird, ist es wichtig zu verstehen, was passiert, um zu begreifen, wie dies umgesetzt werden wird. 

Beachten Sie zunächst, dass in Zeile 223 ein nutzerdefiniertes Ereignis aus dem ursprünglichen Code entfernt wurde. Beachten Sie auch, dass in jeder Iteration der Schleife, die in Zeile 225 beginnt, ein Aufruf von CreateBarInReplay erfolgt. Dies geschieht in Zeile 229. Achten Sie nun auf folgendes Detail: Die Funktion CreateBarInReplay wird etwa alle 195 Millisekunden ausgeführt, was auf die Zeile 232 und die Zeit zurückzuführen ist, die für die Ausführung der in Zeile 225 beginnenden Schleife benötigt wird. Dies führt zu etwa fünf Aufrufen pro Sekunde, vorausgesetzt, es gibt keine Verzögerungen zwischen den Iterationen. Szenarien mit hoher Liquidität sollten Sie jetzt vergessen. Ich versuche zu veranschaulichen, wie der Dienst der Wiedergabe/Simulation bei sehr geringer Liquidität tatsächlich funktioniert. Halten Sie sich also diese Zahl vor Augen: Es gibt ungefähr fünf Aufrufe pro Sekunde der Funktion CreateBarInReplay.

Kehren wir nun zur Prozedur CreateBarInReplay zurück, um zu verstehen, was passiert, wenn die Liquidität ausreichend ist, d. h. wenn wir mindestens fünf Aufrufe pro Sekunde haben.

In diesem Szenario wird die Bedingung in Zeile 76 als wahr ausgewertet. Somit wird der Codeblock zwischen den Zeilen 77 und 100 ausgeführt. Beachten Sie jedoch, dass innerhalb dieses Bereichs einige Zeilen aus dem Code entfernt wurden, was durch die Durchstreichungen angezeigt wird. Dazu gehört die Zeile 95, die bei jedem neuen Ein-Minuten-Balken ein nutzerdefiniertes Ereignis auslöste. Dieses Detail wird entscheidend sein, um zu erklären, warum die Funktion iSpread in OnCalculate erscheint. Aber machen Sie sich darüber keine Sorgen. Konzentrieren wir uns darauf, die Grundlagen zu verstehen. Beachten Sie, dass in Zeile 97 ein neues Codestück hinzugefügt wurde, das den Wert der Variablen initialisiert.

Achten Sie jetzt genau darauf: Die Zeilen 98 und 99 wurden durchgestrichen. Die darin enthaltene Logik wurde jedoch nicht über Bord geworfen, sondern lediglich verlagert. Zuvor befand sich dieser Code innerhalb des Blocks, der ausgeführt wurde, wenn die Bedingung in Zeile 76 als wahr bewertet wurde. Jetzt wird sie bedingungslos ausgeführt, wie in den Zeilen 101 und 102 zu sehen ist. Achten Sie nun auf Folgendes: Zeile 101 ist zwar anders, erfüllt aber dieselbe Aufgabe wie Zeile 98. Der Hauptunterschied besteht nun in der Verwendung einer Bitmaske. Dadurch kann der Mauszeiger erkennen, dass der Spread aus dem Dienst der Wiedergabe/Simulation stammt. Alles, was wir hier tun, ist eine ODER-Verknüpfung, um die Maske korrekt zu konfigurieren. Dies führt jedoch zu einem potenziellen Problem: Wenn der Wert der Variablen st_Spread in den Bereich der Bitmaske hineinreicht, kann der Mauszeiger die eingehenden Werte nicht richtig interpretieren.

Wenn also irgendetwas nicht in Ordnung zu sein scheint, überprüfen Sie einfach, ob der Wert der Variablen st_Spread die für die Bitmaske reservierten Grenzen überschreitet. Unter normalen Bedingungen sollte dies nicht auftreten, da die Wiedergabe/Simulation für Intraday-Studien und -Analysen konzipiert ist. Nur wenn der Dienst der Wiedergabe/Simulation bis an sein absolutes Zeitlimit belastet wird, könnte ein solcher Zustand eintreten. Zum Vergleich: Diese Frist beträgt fast 12 Tage in Sekunden, was für unsere Zwecke mehr als ausreichend ist.

Lassen Sie uns weiter verstehen, wie das System funktioniert. Wenn Sie den Dienst der Wiedergabe/Simulation zusammen mit der kompilierten Version des Maus-Indikators aus dem vorigen Artikel kompilieren und ausführen, und wenn der Vermögenswert über eine ausreichende Liquidität verfügt (d. h. mindestens einen Tick pro Sekunde), erhalten Sie genaue Aktualisierungen bezüglich der verbleibenden Zeit, bis der aktuelle Balken geschlossen und der nächste geöffnet wird.

Das ist alles schön und gut, aber es erklärt immer noch nicht, warum das Spread-Array, das in einer der Funktionsversionen von OnCalculate verfügbar ist, nicht verwendet wurde und warum die iSpread-Funktion notwendig war, um den Spread-Wert zu erhalten, der vom Dienst gemeldet wird, wie in Zeile 101 zu sehen ist. Um das zu verstehen, müssen wir ein anderes Konzept untersuchen.


Verstehen, warum iSpread verwendet wird

Zum Zeitpunkt der Erstellung dieses Artikels ist die aktuellste Version von MetaTrader 5 unten aufgeführt:

Bild 01

Selbst in dieser Version - und es ist möglich, dass dieses Verhalten unverändert bleibt, wenn Sie, lieber Leser, dies lesen - behandelt MetaTrader 5 die Balken, zumindest für nutzerdefinierte Assets, immer noch auf eine recht merkwürdige Weise. Vielleicht sind nicht alle Informationen, die sich auf die Balken beziehen, davon betroffen, aber da wir Daten über die Spanne übertragen, ist es klar, dass sich dies etwas eigenartig verhält.

Um dies zu demonstrieren, nehmen wir ein paar kleine Änderungen am Code in der Header-Datei C_Replay.mqh und im Mauszeiger vor. Ich glaube, dass es dadurch viel einfacher wird, deutlich zu machen, was tatsächlich passiert, denn eine bloße Erklärung würde nicht ausreichen. In der Datei C_Replay.mqh ändern wir also den Code im folgenden Fragment, wie unten gezeigt:

068. //+------------------------------------------------------------------+
069. inline void CreateBarInReplay(bool bViewTick)
070.          {
071.             bool    bNew;
072.             double dSpread;
073.             int    iRand = rand();
074.             static int st_Spread = 0;
075. 
076.             if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew))
077.             {
078.                m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay];
079.                if (m_MemoryData.ModePlot == PRICE_EXCHANGE)
080.                {                  
081.                   dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 );
082.                   if (m_Infos.tick[0].last > m_Infos.tick[0].ask)
083.                   {
084.                      m_Infos.tick[0].ask = m_Infos.tick[0].last;
085.                      m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread;
086.                   }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid)
087.                   {
088.                      m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread;
089.                      m_Infos.tick[0].bid = m_Infos.tick[0].last;
090.                   }
091.                }
092.                if (bViewTick)
093.                   CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
094.                st_Spread = (int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time);
095.             }
096.             Print(TimeToString(st_Spread, TIME_SECONDS));
097.             m_Infos.Rate[0].spread = (int)(def_MaskTimeService | st_Spread);
098.             CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate);
099.             m_Infos.CountReplay++;
100.          }
101. //+------------------------------------------------------------------+

Der Code aus der Datei C_Replay.mqh

Beachten Sie, dass der Code in diesem Fragment bereits bereinigt wurde, sodass die Zeilennummerierung leicht abweichen kann. Der Code selbst ist jedoch identisch mit dem, der weiter oben in diesem Artikel gezeigt wurde. Der einzige Unterschied ist die Zeile 96, die hinzugefügt wurde, um im Terminal den Wert anzuzeigen, der gerade in das Spread-Feld des Balkens geschrieben wird. Wenn Sie diesen geänderten Code ausführen, erhalten Sie die unten dargestellte Ausgabe:

Animation 01

Beachten Sie, dass der gedruckte Wert genau mit dem Wert übereinstimmt, der im Tickchart als aktuelle Zeit angezeigt wird. Es ist sehr wichtig, dies zu verstehen. Wir haben nun die Bestätigung, dass der Wert, der in das Spread-Feld des Balkens eingefügt wird, tatsächlich der im Chart angezeigte Zeitwert ist. Kommen wir nun zu etwas anderem. Wir werden den Kontrollindikator geringfügig ändern (etwas sehr Subtiles), um zu analysieren, wie sich das System verhält. Diese Änderung wird am Code in der Header-Datei C_Study.mqh vorgenommen. Sie können die Änderung unten sehen:

109. //+------------------------------------------------------------------+
110.       void Update(const eStatusMarket arg)
111.          {
112.             int i0;
113.             datetime dt;
114.                      
115.             switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status))
116.             {
117.                case eCloseMarket :
118.                   m_Info.szInfo = "Closed Market";
119.                   break;
120.                case eInReplay    :
121.                case eInTrading   :
122.                   i0 = PeriodSeconds();
123.                   dt = (m_Info.Status == eInReplay ? (datetime) GL_TimeAdjust : TimeCurrent());
124.                   m_Info.Rate.time = (m_Info.Rate.time <= dt ? (datetime)(((ulong) dt / i0) * i0) + i0 : m_Info.Rate.time);
125.                   if (dt > 0) m_Info.szInfo = TimeToString((datetime)m_Info.Rate.time/* - dt*/, TIME_SECONDS);
126.                   break;
127.                case eAuction     :
128.                   m_Info.szInfo = "Auction";
129.                   break;
130.                default           :
131.                   m_Info.szInfo = "ERROR";
132.             }
133.             Draw();
134.          }
135. //+------------------------------------------------------------------+

Teil der Datei C_Study.mqh

Achten Sie hier genau darauf, denn die Veränderung ist recht subtil. In Zeile 125 wurde die Einstellung dt entfernt. Das bedeutet, dass die jetzt angezeigte Information genau dem Zeitpunkt entspricht, zu dem ein neuer Balken erwartet wird. Beachten Sie: Es wird nicht angegeben, wie viel Zeit bis zum nächsten Balken verbleibt, sondern wann der nächste Balken tatsächlich erwartet wird. Nach dieser Änderung kompilieren wir den Mausindikator neu, um die Ausgabe zu testen, die angezeigt wird. In der folgenden Animation können Sie sehen, was tatsächlich passiert:

Animation 2

Beachten Sie, dass der verwendete Chart-Zeitrahmen zwei Minuten beträgt. Die jetzt durchgeführte Berechnung zeigt den genauen Zeitpunkt an, an dem der nächste Balken erscheinen wird. Dies wird im Mauszeiger angezeigt. Sie können sehen, dass der Indikator sofort beginnt zu melden, wann der neue Balken erscheint, wenn die Chartzeit den angegebenen Punkt erreicht. Mit anderen Worten: Das System funktioniert wie vorgesehen. Diese Tests überprüfen jedoch noch nicht den Wert, den der Dienst der Wiedergabe/Simulation liefert. Bislang haben wir lediglich die Informationen bestätigt, die wir erwartet haben. Nun müssen wir den tatsächlichen Wert überprüfen, den der Dienst weitergibt. Es ist wichtig sicherzustellen, dass der Zeitrahmen des Charts nicht auf eine Minute eingestellt ist, da der Test sonst ungültig ist. Belassen wir es also bei zwei Minuten, was für eine Analyse des Geschehens ausreicht.

Damit der Test wie erwartet funktioniert, müssen wir eine kleine Änderung vornehmen. Achten Sie noch einmal genau auf den Code im folgenden Fragment:

109. //+------------------------------------------------------------------+
110.       void Update(const eStatusMarket arg)
111.          {
112.             int i0;
113.             datetime dt;
114.                      
115.             switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status))
116.             {
117.                case eCloseMarket :
118.                   m_Info.szInfo = "Closed Market";
119.                   break;
120.                case eInReplay    :
121.                case eInTrading   :
122.                   i0 = PeriodSeconds();
123.                   dt = (m_Info.Status == eInReplay ? (datetime) GL_TimeAdjust : TimeCurrent());
124.                   m_Info.Rate.time = (m_Info.Rate.time <= dt ? (datetime)(((ulong) dt / i0) * i0) + i0 : m_Info.Rate.time);
125.                   if (dt > 0) m_Info.szInfo = TimeToString((datetime)/*m_Info.Rate.time -*/ dt, TIME_SECONDS);
126.                   break;
127.                case eAuction     :
128.                   m_Info.szInfo = "Auction";
129.                   break;
130.                default           :
131.                   m_Info.szInfo = "ERROR";
132.             }
133.             Draw();
134.          }
135. //+------------------------------------------------------------------+

Der Teil der Datei C_Study.mqh

Nun wählen wir den vom Dienst bereitgestellten Wert aus und zeigen ihn im Mauszeiger an. Das Ergebnis ist unten zu sehen:

Animation 3

Wie Sie sehen können, entspricht es genau dem, was wir erwartet haben. An dieser Stelle werden wir keine weiteren Änderungen an der Header-Datei vornehmen. Stattdessen werden wir uns auf etwas anderes im Mauszeiger konzentrieren. Schauen wir uns an, was passiert, wenn wir den beim Aufruf von OnCalculate erhaltenen Spread-Wert verwenden. Dazu müssen wir den Code des Mausindikators ändern. Beachten Sie jedoch Folgendes: Der im Indikator angezeigte Wert entspricht dem Wert, der erfasst und der Variablen GL_TimeAdjust zugewiesen wird. Dies zu bedenken ist entscheidend. Ändern wir nun den Code des Indikators, um zu testen, ob die Verwendung des von OnCalculate erhaltenen Spread-Wertes tatsächlich geeignet ist. Der aktualisierte Code sieht wie folgt aus:

46. //+------------------------------------------------------------------+
47. int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[],
48.                 const double& high[], const double& low[], const double& close[], const long& tick_volume[], 
49.                 const long& volume[], const int& spread[])
50. //int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double& price[])
51. {
52.    GL_PriceClose = close[rates_total - 1];
53. //   GL_PriceClose = price[rates_total - 1];
54.    GL_TimeAdjust = (spread[rates_total - 1] & (~def_MaskTimeService);
55. //   if (_Symbol == def_SymbolReplay)
56. //      GL_TimeAdjust = iSpread(NULL, PERIOD_M1, 0) & (~def_MaskTimeService);
57.    m_posBuff = rates_total;
58.    (*Study).Update(m_Status);   
59.    
60.    return rates_total;
61. }
62. //+------------------------------------------------------------------+

Dateifragment des Mauszeigers

Beobachten Sie genau, was wir in dem obigen Fragment tun. Wir isolieren den Code so, dass die durchgestrichenen Zeilen die aktuelle Version darstellen, wie im vorherigen Artikel zu sehen. Diese Zeilen sollten vorübergehend entfernt werden. An ihrer Stelle haben wir neue Zeilen hinzugefügt, die die vom MetaTrader 5 über die Funktion OnCalculate bereitgestellten Daten verwenden sollen. Mit anderen Worten, wir verlassen uns nicht mehr auf den Aufruf der iSpread-Funktion. Stattdessen verwenden wir den vom MetaTrader 5 bereitgestellten Wert im Spread-Array. Um die Kompatibilität mit dem Dienst zu gewährleisten, müssen wir eine kleine Anpassung vornehmen, wie in Zeile 54 gezeigt. Sie werden feststellen, dass derselbe Vorgang ausgeführt wird, der zuvor mit iSpread durchgeführt wurde, nur dass jetzt der verwendete Wert direkt aus den an OnCalculate übergebenen Argumenten stammt. Dies kann für uns einen bedeutenden Unterschied machen, da dadurch kein zusätzlicher Funktionsaufruf mehr nötig ist, nur um einen Wert abzurufen, den MetaTrader 5 bereits bereitstellt.

Werfen wir nun einen Blick auf das Ergebnis der Ausführung dieses aktualisierten Codes. Dies wird in der folgenden Animation gezeigt:

Animation 4

Ups. Was ist hier gerade passiert? Warum ist der Wert in der Mausanzeige eingefroren? Die Antwort auf diese Frage ist nicht einfach. Im Gegensatz zu dem, was viele vermuten, kenne ich die Antwort eigentlich nicht. Wie kann das sein, dass ich das nicht weiß? Es stimmt, dass ich einen gewissen Verdacht hege. Aber anstatt zu spekulieren, zeige ich Ihnen lieber, dass etwas, womit Sie wahrscheinlich nicht gerechnet haben, passieren kann. Auf diese Weise können Sie sich selbst ein Bild machen und Ihre eigenen Schlüsse ziehen.

In jedem Fall gibt der Dienst die Werte wie bisher aus, genau wie in den vorherigen Animationen. Der Indikator erfasst nach wie vor den Wert des Spreads. Aber warum ist es eingefroren? Ich kann es nicht erklären. Ich weiß nur, dass der Wert im Spread-Array korrekt aktualisiert wird, wenn ein neuer Balken im Chart erscheint, und deshalb ist es wichtig, einen anderen Zeitrahmen als eine Minute zu verwenden, um dieses Problem aufzudecken.

Ich möchte Sie daran erinnern, dass das, was ich hier zeige, zu dem Zeitpunkt, an dem Sie diesen Artikel lesen, vielleicht noch nicht in Kraft ist. Es ist nämlich gut möglich, dass MetaTrader 5 ein Update erhalten hat, das dieses Problem behebt. Bis es soweit ist, umgehe ich das Problem mit der iSpread-Funktion. Sobald dieses kleine Problem behoben ist, werden wir iSpread nicht mehr verwenden und uns stattdessen auf den Wert verlassen, der von MetaTrader 5 direkt an OnCalculate übergeben wird. Hängen Sie also nicht zu sehr an einem bestimmten Teil des Codes - alles wird im Laufe der Entwicklung verbessert werden. Ich glaube, Sie verstehen jetzt, warum ich iSpread verwende und nicht den Spread-Wert, der als Argument an OnCalculate übergeben wird. Aber wir sind noch nicht fertig. Wir müssen noch einen Weg finden, wie der Dienst uns über die verbleibende Zeit auf einem Balken informieren kann, wenn eine geringe Liquidität verhindert, dass wir jede Sekunde Ticks - oder genauer gesagt, OnCalculate-Ereignisse - erhalten. Um fortzufahren, werden wir nun die in diesem Abschnitt vorgenommenen Änderungen rückgängig machen (um den Grund für die Verwendung von iSpread im Indikator zu veranschaulichen) und zur Arbeit am Dienst zurückkehren.


Behebung eines Fehlers im Dienst

Damit alles richtig funktioniert, wenn die Zeit zwischen den Ticks eine Sekunde überschreitet, müssen wir leider einen etwas anderen Ansatz wählen als ursprünglich zu Beginn dieses Artikels geplant. Das Problem liegt darin, dass ich beabsichtigt hatte, den Zähler innerhalb der Routine zur Erstellung des Balkens zu platzieren. Allerdings habe ich ein wichtiges Detail übersehen: ZEIT. Gehen Sie zum Anfang dieses Artikels zurück und sehen Sie sich den Quellcode für die Header-Datei C_Replay.mqh an. In Zeile 230 befindet sich eine Schleife, die den Dienst von Wiedergabe/Simulation veranlasst, so lange zu warten, bis ein neuer Tick erscheint. Und genau hier liegt das Problem.

Während der Entwicklung und der Tests habe ich mit Vermögenswerten gearbeitet, die eine hohe Liquidität aufwiesen, d. h. mit historischen Daten, bei denen die Zeit zwischen den Ticks im Allgemeinen weniger als eine Sekunde betrug. Als ich begann, Änderungen vorzunehmen, um die Möglichkeit längerer Tickintervalle zu unterstützen, trat ein Fehler auf. Nicht weil er plötzlich aufgetaucht ist. Sondern weil er die ganze Zeit existierte, verborgen durch die relativ kurzen Tick-Intervalle. Achten Sie nun genau auf die Schleife zwischen den Zeilen 230 und 236. Was ist daran falsch? Das Problem ist, dass nicht berücksichtigt wird, dass der Nutzer das System anhalten könnte. Wie ist das möglich? Wenn sich der Dienst in einer Schleife befindet und auf den nächsten Tick wartet, ist das doch in Ordnung, oder? Nicht ganz. Wenn die Wartezeit mehr als eine Sekunde beträgt, gibt es ein Problem.

Gehen wir davon aus, dass wir Forex-Daten wiedergeben. Zu Beginn einer täglichen Sitzung können die Tick-Intervalle recht groß sein. Wenn Sie auf „Play“ drücken und der Dienst feststellt, dass er 40 Sekunden bis zum nächsten Tick warten muss, reagiert der Dienst auch dann nicht, wenn Sie auf „Pause“ drücken, den Schieberegler an eine andere Stelle bewegen und dann erneut auf „Play“ drücken. Denn er bleibt in der Schleife von Zeile 230 bis 236 hängen und wartet die vollen 40 Sekunden ab. Das müssen wir also als erstes in Ordnung bringen. Aber anstatt dieses Problem isoliert zu beheben, sollten wir sowohl die Korrektur als auch die Lösung für die Anzeige der verbleibenden Balkenzeit in Zeiten geringer Liquidität auf einmal umsetzen. Die aktualisierte Version der gesamten Datei C_Replay.mqh ist unten abgebildet:

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.             static int st_Spread = 0;
086. 
087.             if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew))
088.             {
089.                m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay];
090.                if (m_MemoryData.ModePlot == PRICE_EXCHANGE)
091.                {                  
092.                   dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 );
093.                   if (m_Infos.tick[0].last > m_Infos.tick[0].ask)
094.                   {
095.                      m_Infos.tick[0].ask = m_Infos.tick[0].last;
096.                      m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread;
097.                   }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid)
098.                   {
099.                      m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread;
100.                      m_Infos.tick[0].bid = m_Infos.tick[0].last;
101.                   }
102.                }
103.                if (bViewTick)
104.                   CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
105.                RateUpdate(true);
106.                st_Spread = (int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time);
107.             }
108.             m_Infos.Rate[0].spread = (int)(def_MaskTimeService | st_Spread);
109.             CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate);
110.             m_Infos.CountReplay++;
111.          }
112. //+------------------------------------------------------------------+
113.       void AdjustViewDetails(void)
114.          {
115.             MqlRates rate[1];
116. 
117.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX);
118.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX);
119.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE);
120.             m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
121.             CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate);
122.             if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE))
123.                for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++);
124.             if (rate[0].close > 0)
125.             {
126.                if (GetInfoTicks().ModePlot == PRICE_EXCHANGE)
127.                   m_Infos.tick[0].last = rate[0].close;
128.                else
129.                {
130.                   m_Infos.tick[0].bid = rate[0].close;
131.                   m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick);
132.                }               
133.                m_Infos.tick[0].time = rate[0].time;
134.                m_Infos.tick[0].time_msc = rate[0].time * 1000;
135.             }else
136.                m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay];
137.             CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
138.          }
139. //+------------------------------------------------------------------+
140.       void AdjustPositionToReplay(void)
141.          {
142.             int nPos, nCount;
143.             
144.             if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return;
145.             nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider);
146.             for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread);
147.             if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1);
148.             while ((nPos > m_Infos.CountReplay) && def_CheckLoopService)
149.                CreateBarInReplay(false);
150.          }
151. //+------------------------------------------------------------------+
152.    public   :
153. //+------------------------------------------------------------------+
154.       C_Replay()
155.          :C_ConfigService()
156.          {
157.             Print("************** Market Replay Service **************");
158.             srand(GetTickCount());
159.             SymbolSelect(def_SymbolReplay, false);
160.             CustomSymbolDelete(def_SymbolReplay);
161.             CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay));
162.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0);
163.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0);
164.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0);
165.             CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation");
166.             CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8);
167.             SymbolSelect(def_SymbolReplay, true);
168.             m_Infos.CountReplay = 0;
169.             m_IndControl.Handle = INVALID_HANDLE;
170.             m_IndControl.Mode = C_Controls::ePause;
171.             m_IndControl.Position = 0;
172.             m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState;
173.          }
174. //+------------------------------------------------------------------+
175.       ~C_Replay()
176.          {
177.             SweepAndCloseChart();
178.             IndicatorRelease(m_IndControl.Handle);
179.             SymbolSelect(def_SymbolReplay, false);
180.             CustomSymbolDelete(def_SymbolReplay);
181.             Print("Finished replay service...");
182.          }
183. //+------------------------------------------------------------------+
184.       bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate)
185.          {
186.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0)
187.                return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket.");
188.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0)
189.                return MsgError("Asset configuration is not complete, need to declare the ticket value.");
190.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0)
191.                return MsgError("Asset configuration not complete, need to declare the minimum volume.");
192.             SweepAndCloseChart();
193.             m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1);
194.             if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl"))
195.                Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl");
196.             else
197.                Print("Apply template: ", szNameTemplate, ".tpl");
198. 
199.             return true;
200.          }
201. //+------------------------------------------------------------------+
202.       bool InitBaseControl(const ushort wait = 1000)
203.          {
204.             Print("Waiting for Mouse Indicator...");
205.             Sleep(wait);
206.             while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200);
207.             if (def_CheckLoopService)
208.             {
209.                AdjustViewDetails();
210.                Print("Waiting for Control Indicator...");
211.                if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false;
212.                ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle);
213.                UpdateIndicatorControl();
214.             }
215.             
216.             return def_CheckLoopService;
217.          }
218. //+------------------------------------------------------------------+
219.       bool LoopEventOnTime(void)
220.          {         
221.             int iPos, iCycles;
222. 
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.             while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService))
232.             {
233.                if (m_IndControl.Mode == C_Controls::ePause) return true;
234.                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);
235.                CreateBarInReplay(true);
236.                while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause))
237.                {
238.                   Sleep(195);
239.                   iPos -= 200;
240.                   m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks);
241.                   UpdateIndicatorControl();
242.                   iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1);
243.                }
244.             }
245. 
246.             return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService));
247.          }
248. };
249. //+------------------------------------------------------------------+
250. #undef def_SymbolReplay
251. #undef def_CheckLoopService
252. #undef def_MaxSlider
253. //+------------------------------------------------------------------+

Der Quellcode der Datei C_Replay.mqh

Lassen Sie uns nun erläutern, wie dieser aktualisierte Code funktioniert. Wir beginnen am Ende, wo sich der Dienst bisher bei geringer Liquidität seltsam verhalten hat. Beachten Sie, dass in Zeile 236 eine Korrektur vorgenommen wurde. Der Code bleibt nicht mehr stecken und wartet auf eine lange erkannte Verzögerung, die sonst dazu führen würde, dass das System nicht mehr auf den Nutzer reagiert. In diesem Fall mussten wir lediglich eine Prüfung hinzufügen, um festzustellen, ob der Nutzer das System angehalten hat. Ist dies der Fall, wird die Schleife verlassen, und wenn die Ausführung die Zeile 233 erreicht, wird die Funktion beendet und kehrt zum Hauptkontrollfluss zurück. Die Hauptlogik ruft dann die Funktion erneut auf und wartet erneut. Diesmal wird jedoch eine Schleife in Zeile 223 ausgeführt, die es dem Nutzer ermöglicht, den Kontrollindikator neu zu positionieren und zu einem anderen Zeitpunkt zu springen. Dies ermöglicht uns eine wesentlich reibungslosere Abwicklung, insbesondere wenn ein Vermögenswert eine geringe Liquidität aufweist oder in eine Auktionsphase eintritt. Sie verstehen vielleicht nicht ganz, was ich meine, wenn Sie sich diese Routine LoopEventOnTime ansehen. Aber die Dinge werden im Laufe der Erklärung klarer werden.

Sehen wir uns die Änderungen an, die vorgenommen wurden, um auch bei geringer Tickaktivität ein zeitlich begrenztes Feedback zu geben. In Zeile 221 wurde eine neue Variable hinzugefügt und in Zeile 230 initialisiert. Achten Sie nun auf die Zeile 242. Wir verwenden dieselbe Variable, um von 0 bis 4 zu zählen. Wenn der Wert 4 erreicht, rufen wir die Funktion RateUpdate auf. Aber was ist RateUpdate? Keine Sorge, dazu kommen wir noch. Beachten Sie zunächst, dass die Funktion mit dem Argument false aufgerufen wird und ihr Rückgabewert der Variablen zugewiesen wird. Dieses Detail ist wichtig. Erinnern Sie sich, dass ich weiter oben im Artikel erwähnt habe, dass wir ungefähr fünf Zyklen pro Sekunde haben? Deshalb haben wir diesen Zähler. Die Idee ist, dem Mauszeiger das Gefühl zu geben, dass eine Sekunde vergangen ist. Aber bedenken Sie: Dies ist nur ein Näherungswert. Wir haben kein perfektes Timing. Dabei geht es nicht um strikte Genauigkeit, sondern vielmehr darum, dem Nutzer ein allgemeines Gefühl dafür zu vermitteln, wie viel Zeit noch verbleibt, bevor sich der Balken schließt.

Gehen wir zu einer anderen Stelle in diesem Code und zwar zu der Prozedur, die in Zeile 80 beginnt. Hier werden die durchgestrichenen Zeilen durch einen Aufruf von RateUpdate ersetzt. Dieses Mal ist das übergebene Argument jedoch wahr. Wenn wir ein neuen Tick setzen, sollte das Argument true sein. Wenn wir nur die Zeit aktualisieren (ohne einen Tick zu erhalten), sollte das Argument false sein. Interessant, nicht wahr? Werfen wir nun einen Blick auf die Prozedur RateUpdate selbst, die in Zeile 69 beginnt.

Die Funktion RateUpdate musste geschaffen werden, da bei der direkten Aktualisierung der Zeit das Risiko bestand, versehentlich einige Ticks auszulassen. Dies bezieht sich auf Zeile 110. Um dies zu vermeiden, haben wir die ZeiBalkenualisierungslogik in eine eigene Funktion ausgelagert. Sie werden feststellen, dass die Variable, die zuvor in Zeile 85 deklariert war, in Zeile 71 verschoben wurde. Ebenso wird die Arbeit, die zuvor in den Zeilen 108 und 109 erledigt wurde, nun in den Zeilen 74 und 75 erledigt. Im Grunde genommen ist diese Funktion fast eine Kopie dessen, was wir vorher hatten. Der Unterschied liegt in Zeile 73, aber beachten Sie, dass diese Funktion immer Null zurückgibt. Das ist gewollt, denn wir gehen davon aus, dass wir bei der Zeile 242 etwas erwarten.

Aber kommen wir zurück zur Frage der Zeile 73. Die Wirkung dieser Zeile ist recht interessant. Sie sehen, es ist nicht notwendig, die Zeit genau zu bestimmen. Sie muss nur einigermaßen nah sein. Wenn ein Tick von den echten Daten kommt, wird er von CreateBarInReplay verarbeitet. In diesem Fall spiegelt der st_Spread-Wert den Zeitstempel dieses Ticks wider. Wenn der Aufruf jedoch von LoopEventOnTime kommt, wird st_Spread einfach um eins erhöht. Dies entspricht einem Schritt von einer Sekunde. Unabhängig vom st_Spread-Wert wird der Wert korrigiert, sobald der nächste echte Tick eintrifft, und wieder an den Echtzeitwert angepasst. Wenn also die Liquidität sinkt und eine Verzögerung von 50 Sekunden zwischen den Ticks besteht, kann der Timer leicht vor- oder nachlaufen. Sie werden sehen, dass der Mauszeiger einen Wert anzeigt, dann einen etwas anderen, der sich nicht unbedingt um eine Sekunde unterscheidet. Dies ist kein Fehler. Sie bietet sogar einen kleinen Vorteil. Wenn die Liquidität für einige Sekunden versiegt, können Sie den Dienst einfach unterbrechen und sofort wieder aufnehmen. Dadurch überspringt das System effektiv die lange Wartezeit. Interessant, nicht wahr?


Abschließende Überlegungen

Um ein klareres Bild von dem zu bekommen, was ich gerade erklärt habe, können Sie Simulations-/Wiederholungs-Ticks für einen Vermögenswert mit geringer Liquidität verwenden. Aber auch wenn Sie das nicht tun, zeigt das folgende Video den Pause-Play-Trick in Aktion, mit dem Sie lange Wartezeiten überbrücken können.

Es gibt jedoch noch eine weitere Frage, die wir klären müssen: Wie kann uns der Mauszeiger sagen, wann ein Vermögenswert in den Auktionsmodus übergegangen ist? Das ist ein heikles Thema, so komplex, dass es einen eigenen Artikel verdient. Ja, die Lösung ist bereits im Mauszeiger implementiert. Wenn Sie ihn auf einem Live-Chart platzieren und einen Vermögenswert mit Echtzeitdaten verfolgen, werden Sie sehen, dass der Indikator deutlich anzeigt, wenn der Vermögenswert in eine Auktion eintritt. Aber im Fall unserer Wiedergabe/Simulation, wo wir nutzerdefinierte Assets verwenden, wird dies zu einer Herausforderung. Hier gibt es ein spezielles Problem, das die Sache für uns kompliziert macht. Und das, liebe Leserinnen und Leser, wird das Thema unseres nächsten Artikels sein. Bis bald!


Demo-Video

Übersetzt aus dem Portugiesischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/pt/articles/12317

Beigefügte Dateien |
Anexo.zip (420.65 KB)
Neuronale Netze im Handel: Kontrollierte Segmentierung (letzter Teil) Neuronale Netze im Handel: Kontrollierte Segmentierung (letzter Teil)
Wir setzen die im vorigen Artikel begonnene Arbeit am Aufbau des RefMask3D-Frameworks mit MQL5 fort. Dieser Rahmen wurde entwickelt, um multimodale Interaktion und Merkmalsanalyse in einer Punktwolke umfassend zu untersuchen, gefolgt von der Identifizierung des Zielobjekts auf der Grundlage einer in natürlicher Sprache gegebenen Beschreibung.
Neuronale Netze im Handel: Verallgemeinerte 3D-Segmentierung von referenzierten Ausdrücken Neuronale Netze im Handel: Verallgemeinerte 3D-Segmentierung von referenzierten Ausdrücken
Bei der Analyse der Marktsituation unterteilen wir den Markt in einzelne Segmente und ermitteln die wichtigsten Trends. Herkömmliche Analysemethoden konzentrieren sich jedoch oft auf einen Aspekt und schränken so die richtige Wahrnehmung ein. In diesem Artikel lernen wir eine Methode kennen, die die Auswahl mehrerer Objekte ermöglicht, um ein umfassenderes und vielschichtigeres Verständnis der Situation zu gewährleisten.
Algorithmus für eine auf künstlichen Ökosystemen basierende Optimierung (AEO) Algorithmus für eine auf künstlichen Ökosystemen basierende Optimierung (AEO)
Der Artikel befasst sich mit einem metaheuristischen AEO-Algorithmus (Artificial Ecosystem-based Optimization), der Interaktionen zwischen Ökosystemkomponenten simuliert, indem er eine anfängliche Lösungspopulation erstellt und adaptive Aktualisierungsstrategien anwendet, und beschreibt im Detail die Phasen des AEO-Betriebs, einschließlich der Verbrauchs- und Zersetzungsphasen, sowie verschiedene Agentenverhaltensstrategien. Der Artikel stellt die Merkmale und Vorteile dieses Algorithmus vor.
Von der Grundstufe bis zur Mittelstufe: Arrays und Zeichenketten (III) Von der Grundstufe bis zur Mittelstufe: Arrays und Zeichenketten (III)
Dieser Artikel behandelt zwei Aspekte. Erstens, wie die Standardbibliothek binäre Werte in andere Darstellungen wie oktal, dezimal und hexadezimal konvertieren kann. Zweitens werden wir darüber sprechen, wie wir die Breite unseres Passworts auf der Grundlage der geheimen Phrase bestimmen können, indem wir das bereits erworbene Wissen nutzen.