English Русский Español 日本語 Português
preview
Entwicklung eines Replay System (Teil 31): Expert Advisor Projekt — Die Klasse C_Mouse (V)

Entwicklung eines Replay System (Teil 31): Expert Advisor Projekt — Die Klasse C_Mouse (V)

MetaTrader 5Tester | 20 März 2024, 13:06
99 0
Daniel Jose
Daniel Jose

Einführung

Im vorherigen Artikel Entwicklung eines Replay System (Teil 28): Expert Advisor Projekt — Die Klasse C_Mouse (IV), haben wir uns angesehen, wie man das Klassensystem ändern, hinzufügen oder an seinen Stil anpassen kann, um eine neue Ressource oder ein neues Modell zu testen. Auf diese Weise vermeiden wir Abhängigkeiten im Hauptcode und halten ihn stets zuverlässig, stabil und robust, da neue Ressourcen erst dann in das Hauptsystem eingefügt werden, wenn das von Ihnen erstellte Modell vollständig geeignet ist. Die größte Herausforderung bei der Entwicklung des Replay-/Simulationssystems - und vielleicht auch das, was die Aufgabe so schwierig macht - besteht darin, Mechanismen zu entwickeln, die dem System, das wir beim Handel auf einem echten Konto verwenden werden, so nahe wie möglich kommen, wenn nicht sogar identisch sind. Es macht keinen Sinn, ein System zur Erstellung oder Ausführung von Wiederholungen/Simulationen zu erstellen, wenn die gleichen Ressourcen nicht verfügbar sind, wenn das echte Konto verwendet wird.

Wenn Sie sich das System der Klasse C_Mouse und die in den vorangegangenen Artikeln gezeigten analytischen Klassen ansehen, werden Sie feststellen, dass der Timer bei Verwendung in einem Live-Markt, sei es ein Demo- oder ein echtes Konto, immer anzeigt, wann der nächste Balken beginnt. Bei der Verwendung eines Replay-/Simulationssystems können wir uns jedoch nicht darauf verlassen. Hier erhalten wir eine Nachricht. Auf den ersten Blick mag es scheinen, dass eine solche Verletzung der Symmetrie nicht besonders bedeutsam ist. Wenn wir jedoch zulassen, dass sich nutzlose Dinge ansammeln, ohne sie zu korrigieren oder zu entfernen, werden wir mit einem Haufen völlig nutzlosen Gerümpel enden, das die Lösung von Problemen, die wir wirklich lösen müssen, nur behindert. Wir brauchen einen Timer, der anzeigt, wie viel Zeit bis zum Ende der Wiedergabe/Simulation verbleibt. Dies mag auf den ersten Blick eine einfache und schnelle Lösung sein. Viele versuchen einfach, sich anzupassen und das gleiche System zu verwenden, das der Handelsserver verwendet. Aber es gibt eine Sache, die viele Leute nicht bedenken, wenn sie über diese Lösung nachdenken: Bei der Wiederholung und noch mehr bei der Simulation funktioniert die Uhr anders. Dafür gibt es mehrere Gründe:

  • Ein Replay bzw. eine Wiedergabe bezieht sich immer auf die Vergangenheit. Eine Uhr auf einer Plattform oder einem Computer ist also keineswegs eine ausreichende Zeitangabe.
  • Während der Wiedergabe/Simulation können wir die Zeit vorspulen, anhalten oder zurückdrehen. Letzteres ist nicht mehr möglich, und das ist aus verschiedenen Gründen, über die wir in früheren Artikeln berichtet haben, schon vor langer Zeit geschehen. Wir können immer noch vorspulen und pausieren. Der auf dem Handelsserver verwendete Zeitplan ist also nicht mehr angemessen.

Um Ihnen eine Vorstellung davon zu geben, womit wir es eigentlich zu tun haben und wie komplex das Setzen eines Zeitgebers in einem Wiedergabe-/Simulationssystem sein kann, sehen wir uns Abbildung 01 an.

Abbildung 01

Abbildung 01 - Timer auf dem realen Markt

Diese Abbildung 01 zeigt, wie der Timer funktioniert, um anzuzeigen, wann ein neuer Balken im Chart erscheint. Dies ist ein sehr kurzes Diagramm. Von Zeit zu Zeit gibt es das Ereignis OnTime. Es löst das Ereignis Update aus, das den Wert des Timers aktualisiert. Auf diese Weise können wir darstellen, wie viel Zeit noch verbleibt, bis der neue Balken erscheint. Damit die Funktion Update jedoch weiß, welcher Wert angezeigt wird, fragt sie bei der Funktion GetBarTime ab, wie lange es dauert, bis der Balken erscheint. GetBarTime verwendet TimeCurrent, das nicht auf dem Server ausgeführt wird, sondern die lokale Zeit auf dem Server festlegt. So können wir herausfinden, wie viel Zeit vergangen ist, seit der Server beim letzten Balken ausgelöst wurde, und auf der Grundlage dieses Wertes berechnen, wie viel Zeit bis zum Auslösen des neuen Balkens verbleibt. Dies ist der einfachste Teil der ganzen Geschichte, da wir uns keine Gedanken darüber machen müssen, ob das System pausiert hat oder ob eine bestimmte Zeit vergangen ist. Dies ist der Fall, wenn wir ein Asset verwenden, dessen Daten direkt vom Handelsserver stammen. Wenn wir mit dem Wiederholungs-/Simulationssystem arbeiten, werden die Dinge noch viel komplizierter. Das große Problem ist nicht, dass wir mit dem Replay/Simulationsmodus arbeiten. Das Problem besteht darin, einen Weg zu finden, um den Aufruf TimeCurrent in der Wiedergabe/Simulation zu umgehen, da in diesem Moment das ganze Problem auftritt. Dies muss jedoch mit minimalen Änderungen geschehen. Wir wollen nur das Aufrufsystem TimeCurrent umgehen. Wenn wir jedoch mit dem Server arbeiten, soll das System wie in Abbildung 01 dargestellt funktionieren.

