![Entwicklung eines Replay Systems — Marktsimulation (Teil 25): Vorbereitungen für die nächste Phase](https://c.mql5.com/2/58/replay-p25_600x314.jpg)
Entwicklung eines Replay Systems — Marktsimulation (Teil 25): Vorbereitungen für die nächste Phase
Einführung
Im vorherigen Artikel „Entwicklung eines Replay-Systems — Marktsimulation (Teil 24): FOREX (V)“ Ich habe gezeigt, wie wir zwei auf den ersten Blick unterschiedliche Universen harmonisch integrieren können. Auf der einen Seite gibt es einen Markt mit einem auf den Bid-Preisen basierenden Chart, auf der anderen Seite gibt es einen Markt, der auf den Kursen „Last“ basiert. Unser Ziel war es, eine Methode zu entwickeln, die die wahrscheinliche Kursbewegung simuliert oder genauer gesagt generiert, indem nur Balken gezählt werden, die idealerweise eine 1-Minuten-Chartzeit darstellen. Dies war eine sehr interessante Herausforderung. Die vorgestellte Lösung ist zwar wirksam, aber nicht die einzige Möglichkeit, dieses spezifische Ziel zu erreichen. Da sich die Lösung jedoch als wirksam erwiesen hat, betrachte ich diese Phase als abgeschlossen. Bis es nicht in der Lage ist, ein bestimmtes Modell zu lösen. In diesem Fall werden wir die vorgeschlagene Lösung nochmals verbessern, damit sie das ungelöste Modell abdecken kann.
Es gibt einige Korrekturen, die wir noch vornehmen müssen. In Wirklichkeit geht es aber nicht darum, Änderungen vorzunehmen, sondern darum, einige Funktionen zu eliminieren, die Elemente, die wir noch implementieren wollen, erheblich stören könnten. Einer dieser Aspekte ist die Möglichkeit, in der Zeit zurückzugehen. Der Punkt, mit Hilfe der Steuerung sich in die Vergangenheit zurück zu bewegen, muss aus dem System entfernt werden. Diese Funktion erweist sich auf lange Sicht als unpraktisch und wird, auch wenn sie noch keine Probleme verursacht, mit Sicherheit Probleme verursachen, wenn wir neue Funktionen einführen. Die Idee, mit Hilfe eines Steuerelements in der Zeit zurückzureisen, könnte für Sie interessant sein. Ich stimme zu, dass es ein interessantes Konzept ist, aber in der Praxis ist es nicht so funktionell. Die Möglichkeit, die Zeit zurückzudrehen, kann in vielen Situationen Kopfzerbrechen bereiten, wenn es darum geht, die damit verbundenen Probleme zu lösen.
Die Behebung dieser Funktion ist nicht besonders schwierig, nur ein wenig mühsam, da sie das Hinzufügen von Tests und Prüfungen für die Kontrollanzeige erfordert. Ich erwäge auch, ein weiteres Element aus dem System zu entfernen. Ich werde diese Entscheidung in diesem Artikel treffen. Neben dieser Änderung der Kontrollanzeige werde ich mich auch auf einige andere Punkte konzentrieren, die verbessert werden müssen, damit der Dienst effektiv funktioniert. Ich lade Sie ein, die Entwicklung in diesem Artikel zu verfolgen, der sehr informativ zu sein verspricht. Heute werden wir eine Menge interessanter Konzepte behandeln, die Ihnen beim Erlernen von Programmierung und Systementwicklung sicher von Nutzen sein werden. Beginnen wir mit dem ersten Thema dieses Artikels.
Einschränkung der Verwendung des Kontrollindikators
Wir beginnen mit der Einführung einiger Einschränkungen für den Kontrollindikator, damit der Nutzer nicht in der Zeit zurückgehen kann. Mit „in der Zeit zurückgehen“ meine ich, dass es nach einem bestimmten Fortschritt nicht mehr möglich ist, mit dem Kontrollanzeiger zu einer früheren Position zurückzukehren. Um die Aktionen rückgängig zu machen, müssen Sie den Replay- oder Wiedergabe-/Simulationsdienst schließen und den Prozess von Anfang an neu starten. Ich verstehe, dass diese Einschränkung entmutigend erscheinen mag, aber glauben Sie mir, dieser Ansatz wird in Zukunft viele Probleme vermeiden, die bei der Verwendung der Rücksprungfunktion auftreten können.
Die Umsetzung dieser Einschränkung ist nicht schwierig, erfordert aber einen gewissen Aufwand, da Sie dem System spezifische Tests hinzufügen müssen. Diese Tests müssen mit Bedacht eingesetzt werden, damit sie nicht zu Konflikten mit anderen Funktionen des Indikators führen, damit dieser effektiv arbeiten kann. Wir werden diese Aufgabe in mehrere Schritte unterteilen, um die effiziente Umsetzung von Änderungen zu erleichtern.
Erste Schritte Ein- und Ausschalten der Feinabstimmungstasten
Die Aufgabe ist in diesem Stadium relativ einfach. Dazu muss der Zugriff auf die Tasten für die Feineinstellung an den Enden des Bedienfelds aktiviert oder deaktiviert werden. Diese Schaltflächen sind in der nachstehenden Abbildung dargestellt:
Abbildung 01: Tasten für die Feinabstimmung
Diese Tasten erleichtern die Feinabstimmung der gewünschten Vorschubgeschwindigkeit. Mit ihnen kann man sich mit großer Präzision für eine bestimmte Zeit vorwärts oder rückwärts bewegen, was sehr nützlich ist. Um jedoch zu verhindern, dass der Nutzer in der Zeit zurückgeht, ist es wichtig, diese Schaltflächen ein- oder auszublenden, je nachdem, ob ihre Anwesenheit notwendig ist. Um diesen Schritt besser zu verstehen, sollten Sie sich Folgendes vor Augen halten: Warum sollte die Schaltfläche auf der linken Seite aktiv bleiben, wenn das System keine einzige Position vorverlegt hat? Oder warum brauchen wir die Schaltfläche auf der rechten Seite, wenn das System seine maximale Vorrückungsgrenze erreicht hat? Also, wir haben die letzten generierten Ticks präsentiert. Warum brauchen wir also die rechte Taste? Es besteht also kein Grund, sie zu behalten? In dieser Phase wird der Nutzer darüber informiert, dass es nicht möglich ist, über die festgelegte Grenze hinaus vorwärts oder rückwärts zu gehen.
Diese Aufgabe ist leicht und einfach, denn die Hauptsache ist, dass man die Grenzwerte überprüft. Wenn wir die Grenzen erreichen, an denen eine weitere Bewegung nicht mehr möglich ist, sollten wir die Tastenanzeige deaktivieren. Ich werde jedoch einen etwas anderen Ansatz wählen, der meiner Meinung nach das Ergebnis interessanter macht. Erstens brauchen wir nicht viel Code zu schreiben, sondern nur kleine Änderungen vorzunehmen. Der erste Schritt besteht darin, die Bitmaps, die die Schaltflächen darstellen, wenn sie deaktiviert sind, als Kontrollindikatorressource aufzunehmen. Dies geschieht wie folgt:
#define def_ButtonPlay "Images\\Market Replay\\Play.bmp" #define def_ButtonPause "Images\\Market Replay\\Pause.bmp" #define def_ButtonLeft "Images\\Market Replay\\Left.bmp" #define def_ButtonLeftBlock "Images\\Market Replay\\Left_Block.bmp" #define def_ButtonRight "Images\\Market Replay\\Right.bmp" #define def_ButtonRightBlock "Images\\Market Replay\\Right_Block.bmp" #define def_ButtonPin "Images\\Market Replay\\Pin.bmp" #define def_ButtonWait "Images\\Market Replay\\Wait.bmp" #resource "\\" + def_ButtonPlay #resource "\\" + def_ButtonPause #resource "\\" + def_ButtonLeft #resource "\\" + def_ButtonLeftBlock #resource "\\" + def_ButtonRight #resource "\\" + def_ButtonRightBlock #resource "\\" + def_ButtonPin #resource "\\" + def_ButtonWait
Diese Zeilen fügen Bitmaps innerhalb des Kontrollindikators hinzu, die deaktivierte Schaltflächen an den Grenzen symbolisieren. Dies macht die Schnittstelle attraktiver und ermöglicht es Ihnen, Schaltflächen zu erstellen, deren Aussehen besser zu dem passt, was Sie dem Nutzer anbieten möchten. Sie können gerne Änderungen vornehmen. Sobald dieser Schritt abgeschlossen ist, müssen wir uns auf diese Werte beziehen. Der Code ist fast fertig, wir müssen nur noch auf diese Ressourcen verweisen. Dies geschieht im folgenden Code:
void CreteCtrlSlider(void) { u_Interprocess Info; m_Slider.szBarSlider = def_NameObjectsSlider + " Bar"; m_Slider.szBtnLeft = def_NameObjectsSlider + " BtnL"; m_Slider.szBtnRight = def_NameObjectsSlider + " BtnR"; m_Slider.szBtnPin = def_NameObjectsSlider + " BtnP"; m_Slider.posY = 40; CreteBarSlider(82, 436); CreateObjectBitMap(52, 25, m_Slider.szBtnLeft, def_ButtonLeft, def_ButtonLeftBlock); CreateObjectBitMap(516, 25, m_Slider.szBtnRight, def_ButtonRight, def_ButtonRightBlock); CreateObjectBitMap(def_MinPosXPin, m_Slider.posY, m_Slider.szBtnPin, def_ButtonPin); ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_ANCHOR, ANCHOR_CENTER); if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.u_Value.df_Value = 0; PositionPinSlider(Info.s_Infos.iPosShift);
Das Einfügen dieser Verweise ermöglicht es dem für die Schaltflächen zuständigen Objekt, sie so zu verarbeiten, dass das gewünschte Ergebnis erzielt wird. Bitte beachten Sie, dass ich bisher nichts anderes als Ressourcenlinks hinzugefügt habe und das System nun die erwartete Funktion erfüllen kann. Um jedoch die Schaltflächen zu ändern, wenn die Einstellgrenzen erreicht sind, müssen wir ein wenig mehr Code hinzufügen. Aber keine Sorge, das ist eine ziemlich einfache und unkomplizierte Aufgabe. Der erforderliche Code ist nachstehend aufgeführt:
inline void PositionPinSlider(int p, const int minimal = 0) { m_Slider.posPinSlider = (p < 0 ? 0 : (p > def_MaxPosSlider ? def_MaxPosSlider : p)); ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_XDISTANCE, m_Slider.posPinSlider + def_MinPosXPin); ObjectSetInteger(m_id, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != minimal); ObjectSetInteger(m_id, m_Slider.szBtnRight, OBJPROP_STATE, m_Slider.posPinSlider < def_MaxPosSlider); ChartRedraw(); }
Ich habe dem Aufruf einen neuen Parameter hinzugefügt, aber da wir das System zunächst im Standardmodus verwenden werden, beginnt dieser Parameter mit dem Wert Null. Das bedeutet, dass derzeit kein Bedarf für Änderungen besteht. Danach können wir beginnen, die Grenzen in bestimmten Situationen zu testen. Um die Schaltfläche links neben einem Steuerelement zu aktivieren oder zu deaktivieren, verwenden wir eine Berechnung. Um die Schaltfläche in der rechten Ecke des Steuerelements zu deaktivieren, führen wir eine weitere Berechnung durch. Bei einem Rechtsklick wird bei der Berechnung nur berücksichtigt, ob der Schieberegler die obere Grenze erreicht hat oder nicht. Die linke Taste funktioniert jedoch anders und basiert zunächst nur auf dem Nullwert. Nach dem Kompilieren des Steuerungsindikatorcodes und dem Ausführen des Wiedergabe-/Simulationsdienstes wird das in der folgenden Animation gezeigte Verhalten sichtbar:
Animation 01: Demonstration des Ein/Aus-Knopfsystems
Die Lösung war sehr einfach zu verstehen und zu implementieren und war ein großartiger Ausgangspunkt für das, was wir wirklich entwickeln mussten. Nun stehen wir vor einer etwas schwierigeren Aufgabe, die jedoch notwendig ist, damit der Nutzer versteht, was geschieht. Wir werden diese Frage im nächsten Thema ausführlich behandeln.
Benachrichtigung des Nutzers über Grenzwertänderungen
Wir könnten den Prozess recht einfach gestalten, indem wir die linke Taste einfach ein- und ausschalten, wenn der Schieberegler den vom Wiedergabe-/Simulationsdienst festgelegten Mindestwert erreicht. Dies kann jedoch den Nutzer verwirren, da er nicht in der Lage ist, die Steuerung rückwärts, d.h. zum Nullpunkt, zu bewegen. Zum besseren Verständnis sehen Sie sich die folgende Animation an:
Animation 02: Warum kann ich nicht auf Null gehen?
Animation 02 zeigt deutlich die Verwirrung, die der Nutzer erleben kann, wenn der Schieberegler nicht den Nullpunkt erreicht, obwohl die linke Taste anzeigt, dass eine Bewegung nicht möglich ist. Diese Situation zeigt, dass die derzeitigen Angaben nicht eindeutig genug sind. Daher müssen wir die Benachrichtigung über die bestehenden Einschränkungen oder Grenzen verbessern, aufgrund derer der Schieberegler nicht über einen bestimmten Punkt hinaus bewegt werden kann. Bevor wir nun im Detail beschreiben, wie diese Anzeige implementiert wird, fragen Sie sich vielleicht, welche Methode verwendet wird, um die Steuerung zu blockieren, bevor sie den Nullpunkt erreicht. Die Neugierde ist groß! Ich habe keine komplizierten Softwaretricks angewandt, sondern einfach einen Haltepunkt festgelegt. Aber wo? Der Standort ist unten zu sehen:
inline void PositionPinSlider(int p, const int minimal = 0) { m_Slider.posPinSlider = (p < minimal ? minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p)); ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_XDISTANCE, m_Slider.posPinSlider + def_MinPosXPin); ObjectSetInteger(m_id, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != minimal); ObjectSetInteger(m_id, m_Slider.szBtnRight, OBJPROP_STATE, m_Slider.posPinSlider < def_MaxPosSlider); ChartRedraw(); }
Sie fragen sich sicher: „Was wurde hier gemacht?“ Keine Sorge, es gibt ein kleines, aber wichtiges Detail zu beachten: Die Variable minimal ist auf Null gesetzt. Was passiert, wenn wir diesen Wert auf, sagen wir, 100 oder 80 ändern? Wenn Sie den Wert an dieser Stelle markieren, wird die Schaltfläche in der linken Ecke deaktiviert. Dies hindert das System jedoch nicht daran, den Wert zu verringern, wenn der Nutzer mit der linken Maustaste klickt oder den Schieberegler nach links zieht. Das ist richtig. Jetzt setze ich den Schieberegler jedoch genau auf die Position, die durch die Variable minimal definiert ist. Ist das jetzt klar? Ganz gleich, wie sehr der Nutzer versucht, den Schieberegler zu bewegen oder die linke Taste zu drücken, der angegebene Punkt wird nicht unter den als Mindestwert festgelegten Wert fallen.
Interessant, nicht wahr? Die Bestimmung des kleinstmöglichen Wertes ist Aufgabe des Wiedergabe-/Simulationsdienstes, der diesen Wert bei fortschreitender Wiedergabe oder Simulation automatisch anpasst. Der Nutzer kann diesen Punkt jedoch ändern, wenn der Dienst den Mindestwert, der verwendet werden kann, nicht geändert hat. Das mag kompliziert erscheinen, aber es ist einfacher als Sie denken. Darauf gehen wir später ein. Konzentrieren wir uns zunächst auf das Problem, das durch die Animation 02 aufgeworfen wurde, die zeigt, dass der Nutzer keine klaren Hinweise auf die linke Grenze erhält. Dafür gibt es verschiedene Möglichkeiten, von denen einige ästhetisch seltsam und andere ein wenig skurril erscheinen mögen. Wir können eine Zwischenlösung wählen. Wie wäre es mit der Erstellung einer Wandbenachrichtigung? Meiner Meinung nach ist dies eine kluge Wahl, da es einen interessanten visuellen Aspekt bieten kann. Wenn Sie ein Grafiker sind, könnten die Ergebnisse sogar noch besser sein als die hier vorgestellten. Wir werden den folgenden Code verwenden:
inline void CreteBarSlider(int x, int size) { ObjectCreate(m_id, m_Slider.szBarSlider, OBJ_RECTANGLE_LABEL, 0, 0, 0); ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_XDISTANCE, x); ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_YDISTANCE, m_Slider.posY - 4); ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_XSIZE, size); ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_YSIZE, 9); ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BGCOLOR, clrLightSkyBlue); ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BORDER_COLOR, clrBlack); ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_WIDTH, 3); ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BORDER_TYPE, BORDER_FLAT); //--- ObjectCreate(m_id, m_Slider.szBarSliderBlock, OBJ_RECTANGLE_LABEL, 0, 0, 0); ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_XDISTANCE, x); ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_YDISTANCE, m_Slider.posY - 9); ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_YSIZE, 19); ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_BGCOLOR, clrRosyBrown); ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_BORDER_TYPE, BORDER_RAISED); }
Die grün hervorgehobenen Zeilen zeigen den Code an, der diese untere Grenzwertanzeige erzeugt. Und ja, wir verwenden dafür ein Objekt. Wenn Sie es vorziehen, können Sie auch eine Bitmap verwenden, um optisch ansprechendere Ergebnisse zu erzielen. Ich möchte den Code einfach halten, da viele der Leserinnen und Leser vielleicht nur über begrenzte Programmierkenntnisse verfügen. Ein besser zugänglicher Code macht es einfacher zu verstehen, wie alles implementiert wurde. Das Hinzufügen eines Bitmaps oder sogar eines Texturmusters ist einfach und die Ergebnisse können recht interessant sein, vor allem, wenn Sie mit DirectX programmieren. Und ja, mit MQL5 ist dies möglich. Aber das lassen wir auf ein anderes Mal verschieben. Fürs Erste sollten wir die Dinge einfach und funktional halten. Das Ergebnis ist in der folgenden Animation 03 dargestellt:
Animation 03: Jetzt haben wir eine linke Grenze.
Durch die Einführung der linken Grenzwertanzeige ist es für die Nutzer viel einfacher geworden zu verstehen, warum sie nicht zur Wiedergabe/Simulation zurückkehren können. Sie haben jedoch vielleicht bemerkt, dass der obige Code nicht angibt, wie die Größe der linken Begrenzungsleiste geändert wird. Der Code, der diese Größe definiert, ist unten dargestellt:
inline void PositionPinSlider(int p, const int minimal = 0) { m_Slider.posPinSlider = (p < minimal ? minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p)); ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_XDISTANCE, m_Slider.posPinSlider + def_MinPosXPin); ObjectSetInteger(m_id, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != minimal); ObjectSetInteger(m_id, m_Slider.szBtnRight, OBJPROP_STATE, m_Slider.posPinSlider < def_MaxPosSlider); ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, minimal + 2); ChartRedraw(); }
Die Größe des Anzeigebalkens wird durch die Variable minimal bestimmt. Wenn der Wiedergabe-/Modellierungsdienst seine Daten aktualisiert, wird der Balken entsprechend angepasst. Der nächste Schritt besteht darin, sicherzustellen, dass diese Grenze vom Wiedergabe-/Simulationsdienst korrekt aktualisiert wird. Das nächste Thema wird sich damit befassen.
Sprechen mit dem Wiedergabe-/Simulationsdienst
Da der Kern des Systems so eingerichtet ist, dass der Nutzer nicht in der Zeit zurückgehen kann, wenn die Eingabeaufforderung erscheint, muss der Wiedergabe-/Simulationsdienst dem Kontrollindikator mitteilen, ab welchem Punkt der Nutzer nicht mehr in der Zeit zurückgehen kann. Diese Aufgabe ist im Vergleich zu dem, was wir bereits getan haben, relativ einfach. Die Hauptsache ist, dass die aktuelle Position des Wiedergabe-/Simulationsdienstes zum Zeitpunkt der Pause überprüft wird. Dieser Teil ist einfach. Schauen wir uns an, wie man die notwendige Funktionalität implementiert. Zunächst müssen Sie eine kleine Änderung am Code vornehmen, der nun wie folgt aussieht:
class C_Controls { private : //+------------------------------------------------------------------+ string m_szBtnPlay; long m_id; bool m_bWait; struct st_00 { string szBtnLeft, szBtnRight, szBtnPin, szBarSlider, szBarSliderBlock; int posPinSlider, posY, Minimal; }m_Slider; //+------------------------------------------------------------------+ void CreteCtrlSlider(void) { u_Interprocess Info; m_Slider.szBarSlider = def_NameObjectsSlider + " Bar"; m_Slider.szBarSliderBlock = def_NameObjectsSlider + " Bar Block"; m_Slider.szBtnLeft = def_NameObjectsSlider + " BtnL"; m_Slider.szBtnRight = def_NameObjectsSlider + " BtnR"; m_Slider.szBtnPin = def_NameObjectsSlider + " BtnP"; m_Slider.posY = 40; CreteBarSlider(82, 436); CreateObjectBitMap(52, 25, m_Slider.szBtnLeft, def_ButtonLeft, def_ButtonLeftBlock); CreateObjectBitMap(516, 25, m_Slider.szBtnRight, def_ButtonRight, def_ButtonRightBlock); CreateObjectBitMap(def_MinPosXPin, m_Slider.posY, m_Slider.szBtnPin, def_ButtonPin); ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_ANCHOR, ANCHOR_CENTER); if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.u_Value.df_Value = 0; m_Slider.Minimal = Info.s_Infos.iPosShift; PositionPinSlider(Info.s_Infos.iPosShift); } //+------------------------------------------------------------------+ inline void PositionPinSlider(int p, const int minimal = 0) { m_Slider.posPinSlider = (p < minimal ? minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p)); m_Slider.posPinSlider = (p < m_Slider.Minimal ? m_Slider.Minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p)); ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_XDISTANCE, m_Slider.posPinSlider + def_MinPosXPin); ObjectSetInteger(m_id, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != minimal); ObjectSetInteger(m_id, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != m_Slider.Minimal); ObjectSetInteger(m_id, m_Slider.szBtnRight, OBJPROP_STATE, m_Slider.posPinSlider < def_MaxPosSlider); ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, minimal + 2); ObjectSetInteger(m_id, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, m_Slider.Minimal + 2); ChartRedraw(); } //+------------------------------------------------------------------+
Sie werden feststellen, dass der Code einige einfache Anpassungen erfahren hat, die ausreichen, um sicherzustellen, dass die Begrenzungsleiste sowie die Schaltflächen korrekt erstellt und konfiguriert werden. Dazu mussten wir die Variable aus dem Funktionsaufruf herausnehmen und innerhalb der Struktur platzieren. Sie wird an einer bestimmten Stelle im Code initialisiert, sodass in Zukunft an den entsprechenden Stellen auf sie zugegriffen werden kann. Warum habe ich diesen Ansatz gewählt? Dies geschah, um Anpassungen an anderen Stellen des Codes zu vermeiden. Jedes Mal, wenn der Wiedergabe-/Simulationsdienst ausgesetzt wird, wird die Funktion CreateCtrlSlider aufgerufen. Selbst wenn einige Objekte zerstört werden, wird der Aufruf dieser Funktion trotzdem erfolgen, was die gesamte Erstellungslogik vereinfacht.
Nachdem wir nun das Problem mit den Kontrollindikatoren gelöst haben, ist es an der Zeit, sich auf den Code des Wiedergabe-/Simulationsdienstes zu konzentrieren und einige Änderungen vorzunehmen. Obwohl viele dieser Änderungen eher ästhetischer Natur sind, ist es wichtig sicherzustellen, dass das System reibungslos funktioniert, bevor komplexere Probleme in Angriff genommen werden.
Lösung ästhetischer Probleme im Replay/Simulationsdienst
Das erste Problem, das wir lösen müssen, ist nicht nur ein ästhetisches, sondern auch ein technisches. Dies ist der Fall, wenn der Wiedergabe-/Simulationsdienst aufgefordert wird, sich zu einer zukünftigen Position zu bewegen, bevor die Wiedergabe beginnt. Mit anderen Worten: Wenn Sie den Dienst gerade erst geöffnet haben und, anstatt auf „Play“ zu drücken, beschließen, das Chart um einige Positionen nach vorne zu verschieben und erst dann „Play“ zu starten, dann gibt es ein Problem mit der korrekten Anzeige der Chart. Um dies zu beheben, müssen Sie das System zwingen, ein „falsches play“ durchzuführen und sich dann zu der vom Schieberegler angezeigten Position zu bewegen. Die erforderliche Code-Änderung ist unten dargestellt:
void AdjustPositionToReplay(const bool bViewBuider) { u_Interprocess Info; MqlRates Rate[def_BarsDiary]; int iPos, nCount; Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); if ((m_ReplayCount == 0) && (m_Ticks.ModePlot == PRICE_EXCHANGE)) for (; m_Ticks.Info[m_ReplayCount].volume_real == 0; m_ReplayCount++); if (Info.s_Infos.iPosShift == (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks)) return; iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1))); Rate[0].time = macroRemoveSec(m_Ticks.Info[iPos].time); if (iPos < m_ReplayCount) { CustomRatesDelete(def_SymbolReplay, Rate[0].time, LONG_MAX); CustomTicksDelete(def_SymbolReplay, m_Ticks.Info[iPos].time_msc, LONG_MAX); if ((m_dtPrevLoading == 0) && (iPos == 0)) FirstBarNULL(); else { for(Rate[0].time -= 60; (m_ReplayCount > 0) && (Rate[0].time <= macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)); m_ReplayCount--); m_ReplayCount++; } }else if (iPos > m_ReplayCount) { CreateBarInReplay(true); if (bViewBuider) { Info.s_Infos.isWait = true; GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); }else { for(; Rate[0].time > (m_Ticks.Info[m_ReplayCount].time); m_ReplayCount++); for (nCount = 0; m_Ticks.Rate[nCount].time < macroRemoveSec(m_Ticks.Info[iPos].time); nCount++); nCount = CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, nCount); } for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag);) CreateBarInReplay(false); CustomTicksAdd(def_SymbolReplay, m_Ticks.Info, m_ReplayCount); Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); Info.s_Infos.isWait = false; GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); }
Dieser Codeaufruf ist entscheidend für die Erzeugung des erforderlichen „falschen Play“. Ohne diesen Aufruf erscheint ein Fehler in der Chartdarstellung. Außerdem habe ich eine zusätzliche Zeile in den Code eingefügt, die die fehlenden Ticks zum Fenster der Marktübersicht hinzufügt und so eine realistischere und interessantere Wiedergabe ermöglicht. Auch andere Änderungen wurden am Code vorgenommen, wie aus den durchgestrichenen Zeilen ersichtlich ist. Diese Kontrollen verhindern, dass wir das System betreten, wenn wir uns in der gleichen Bewegungsposition befinden. Dies ist eine direkte Folge unserer Entscheidung, dem Nutzer nicht zu erlauben, in der Zeit zurückzugehen, sodass Codes, die mit dieser Funktion verbunden sind, entfernt werden können.
Nachdem wir diesen Fehler behoben haben, wollen wir uns nun einem ästhetischen Problem zuwenden, das schon seit langem besteht, das wir aber jetzt angehen können, indem wir die Nutzererfahrung mit dem Wiedergabe-/Simulationsdienst angenehmer gestalten. Dieses ästhetische Problem tritt auf, wenn eine Datei zur Darstellung früherer Balken im Chart ausgewählt wird. Beim Öffnen eines Charts über den Replay/Simulationsdienst werden die Kurslinien zunächst nicht angezeigt. Obwohl dies die Funktionalität des Systems nicht beeinträchtigt, ist es aus ästhetischer Sicht unpraktisch, das Chart ohne Preislinie zu betrachten. Um diesen Teil zu beheben oder zu bewältigen, sind einige Änderungen erforderlich. Die erste dieser Änderungen wird im Folgenden vorgestellt:
bool LoopEventOnTime(const bool bViewBuider) { u_Interprocess Info; int iPos, iTest; if (!m_Infos.bInit) ViewInfos(); if (!m_Infos.bInit) { ChartSetInteger(m_IdReplay, CHART_SHOW_ASK_LINE, m_Ticks.ModePlot == PRICE_FOREX); ChartSetInteger(m_IdReplay, CHART_SHOW_BID_LINE, m_Ticks.ModePlot == PRICE_FOREX); ChartSetInteger(m_IdReplay, CHART_SHOW_LAST_LINE, m_Ticks.ModePlot == PRICE_EXCHANGE); m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); m_MountBar.Rate[0].time = 0; m_Infos.bInit = true; ChartRedraw(m_IdReplay); } iTest = 0; while ((iTest == 0) && (!_StopFlag)) { iTest = (ChartSymbol(m_IdReplay) != "" ? iTest : -1); iTest = (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value) ? iTest : -1); iTest = (iTest == 0 ? (Info.s_Infos.isPlay ? 1 : iTest) : iTest); if (iTest == 0) Sleep(100); } if ((iTest < 0) || (_StopFlag)) return false; AdjustPositionToReplay(bViewBuider); iPos = 0; while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag)) { iPos += (int)(m_ReplayCount < (m_Ticks.nTicks - 1) ? m_Ticks.Info[m_ReplayCount + 1].time_msc - m_Ticks.Info[m_ReplayCount].time_msc : 0); CreateBarInReplay(true); while ((iPos > 200) && (!_StopFlag)) { if (ChartSymbol(m_IdReplay) == "") return false; GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value); if (!Info.s_Infos.isPlay) return true; Info.s_Infos.iPosShift = (ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks); GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); Sleep(195); iPos -= 200; } } return (m_ReplayCount == m_Ticks.nTicks); }
Wir entfernen die durchgestrichenen Teile des Codes und fügen eine hervorgehobene neue Zeile ein. Wir könnten den Code für diesen Aufruf hier einfügen, aber in Zukunft wird dieser Code höchstwahrscheinlich in eine andere Funktion verschoben werden. Um die künftige Übertragbarkeit zu erleichtern, ziehe ich es daher vor, den erforderlichen Code an anderer Stelle zu sammeln.
Um das ästhetische Problem zu lösen, dass die Kurslinien nicht sofort angezeigt werden, wenn das Chart vom Replay/Simulationsdienst geöffnet wird, ist der folgende Code erforderlich:
void ViewInfos(void) { MqlRates Rate[1]; ChartSetInteger(m_IdReplay, CHART_SHOW_ASK_LINE, m_Ticks.ModePlot == PRICE_FOREX); ChartSetInteger(m_IdReplay, CHART_SHOW_BID_LINE, m_Ticks.ModePlot == PRICE_FOREX); ChartSetInteger(m_IdReplay, CHART_SHOW_LAST_LINE, m_Ticks.ModePlot == PRICE_EXCHANGE); m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); m_MountBar.Rate[0].time = 0; m_Infos.bInit = true; CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, Rate); if ((m_ReplayCount == 0) && (m_Ticks.ModePlot == PRICE_EXCHANGE)) for (; m_Ticks.Info[m_ReplayCount].volume_real == 0; m_ReplayCount++); if (Rate[0].close > 0) { if (m_Ticks.ModePlot == PRICE_EXCHANGE) m_Infos.tick[0].last = Rate[0].close; else { m_Infos.tick[0].bid = Rate[0].close; m_Infos.tick[0].ask = Rate[0].close + (Rate[0].spread * m_Infos.PointsPerTick); } m_Infos.tick[0].time = Rate[0].time; m_Infos.tick[0].time_msc = Rate[0].time * 1000; }else m_Infos.tick[0] = m_Ticks.Info[m_ReplayCount]; CustomTicksAdd(def_SymbolReplay, m_Infos.tick); ChartRedraw(m_IdReplay); }
Diese Codezeilen wurden in der vorherigen Funktion durchgestrichen. Es sollte jedoch auf die zusätzlich erforderlichen Leitungen geachtet werden. Wir identifizieren den letzten Balken, der vom Replay/Simulationsdienst auf dem Chart platziert wurde, indem wir eine allgemeine Funktion für die Arbeit mit Indikatoren verwenden. Wenn es uns gelingt, den Balken zu erfassen, d.h. wenn der Schlusswert größer als Null ist, setzen wir je nach Konstruktionsmodus einen speziellen Tick. Wenn der Schlusswert Null ist, wird der erste gültige Tick aus der Liste der geladenen oder simulierten Ticks verwendet. Die Funktion, die für die Suche nach einem gültigen Tick zuständig ist, befindet sich genau in den beiden genannten Zeilen. Diese Funktion ist besonders nützlich, wenn Sie mit dem Darstellungsmodus Last-Preise arbeiten, da im Bid-Modus der erste Tick bereits gültig ist. Letztendlich wird dieser speziell erstellte Tick im Fenster der Marktübersicht angezeigt, sodass Kurslinien auf dem Chart erscheinen, sobald der Dienst der MetaTrader 5 Plattform befiehlt, den Chart zu öffnen.
Wir brauchen noch eine weitere Änderung, weil wir ein Problem mit der Wiedergabe/Modellierung haben, die zwar etwas unzuverlässig ist, aber keine Balken vor dem zu präsentierenden Datensatz anzeigt. In diesem Fall kann die Konstruktion des ersten Balkens abgeschnitten werden. Um dieses Problem endgültig zu lösen, müssen wir den Balken angeben, der dem gesamten Satz, der später präsentiert wird, vorausgeht. Diese Codeänderung ermöglicht es dem System, auf Charts mit unterschiedlichen Zeitintervallen korrekt zu arbeiten: von 1 Minute bis 1 Tag und sogar 1 Woche. Nun, ich denke, 1 Monat ist zu viel.
inline void FirstBarNULL(void) { MqlRates rate[1]; int c0 = 0; for(; (m_Ticks.ModePlot == PRICE_EXCHANGE) && (m_Ticks.Info[c0].volume_real == 0); c0++); rate[0].close = (m_Ticks.ModePlot == PRICE_EXCHANGE ? m_Ticks.Info[c0].last : m_Ticks.Info[c0].bid); rate[0].open = rate[0].high = rate[0].low = rate[0].close; rate[0].tick_volume = 0; rate[0].real_volume = 0; rate[0].time = macroRemoveSec(m_Ticks.Info[c0].time) - 86400; CustomRatesUpdate(def_SymbolReplay, rate); m_ReplayCount = 0; }
Der erste Schritt besteht darin, einen gültigen Tick zu finden, vor allem, wenn das Charting-System Last-Kurse verwendet. Sobald dies geschehen ist, wird der vorherige Balken mit dem ersten gültigen Preis der Tickserie erstellt, der in der Wiedergabe oder Simulation verwendet wird. Wichtig ist hier die Angabe der Position in der Zeit, die durch Abzug des Wertes von 1 Tag in Minuten korrigiert wird. Dadurch wird sichergestellt, dass der vorherige Balken im Chart korrekt angezeigt wird und selbst in einem Tageschart vollständig sichtbar ist. Dieses System funktioniert sowohl für Devisenmarktdaten als auch für Aktienmärkte.
Schlussfolgerung
Mit Hilfe der mitgelieferten Anhänge können Sie die aktuelle Implementierung des Replay/Simulationsdienstes testen. Das Basissystem ist fertig, aber da es einige Funktionen gibt, die wir noch nicht nutzen, werden zusätzliche Änderungen und Anpassungen erforderlich sein, um das System an einen effektiveren Schulungsmodus anzupassen. An diesem Punkt betrachten wir das Wiedergabe-/Simulationssystem als vollständig. In den nächsten Artikeln werden wir uns mit Möglichkeiten zur weiteren Verbesserung befassen und damit eine neue Phase in der Entwicklung dieses Systems einleiten.
Übersetzt aus dem Portugiesischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/pt/articles/11203
![Entwicklung eines Replay System (Teil 26): Expert Advisor Projekt — die Klasse C_Terminal](https://c.mql5.com/2/58/replay-p26-avatar.png)
![Quantisierung beim maschinellen Lernen (Teil 1): Theorie, Beispielcode, Analyse der Implementierung in CatBoost](https://c.mql5.com/2/59/Quantization_in_machine_learning_logo.png)
![Von der Saisonalität des Devisenmarktes profitieren](https://c.mql5.com/2/59/Seasonal_analysis_logo_UP.png)
![Datenkennzeichnung für die Zeitreihenanalyse (Teil 4):Deutung der Datenkennzeichnungen durch Aufgliederung](https://c.mql5.com/2/61/Data_label_for_time_series_mining_nPart_45Interpretability_Decomposition_Using_Label_Data_LOGO.png)
![MQL5 - Sprache von Handelsstrategien, eingebaut ins Kundenterminal MetaTrader 5](https://c.mql5.com/i/registerlandings/logo-2.png)
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.