Glücklicherweise gibt es mit MetaTrader 5 eine Möglichkeit, dies mit minimalem Aufwand und deutlich weniger Änderungen oder Ergänzungen des bereits implementierten Codes zu erreichen. Genau darüber werden wir in diesem Artikel sprechen.


Planung

Die Planung, wie dies geschehen soll, ist vielleicht der einfachste Teil der Geschichte. Wir werden dem System einfach den vom Replay/Simulationsdienst berechneten Zeitwert senden. Das ist der einfache Teil. Wir werden dafür einfach eine globale Terminalvariable verwenden. In früheren Artikeln haben wir gesehen, wie man diese Variablen verwendet. In einem der Artikel haben wir ein Steuerelement eingeführt, das dem Dienst mitteilt, was der Nutzer tun möchte. Viele Leute denken, dass man mit diesen Variablen nur doppelte Daten übertragen kann. Dabei wird jedoch eine Tatsache vergessen: Binärdateien sind einfach nur Binärdateien, d. h. sie stellen in keinem Fall eine andere Information als Nullen und Einsen dar. Wir können also jede Information über diese globalen Terminalvariablen weitergeben. Vorausgesetzt natürlich, dass wir die Bits logisch anordnen können, sodass wir die Informationen später rekonstruieren können. Glücklicherweise ist der Typ DateTime eigentlich ein ulong-Wert. Wir verwenden 64 Bits, was bedeutet, dass ein DateTime-Wert 8 Bytes an Informationen benötigt, in denen das vollständige Datum und die Uhrzeit, einschließlich Jahr, Monat, Tag, Stunde, Minute und Sekunden, dargestellt sind. Das ist alles, was wir brauchen, um den Aufruf TimeCurrent zu umgehen, da er denselben Wert in 8 Bytes verwendet und zurückgibt. Da der Typ „double“ genau 64 Bits für die Übertragung von Informationen innerhalb der Plattform verwendet, wird dies unsere Lösung sein.

Aber so einfach ist das nicht. Wir haben einige Probleme, die bei der Erläuterung der Umsetzung dieses Systems deutlicher werden. Obwohl der Dienst recht einfach und leicht zu erstellen ist, gibt es eine kleine Komplikation im Zusammenhang mit der erforderlichen Änderung, die darauf abzielt, die Klasse mit Informationen zu versorgen, die im Timer dargestellt werden können. Am Ende der Konstruktion erhalten wir das in Abbildung 02 dargestellte Ergebnis:

Abbildung 02

Abbildung 02 - Generischer Zeitgeber.

Dieser generische Timer wird in der Lage sein, entsprechend zu reagieren, und er wird völlig transparent sein, sodass der Nutzer eine Wiederholungs-/Simulationserfahrung haben wird, die der eines Demo-/Live-Kontos sehr nahe kommt. Das ist der Zweck der Schaffung eines solchen Systems: damit die Erfahrung dieselbe ist und der Nutzer genau weiß, wie er sich verhalten muss, ohne die Nutzung des Systems von Grund auf neu lernen zu müssen.


Umsetzung

Dieser Teil ist der interessanteste in diesem System. An dieser Stelle werden wir uns ansehen, wie wir den TimeCurrent-Aufruf umgehen können. Der Nutzer sollte nicht wissen, ob er das Replay-System verwendet oder mit dem Server interagiert. Um mit der Implementierung des Systems zu beginnen, müssen wir (wie aus den vorangegangenen Themen ersichtlich sein sollte) eine neue globale Terminalvariable hinzufügen. Gleichzeitig benötigen wir die richtigen Mittel, um DateTime-Informationen über die Double-Variable zu übergeben. Hierfür wird die Header-Datei Interprocess.mqh verwendet. Wir fügen auch die folgenden Dinge hinzu:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#define def_SymbolReplay                "RePlay"
#define def_GlobalVariableReplay        def_SymbolReplay + " Infos"
#define def_GlobalVariableIdGraphics    def_SymbolReplay + " ID"
#define def_GlobalVariableServerTime    def_SymbolReplay + " Time"
#define def_MaxPosSlider                400
#define def_ShortName                   "Market " + def_SymbolReplay
//+------------------------------------------------------------------+
union u_Interprocess
{
   union u_0
   {
      double  df_Value;       // Value of the terminal global variable...
      ulong   IdGraphic;      // Contains the Graph ID of the asset...
   }u_Value;
   struct st_0
   {
      bool    isPlay;         // Indicates whether we are in Play or Pause mode...
      bool    isWait;         // Tells the user to wait...
      ushort  iPosShift;      // Value between 0 and 400...
   }s_Infos;
   datetime   ServerTime;
};
//+------------------------------------------------------------------+


Hier wird der Name der globalen Terminalvariable festgelegt, die für die Kommunikation verwendet werden soll. Als Nächstes definieren wir eine Variable, die für den Zugriff auf Daten im Datetime-Format verwendet wird. Danach gehen wir zur Klasse C_Replay und fügen dem Destruktor der Klasse Folgendes hinzu:

~C_Replay()
   {
      ArrayFree(m_Ticks.Info);
      ArrayFree(m_Ticks.Rate);
      m_IdReplay = ChartFirst();
      do
      {
         if (ChartSymbol(m_IdReplay) == def_SymbolReplay)
         ChartClose(m_IdReplay);
      }while ((m_IdReplay = ChartNext(m_IdReplay)) > 0);
      for (int c0 = 0; (c0 < 2) && (!SymbolSelect(def_SymbolReplay, false)); c0++);
      CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX);
      CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX);
      CustomSymbolDelete(def_SymbolReplay);
      GlobalVariableDel(def_GlobalVariableReplay);
      GlobalVariableDel(def_GlobalVariableIdGraphics);
      GlobalVariableDel(def_GlobalVariableServerTime);
      Print("Finished replay service...");
   }


Durch Hinzufügen dieser Zeile stellen wir sicher, dass beim Schließen des Wiedergabe-/Simulationsdienstes die globale Terminalvariable, die für die Übergabe des Zeitwerts an den Timer verantwortlich ist, entfernt wird. Nun müssen wir sicherstellen, dass diese globale Terminalvariable zum richtigen Zeitpunkt erstellt wird. Sie sollten niemals dieselbe globale Variable erstellen, während eine Schleife läuft, da sie zu unterschiedlichen Zeitpunkten ausgelöst werden kann. Sobald das System aktiviert ist, sollte es bereits Zugriff auf den Inhalt der globalen Terminalvariable haben. In gewisser Weise habe ich erwogen, ein Erstellungssystem ähnlich dem Kontrollindikator zu verwenden. Aber da der EA im Moment auf dem Chart vorhanden sein kann oder auch nicht, sollten wir die Terminalvariable für den vom Dienst erstellten Timer verantwortlich machen. Auf diese Weise haben wir zumindest ein Mindestmaß an Kontrolle über unser Handeln. Vielleicht wird sich die Situation in Zukunft ändern. Der folgende Code zeigt, wo er erstellt wird:

bool ViewReplay(ENUM_TIMEFRAMES arg1)
   {
#define macroError(A) { Print(A); return false; }
      u_Interprocess info;
                                
      if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0)
         macroError("Asset configuration is not complete, it remains to declare the size of the ticket.");
      if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0)
         macroError("Asset configuration is not complete, need to declare the ticket value.");
      if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0)
         macroError("Asset configuration not complete, need to declare the minimum volume.");
      if (m_IdReplay == -1) return false;
      if ((m_IdReplay = ChartFirst()) > 0) do
      {
         if (ChartSymbol(m_IdReplay) == def_SymbolReplay)
         {
            ChartClose(m_IdReplay);
            ChartRedraw();
         }
      }while ((m_IdReplay = ChartNext(m_IdReplay)) > 0);
      Print("Waiting for [Market Replay] indicator permission to start replay ...");
      info.ServerTime = m_Ticks.Info[m_ReplayCount].time;
      CreateGlobalVariable(def_GlobalVariableServerTime, info.u_Value.df_Value);
      info.u_Value.IdGraphic = m_IdReplay = ChartOpen(def_SymbolReplay, arg1);
      ChartApplyTemplate(m_IdReplay, "Market Replay.tpl");
      CreateGlobalVariable(def_GlobalVariableIdGraphics, info.u_Value.df_Value);
      while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(750);

      return ((!_StopFlag) && (ChartSymbol(m_IdReplay) != ""));
#undef macroError
   }


In der Mitte dieses Zeilenwirrwarrs befindet sich ein Punkt, der den Wert der Variablen initialisiert. Dadurch wird sichergestellt, dass das System bereits einen Wert hat, mit dem es arbeiten kann, und wir können überprüfen, ob die Wiedergabe/Simulation tatsächlich synchron ist. Unmittelbar danach folgt ein Aufruf, der eine globale Terminalvariable erstellt und initialisiert. Dieser Initialisierungscode ist unten dargestellt:

void CreateGlobalVariable(const string szName, const double value)
   {
      GlobalVariableDel(szName);
      GlobalVariableTemp(szName);     
      GlobalVariableSet(szName, value);
   }


Der Erstellungs- und Initialisierungscode ist sehr einfach. Wenn die Plattform geschlossen wird und eine Anfrage zum Speichern globaler Terminalvariablen gestellt wird, werden die Variablen, die im Wiedergabe-/Simulationssystem verwendet werden, nicht gespeichert. Wir wollen nicht, dass solche Werte gespeichert und später abgerufen werden. Jetzt können wir uns die Timer-Messwerte ansehen. Wenn wir dies jedoch zum jetzigen Zeitpunkt tun, wird der darin enthaltene Wert immer der gleiche sein und uns nicht unbedingt einen praktischen Nutzen bringen. Es sieht so aus, als ob der Wiedergabe-/Simulationsdienst angehalten worden wäre. Aber wir wollen, dass sich der Timer bewegt, wenn er aktiv ist, d. h. im Replay-Modus. So werden wir modellieren, was die Anforderungen an die Funktion TimeCurrent, in der wir die entsprechenden Zeit- und Datumsdaten auf dem Server gefunden bekommen. Dazu muss sich der Wert der globalen Variable etwa jede Sekunde ändern. Es wäre richtig, für diesen Vorgang einen Timer einzustellen. Da wir dazu aber nicht in der Lage sind, brauchen wir ein anderes Mittel, um eine solche Änderung des Wertes einer globalen Variablen zu implementieren.

Um die Zeit zu bestimmen, müssen wir auch einige Ergänzungen in der Klasse C_Replay implementieren. Sie sind im unten stehenden Code zu sehen:

bool LoopEventOnTime(const bool bViewBuider)
   {
      u_Interprocess Info;
      int iPos, iTest, iCount;
                                
      if (!m_Infos.bInit) ViewInfos();
      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);
      Info.ServerTime = m_Ticks.Info[m_ReplayCount].time;
      GlobalVariableSet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
      iPos = iCount = 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;
	    iCount++;
            if (iCount > 4)
            {
               iCount = 0;
               GlobalVariableGet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
               Info.ServerTime += 1;
               GlobalVariableSet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
            }
         }
      }                               
      return (m_ReplayCount == m_Ticks.nTicks);
   }                               


Die neu hinzugefügte Variable wird uns dabei helfen, den Zeitpunkt, zu dem ein neuer Balken beginnen wird, näherungsweise zu bestimmen. Da wir jedoch die Zeit vorverlegen können, muss der von unserem „Server“ angegebene Wert zurückgesetzt werden. Dies geschieht zu dem Zeitpunkt, zu dem die Funktion auf den neuen Beobachtungspunkt zeigt. Auch wenn wir eine Zeit lang im Pausenmodus bleiben, müssen wir sicher sein, dass der Wert angemessen ist. Das eigentliche Problem entsteht, wenn wir den Timer einstellen wollen. Wir können nicht einfach irgendeinen Wert erfassen. Das System kann der auf der Computeruhr angezeigten Echtzeit voraus oder hinterher sein. Aus diesem Grund versuchen wir in diesem frühen Stadium, das richtige Timing zu finden. Wir haben einen Timer in den Generator der Balken eingebaut. Wir werden ihn als Leitfaden verwenden. Er erzeugt etwa alle 195 Millisekunden einen Tick, was uns einer Zählung von 5 Einheiten näher bringt. Da wir mit einem Wert von Null begonnen haben, prüfen wir, ob der Zählerstand größer als 4 ist. Wenn dies geschieht, erhöhen wir die Zeit um eine Einheit, d. h. 1 Sekunde, und dieser Wert wird in einer globalen Terminalvariablen gespeichert, damit der Rest des Systems ihn verwenden kann. Dann wird die gesamte Schleife wiederholt.

So wissen wir, wann ein neuer Balken erscheinen wird. Ja, genau das ist die Idee, aber leichte Abweichungen sind zu verschiedenen Zeiten möglich. Und wenn sie sich häufen, gerät die Zeit aus dem Takt. Was wir brauchen, ist eine akzeptable Zeitsynchronität, aber keine wirkliche Perfektion. In den allermeisten Fällen kommen wir dem ziemlich nahe. Zu diesem Zweck werden wir die oben beschriebene Funktion leicht verändern, um eine größere Synchronität zwischen dem Balken und dem Timer zu gewährleisten. Diese Änderungen werden im Folgenden dargestellt:

//...

   Sleep(195);
   iPos -= 200;
   iCount++;
   if (iCount > 4)
   {
      iCount = 0;
      GlobalVariableGet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
      Info.ServerTime += 1;
      Info.ServerTime = ((Info.ServerTime + 1) < m_Ticks.Info[m_ReplayCount].time ? Info.ServerTime : m_Ticks.Info[m_ReplayCount].time);
      GlobalVariableSet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
   }

//...


Sie denken vielleicht, dass das verrückt ist. In gewisser Weise stimme ich zu, dass dies ein etwas verrückter Schritt meinerseits ist. Aber durch Hinzufügen dieser speziellen Zeile können wir die Synchronisation auf einem akzeptablen Niveau halten. Wenn das Instrument eine gute Liquidität aufweist, sodass Geschäfte in relativ kurzer Zeit, vorzugsweise in weniger als einer Sekunde, zustande kommen, dann haben wir ein ziemlich synchronisiertes System. Es könnte einem nahezu perfekten System sehr nahe kommen. Dies ist jedoch nur möglich, wenn das Instrument tatsächlich über eine ausreichende Liquidität verfügt. Was ist hier eigentlich los? Lassen Sie uns ein wenig nachdenken: Alle 5 Schleifen von etwa 195 Millisekunden führen wir Code aus, um den Timer zu aktualisieren. Die Aktualisierungsrate beträgt also etwa 975 Millisekunden, was bedeutet, dass in jeder Schleife 25 Millisekunden fehlen. Tatsächlich ist dieser Wert aber nicht konstant. Manchmal kann es etwas mehr sein, manchmal etwas weniger. Sie sollten nicht versuchen, die Synchronisierung mit dem neuen Befehl sleep einzurichten, um das System zu zwingen, langsamer zu werden, um diesen Unterschied auszugleichen. Auf den ersten Blick sollte dies funktionieren. Mit der Zeit werden diese Mikrounterschiede jedoch so groß, dass das gesamte System aus dem Takt gerät. Um dieses Problem zu lösen, machen wir etwas anderes. Anstatt zu versuchen, die Uhrzeit zu ermitteln, verwenden wir den Balken selbst, um die Synchronität zu gewährleisten. Wenn die Funktion CreateBarInReplay ausgeführt wird, zeigt sie immer auf den aktuellen Tick. Vergleicht man den Zeitwert dieses Ticks mit dem Zeitwert in der globalen Variablen, so erhält man in einigen Fällen einen Wert größer als 1, in diesem Fall 1 Sekunde. Ist dieser Wert niedriger, d. h. die Variable Info.ServerTime ist um insgesamt 25 Millisekunden verzögert, wird der aktuelle Tickzeitwert verwendet, um die Differenz zu korrigieren und den Timer sehr nahe an den perfekten Wert heranzuführen. Aber, wie ich bereits zu Beginn der Erklärung berichtet habe, passt dieser Mechanismus das System automatisch an, wenn das von uns verwendete Instrument über ausreichend Liquidität verfügt. Wenn der Handel für längere Zeit unterbrochen wird und 5-10 Minuten zwischen einem Handel und einem anderen vergehen, leidet die Genauigkeit des Zeitmessungssystems. Das liegt daran, dass sie jede Sekunde um durchschnittlich 25 Millisekunden zurückbleibt (dies ist ein Durchschnittswert, kein exakter Wert).

Jetzt können wir mit dem nächsten Teil fortfahren. Sie befindet sich in der Header-Datei C_Study.mqh, in der wir das System zwingen werden, Daten zu melden, damit wir richtig einschätzen können, wann ein neuer Balken im Chart erscheinen wird.


Anpassen der Klasse C_Study

Um mit den Änderungen zu beginnen, müssen wir zunächst die unten aufgeführten Änderungen vornehmen:

void Update(void)
   {
      switch (m_Info.Status)
      {
         case eCloseMarket: m_Info.szInfo = "Closed Market";                             break;
         case eAuction    : m_Info.szInfo = "Auction";                                   break;
         case eInReplay   :
         case eInTrading  : m_Info.szInfo = TimeToString(GetBarTime(), TIME_SECONDS);    break;
         case eInReplay   : m_Info.szInfo = "In Replay";                                 break;
         default          : m_Info.szInfo = "ERROR";
      }
      Draw();
   }


Wir entfernen die durchgestrichene Linie und verschieben sie auf eine höhere Ebene, um denselben Status und dieselbe Funktionalität wie bei der Verwendung des Systems auf dem physischen Markt zu erhalten. Auf diese Weise gleichen wir die Dinge an, d.h. wir werden in beiden Situationen das gleiche Verhalten haben, sowohl während der Wiedergabe/Simulation als auch auf einem Demo-/Echtgeldkonto. Nachdem wir das getan haben, können wir uns die erste Änderung am Code der Funktion GetBarTime ansehen:

const datetime GetBarTime(void)
   {
      datetime dt;
      u_Interprocess info;
                                
      if (m_Info.Status == eInReplay)
      {
         if (!GlobalVariableGet(def_GlobalVariableServerTime, info.u_Value.df_Value)) return ULONG_MAX;
         dt = info.ServerTime;
      }else dt = TimeCurrent();
                                
      if (m_Info.Rate.time <= dt)
         m_Info.Rate.time = iTime(GetInfoTerminal().szSymbol, PERIOD_CURRENT, 0) + PeriodSeconds();

      return m_Info.Rate.time - dt;
   }


Hier liegt der eigentliche Zauber des Analysesystems. In der alten Version dieser Funktion wurde durch diesen Aufruf ein Timer gesetzt. Es ist jedoch nicht für den Einsatz in einem Replay/Simulationssystem geeignet. Deshalb haben wir eine Methode entwickelt, um diesen Aufruf zu umgehen, sodass das System unabhängig vom Verwendungsort über die gleichen Konzepte und Informationen verfügt. Wir fügen also zusätzliche Dinge hinzu, die während des Zeitraums verwendet werden, in dem sich der Code auf dem Chart befindet, dessen Asset in der Wiedergabe verwendet wird. Dies ist nichts Außergewöhnliches. Wir nehmen einfach den gemeldeten und in die globale Variable eingestellten Wert und verwenden ihn so, als käme er vom Handelsserver. An dieser Stelle wird das System tatsächlich umgangen. Aber wir haben immer noch ein Problem, wenn wir uns in einer niedrigen Charting-Zeit befinden, in der die Anzahl der Trades nicht ausreicht, um die Balken korrekt aufzubauen. Dies gilt insbesondere, wenn die verwendeten Daten Intraday-Geschäfte enthalten (was bei einigen Vermögenswerten sehr häufig der Fall ist). Am Ende werden wir einen Misserfolg haben. Dieser Fehler zeigt sich in einer Lücke zwischen den dargestellten und den angezeigten Informationen. Es liegt nicht daran, dass der Dienst nicht mehr aktualisiert wird. Aber der Indikator zeigt keine Informationen an und lässt uns im Dunkeln, ohne zu wissen, was wirklich passiert. Selbst wenn für das Instrument kein Handel stattfindet, kann es bei einigen Devisenhandelspaaren durchaus vorkommen, dass bei der Eröffnung des Balkens nur ein Handel stattfindet. Dies ist in den beigefügten Dateien zu sehen, wo zu Beginn des Tages eine Lücke zwischen dem Punkt, an dem der Balken geöffnet wurde, und dem Punkt, an dem der Handel tatsächlich stattfand, besteht. In dieser Phase sehen wir keine Informationen über das Geschehen. Dies muss irgendwie korrigiert werden, entweder weil der Handel außerhalb des Zeitpunkts stattfindet, an dem das System ihn erwartet, oder weil der verwendete Vermögenswert an einer Auktion teilnimmt. Wir müssen die Dinge wirklich so realitätsnah wie möglich gestalten.

Um diese Probleme zu lösen, müssen wir zwei Änderungen am Code der Klasse vornehmen. Wir werden es jetzt gleich tun. Die erste Änderung ist unten dargestellt:

void Update(void)
   {
      datetime dt;
                                
      switch (m_Info.Status)
      {
         case eCloseMarket:
            m_Info.szInfo = "Closed Market";
            break;
         case eInReplay   :
         case eInTrading  :
            dt = GetBarTime();
            if (dt < ULONG_MAX)
            {
               m_Info.szInfo = TimeToString(dt, TIME_SECONDS);
               break;
            }
         case eAuction    :
            m_Info.szInfo = "Auction";
            break;
         default          :
            m_Info.szInfo = "ERROR";
      }
      Draw();
   }


Der oben gezeigte Code von Update ist trotz seines seltsamen und komplexen Aussehens viel einfacher und bequemer, als es scheint. Wir haben das folgende Szenario. Wenn wir uns in einem Replay-System oder sogar in einem realen Markt befinden und von der Funktion GetBarTime den Wert ULONG_MAX erhalten, dann werden wir eine Meldung über die Auktion anzeigen. Ist der Wert kleiner als dieser ULONG_MAX, was im Normalfall immer der Fall ist, wird der Wert des Timers angezeigt.

Auf der Grundlage dieser Informationen können wir zur Funktion GetBarTime zurückkehren und die Daten generieren, die für die Funktion Update benötigt werden, um die richtigen Daten darzustellen, damit der Nutzer weiß, wie die Dinge laufen. Die neue Funktion GetBarTime ist also im folgenden Code zu sehen.

const datetime GetBarTime(void)
   {
      datetime dt;
      u_Interprocess info;
      int i0 = PeriodSeconds();
                                
      if (m_Info.Status == eInReplay)
      {
         if (!GlobalVariableGet(def_GlobalVariableServerTime, info.u_Value.df_Value)) return ULONG_MAX;
         dt = info.ServerTime;
         if (dt == ULONG_MAX) return ULONG_MAX;
      }else dt = TimeCurrent();
      if (m_Info.Rate.time <= dt)
         m_Info.Rate.time = (datetime)(((ulong) dt / i0) * i0)) + i0;

      return m_Info.Rate.time - dt;
   }


Dieser nette Code löst unser Problem vollständig, zumindest im Moment, da wir den Dienstcode noch ergänzen müssen. Schauen wir uns an, was hier vor sich geht. Im Falle des physischen Marktes, wo wir die Funktion TimeCurrent verwenden, ändert sich nichts. Das ist der Anfang. Aber wenn wir uns im Replay-System befinden, ändern sich die Dinge auf eine sehr eigenartige Weise. Achten Sie daher darauf, wie das System das Geschehen darstellen kann, unabhängig davon, was mit den Wiedergabe- oder Simulationsdaten geschieht. Wenn der Dienst den Wert ULONG_MAX in eine globale Terminalvariable setzt oder wenn diese Variable nicht gefunden wird, sollte die Funktion GetBarTime den Wert ULONG_MAX zurückgeben. Danach wird uns die Methode Update mitteilen, dass wir uns im Auktionsmodus befinden. Es ist nicht möglich, den Timer „weiterzudrehen“. Jetzt kommt der interessante Teil, der unser zweites Problem löst. Anders als bei der Verwendung des Systems mit einem Instrument, das mit einem Handelsserver verbunden ist, bei dem wir immer synchron sind, können die Dinge im Replay-/Simulationsmodus außer Kontrolle geraten und wir können auf einige ziemlich ungewöhnliche Situationen stoßen. Um dieses Problem zu lösen, verwenden wir diese Berechnung, die sowohl für den physischen Markt als auch für das System, das wir entwickeln, funktioniert. Bei dieser Berechnung ersetzen wir die alte Methode, bei der man die Eröffnungszeit des aktuellen Balkens kennen musste. So konnten wir beide Probleme durch Replay/Simulation lösen.

Wir müssen jedoch auf die Klasse C_Replay zurückgreifen, damit das System angeben kann, wann die Anlage versteigert wurde. Dieser Teil ist relativ einfach, da wir nur den Wert ULONG_MAX der globalen Terminalvariable zuweisen müssen. Sehen Sie, dass dies relativ einfach erklärt wurde, denn wir haben andere Probleme vor uns. Aber mal sehen, wie das in der Praxis aussehen wird.


Anpassung der Klasse C_Replay an das Kommunikationssystem

Als Erstes werden wir in der Klasse C_Replay den folgenden Code ändern:

bool ViewReplay(ENUM_TIMEFRAMES arg1)
   {
#define macroError(A) { Print(A); return false; }
      u_Interprocess info;
                                
      if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0)
         macroError("Asset configuration is not complete, it remains to declare the size of the ticket.");
      if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0)
         macroError("Asset configuration is not complete, need to declare the ticket value.");
      if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0)
         macroError("Asset configuration not complete, need to declare the minimum volume.");
      if (m_IdReplay == -1) return false;
      if ((m_IdReplay = ChartFirst()) > 0) do
      {
         if (ChartSymbol(m_IdReplay) == def_SymbolReplay)
         {
            ChartClose(m_IdReplay);
            ChartRedraw();
         }
      }while ((m_IdReplay = ChartNext(m_IdReplay)) > 0);
      Print("Waiting for [Market Replay] indicator permission to start replay ...");
      info.ServerTime = ULONG_MAX;
      info.ServerTime = m_Ticks.Info[m_ReplayCount].time;
      CreateGlobalVariable(def_GlobalVariableServerTime, info.u_Value.df_Value);
      info.u_Value.IdGraphic = m_IdReplay = ChartOpen(def_SymbolReplay, arg1);
      ChartApplyTemplate(m_IdReplay, "Market Replay.tpl");
      CreateGlobalVariable(def_GlobalVariableIdGraphics, info.u_Value.df_Value);
      while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(750);

      return ((!_StopFlag) && (ChartSymbol(m_IdReplay) != ""));
#undef macroError
   }


Beachten Sie, dass wir eine Zeile entfernt und durch eine andere ersetzt haben. Mit dieser Änderung wird eine Meldung über die Auktion angezeigt, wenn der Replay-/Simulationsdienst gestartet wird und der Vermögenswert im Chart läuft. Es zeigt an, dass das System wie gewünscht kommuniziert. Das war der einfache Teil, jetzt kommen wir zum schwierigen Teil. Es kann vorkommen, dass der Dienst bereits gestartet wurde und wir herausfinden müssen, ob der Vermögenswert versteigert wurde oder nicht. Ein Vermögenswert kann aus einer Reihe von Gründen versteigert werden, die alle völlig unerwartet kommen können. Es hätte eine viel größere Abweichung geben können, oder das Auftragsbuch hätte zusammenbrechen und vollständig gelöscht werden können, und so weiter. Der Grund, warum der Vermögenswert versteigert wurde, spielt keine Rolle. Wichtig ist, was passiert, wenn er versteigert wird. Es gibt spezifische Regeln, die bestimmen, wie die Auktion durchgeführt wird, aber die wichtigste Regel für uns ist folgende: Wie lange muss ein Vermögenswert mindestens versteigert werden? Das ist genau der Punkt. Wenn diese Zeit weniger als 1 Minute beträgt, kann der Vermögenswert auf dem realen Markt mit Sicherheit in den Handel ein- und aussteigen, ohne dass das Replay-/Simulationssystem dies feststellen kann. Oder besser gesagt, es wird nicht möglich sein, diese Veränderung zu bemerken, da sie immer innerhalb der kürzesten Zeit stattfindet, die zur Definition der Zeit eines Balkens verwendet werden kann.

Der einfachste Mechanismus, der im Replay-/Simulationssystem verwendet werden kann, um festzustellen, dass ein Vermögenswert versteigert wurde, ist die Überprüfung der Zeitdifferenz zwischen einem Balken und einem anderen. Übersteigt diese Differenz 1 Minute, müssen wir den Nutzer darüber informieren, dass die Anlage gerade in den Auktionsprozess eingetreten ist, und sie somit für den gesamten Zeitraum aussetzen. Diese Art von Mechanismus wird in Zukunft nützlich sein. Vorerst werden wir uns nur mit der Entwicklung und Umsetzung befassen und andere Themen auf einen anderen Zeitpunkt verschieben. Lassen Sie uns sehen, wie wir dieses Problem lösen können. Dies ist im folgenden Code zu sehen:

bool LoopEventOnTime(const bool bViewBuider)
   {
      u_Interprocess Info;
      int iPos, iTest, iCount;
                                
      if (!m_Infos.bInit) ViewInfos();
      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);
      Info.ServerTime = m_Ticks.Info[m_ReplayCount].time;
      GlobalVariableSet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
      iPos = iCount = 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;
            iCount++;
            if (iCount > 4)
            {
               iCount = 0;
               GlobalVariableGet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
               if ((m_Ticks.Info[m_ReplayCount].time - m_Ticks.Info[m_ReplayCount - 1].time) > 60) Info.ServerTime = ULONG_MAX; else
               {
                  Info.ServerTime += 1;
                  Info.ServerTime = ((Info.ServerTime + 1) < m_Ticks.Info[m_ReplayCount].time ? Info.ServerTime : m_Ticks.Info[m_ReplayCount].time);
               };
               GlobalVariableSet(def_GlobalVariableServerTime, Info.u_Value.df_Value);
            }
         }
      }                               
      return (m_ReplayCount == m_Ticks.nTicks);
   }                               


Beachten Sie, dass wir den Zeitunterschied zwischen einem Tick und dem nächsten überprüft haben. Wenn diese Differenz größer als 60 Sekunden ist, d. h. größer als die kürzeste Zeit für die Erstellung eines Balkens, melden wir, dass es sich um einen „Auktionsaufruf“ handelt, und das gesamte Replay/Simulationssystem zeigt die Auktion an. Wenn die Zeitdifferenz kleiner oder gleich 60 Sekunden ist, bedeutet dies, dass das Wertpapier noch aktiv ist und der Timer wie in diesem Artikel beschrieben gestartet werden sollte. Damit haben wir die aktuelle Phase abgeschlossen.


Schlussfolgerung

Heute haben wir uns angeschaut, wie man einen Timer, der das Erscheinen eines neuen Balkens anzeigt, auf ganz praktische, robuste, zuverlässige und effektive Weise hinzufügen kann. Wir haben einige Momente erlebt, in denen es unmöglich schien, aber in Wirklichkeit ist nichts unmöglich. Vielleicht wird es etwas schwieriger zu überwinden sein, aber wir können immer einen geeigneten Weg finden, um Probleme zu lösen. Hier geht es vor allem darum, zu zeigen, dass man immer versuchen sollte, eine Lösung zu finden, die in allen Situationen, in denen sie benötigt wird, anwendbar ist. Es macht keinen Sinn, etwas für den Nutzer oder sogar für uns selbst zu programmieren (um die Möglichkeiten zu erlernen), wenn die Anwendung eines solchen Modells die Verwendung von völlig anderen Werkzeugen erfordert. Das ist absolut demotivierend. Denken Sie deshalb immer daran: Wenn man etwas tun muss, dann muss es richtig gemacht werden, und nicht so, dass in der Theorie alles funktioniert, aber in der Praxis ein völlig anderes Verhalten zeigt.

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

Beigefügte Dateien |
Files_-_BOLSA.zip (1358.24 KB)
Files_-_FOREX.zip (3743.96 KB)
Files_-_FUTUROS.zip (11397.51 KB)
Neuronale Netze leicht gemacht (Teil 61): Optimismusproblem beim Offline-Verstärkungslernen Neuronale Netze leicht gemacht (Teil 61): Optimismusproblem beim Offline-Verstärkungslernen
Während des Offline-Lernens optimieren wir die Strategie des Agenten auf der Grundlage der Trainingsdaten. Die daraus resultierende Strategie gibt dem Agenten Vertrauen in sein Handeln. Ein solcher Optimismus ist jedoch nicht immer gerechtfertigt und kann zu erhöhten Risiken während des Modellbetriebs führen. Heute werden wir uns mit einer der Methoden zur Verringerung dieser Risiken befassen.
Entwicklung eines Replay System (Teil 30): Expert Advisor Projekt — Die Klasse C_Mouse (IV) Entwicklung eines Replay System (Teil 30): Expert Advisor Projekt — Die Klasse C_Mouse (IV)
Heute werden wir eine Technik lernen, die uns in verschiedenen Phasen unseres Berufslebens als Programmierer sehr helfen kann. Oft ist es nicht die Plattform selbst, die begrenzt ist, sondern das Wissen der Person, die über die Grenzen spricht. In diesem Artikel erfahren Sie, dass Sie mit gesundem Menschenverstand und Kreativität die MetaTrader 5-Plattform viel interessanter und vielseitiger gestalten können, ohne auf verrückte Programme oder ähnliches zurückgreifen zu müssen, und einfachen, aber sicheren und zuverlässigen Code erstellen können. Wir werden unsere Kreativität nutzen, um bestehenden Code zu ändern, ohne eine einzige Zeile des Quellcodes zu löschen oder hinzuzufügen.
Entwicklung eines Replay System (Teil 32): Auftragssystem (I) Entwicklung eines Replay System (Teil 32): Auftragssystem (I)
Von allen Dingen, die wir bisher entwickelt haben, ist dieses System, wie Sie wahrscheinlich bemerken und letztendlich zustimmen werden, das komplexeste. Nun müssen wir etwas sehr Einfaches tun: unser System soll den Betrieb eines Handelsservers simulieren. Die Notwendigkeit, die Funktionsweise des Handelsservers genau zu implementieren, scheint eine Selbstverständlichkeit zu sein. Zumindest in Worten. Aber wir müssen dies so tun, dass alles nahtlos und transparent für den Nutzer des Wiedergabe-/Simulationssystems ist.
Entwicklung eines Replay System (Teil 29): Expert Advisor Projekt — Die Klasse C_Mouse (II) Entwicklung eines Replay System (Teil 29): Expert Advisor Projekt — Die Klasse C_Mouse (II)
Nachdem wir die Klasse C_Mouse verbessert haben, können wir uns auf die Erstellung einer Klasse konzentrieren, die einen völlig neuen Rahmen für unsere Analyse schaffen soll. Wir werden weder Vererbung noch Polymorphismus verwenden, um diese neue Klasse zu erstellen. Stattdessen werden wir die Preislinie ändern, oder besser gesagt, neue Objekte hinzufügen. Genau das werden wir in diesem Artikel tun. In der nächsten Ausgabe werden wir uns ansehen, wie man die Analyse ändern kann. All dies geschieht, ohne den Code der Klasse C_Mouse zu ändern. Nun, eigentlich wäre es einfacher, dies durch Vererbung oder Polymorphismus zu erreichen. Es gibt jedoch auch andere Methoden, um das gleiche Ergebnis zu erzielen.