English Русский Español 日本語 Português
preview
Entwicklung eines Replay Systems — Marktsimulation (Teil 24): FOREX (V)

Entwicklung eines Replay Systems — Marktsimulation (Teil 24): FOREX (V)

MetaTrader 5Tester | 7 März 2024, 10:41
140 0
Daniel Jose
Daniel Jose

Einführung

Im vorherigen Artikel „Entwicklung eines Replay-Systems — Marktsimulation (Teil 23): FOREX (IV)“ haben wir uns mit der Implementierung der teilweisen Blockierung des Simulationssystems beschäftigt. Die Sperrung war notwendig, weil das System Schwierigkeiten hatte, mit extrem niedrigen Transaktionsvolumen umzugehen. Diese Einschränkung wurde deutlich, als versucht wurde, Simulationen auf der Grundlage des Typs der Darstellung der Last-Preise durchzuführen, bei denen das System beim Versuch, die Simulation zu erstellen, absturzgefährdet war. Dieses Problem machte sich vor allem in den Momenten bemerkbar, in denen das durch den 1-Minuten-Balken repräsentierte Handelsvolumen unzureichend war. Um dieses Problem zu lösen, werden wir uns heute ansehen, wie man die Implementierung anpassen und die Prinzipien befolgen kann, die früher in der Simulation auf der Grundlage von Bid-Plot verwendet wurden. Dieser Ansatz ist auf dem Devisenmarkt weit verbreitet, sodass dies bereits der fünfte Artikel zu diesem Thema ist. In diesem Fall werden wir jedoch nicht speziell auf Währungen eingehen, da unser Ziel darin besteht, das Börsensimulationssystem zu verbessern.


Beginnen wir mit der Umsetzung der Änderungen

Der erste Schritt besteht darin, eine private Struktur in unsere Klasse einzuführen. Der Grund dafür ist, dass wir Daten haben, die sowohl für die Last- als auch für die Bid-Simulation gelten. Diese gemeinsamen Elemente, die im Wesentlichen Werte sind, werden in einer einzigen Struktur zusammengefasst. Wir definieren also die folgende Struktur:

struct st00
{
   bool    bHigh, bLow;
   int     iMax;
}m_Marks;

Obwohl dies eine einfache Struktur zu sein scheint, ist sie robust genug, um unseren Code verbessern zu können. Indem wir alle gemeinsamen Werte an einem Ort zusammenfassen, steigern wir die Effizienz unserer Arbeit erheblich.

Nach dieser Vorbereitung können wir mit der ersten wirklichen Änderung des Codes beginnen.

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      m_Marks.iMax = (int) rate.tick_volume - 1;
      m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high);
      m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low);
      Simulation_Time(rate, tick);
      if (m_IsPriceBID) Simulation_BID(rate, tick);
      else Simulation_LAST(rate, tick);
      else return -1;
      CorretTime(tick);

      return m_Marks.iMax;
   }

Heute werden wir eine Einschränkung aufheben, die bisher Simulationen auf der Grundlage des letzten Kurses verhindert hat, und einen neuen Einstiegspunkt speziell für diese Art von Simulationen einführen. Der gesamte Funktionsmechanismus wird auf den Prinzipien des Devisenmarktes beruhen. Der Hauptunterschied in diesem Verfahren ist die Trennung von Bid- und Last-Simulationen. Es ist jedoch wichtig zu beachten, dass die Methode zur Randomisierung der Zeit und zur Anpassung an die Klasse C_Replay in beiden Simulationen identisch bleibt. Diese Konsistenz ist gut, denn wenn wir einen der Modi ändern, profitiert auch der andere davon, insbesondere wenn es um die Verwaltung der Zeit zwischen den Ticks geht. Natürlich wirken sich die hier besprochenen Änderungen auch auf die Simulation auf der Grundlage der Bid-Darstellungsart aus. Diese Änderungen sind recht einfach zu verstehen, sodass ich nicht auf die Einzelheiten eingehen werde.

Kehren wir zu unserem Zielcode zurück. Sobald wir einen Aufruf der Last-basierten Simulationsfunktion hinzufügen, können wir den ersten Punkt dieses Aufrufs sehen. Im Folgenden wird die interne Struktur dieser Funktion dargestellt:

inline void Simulation_LAST(const MqlRates &rate, MqlTick &tick[])
   {
      if (CheckViability_LAST(rate))
      {
      }else
      {
      }
      DistributeVolumeReal(rate, tick);
   }

In diesem Zusammenhang werden wir zwei wichtige Schritte bei der Arbeit mit der Darstellung der Last-Preise durchführen. Der erste Schritt besteht darin, die Möglichkeit zu prüfen, ein Random-Walk-System für die Simulation zu verwenden, wie in früheren Artikeln erörtert. Für diejenigen, die sich mit diesem Thema noch nicht beschäftigt haben, empfehle ich die Lektüre des Artikels „Entwicklung eines Replay-Systems — Marktsimulation (Teil 15): Geburt des SIMULATORS (V) - RANDOM WALK“. Der zweite Schritt besteht darin, das Handelsvolumen auf mögliche Ticks zu verteilen. Diese Schritte sind wichtig, wenn Sie mit der Simulation auf der Grundlage der Darstellung der Last-Preise arbeiten.

Bevor wir die Struktur im Detail beschreiben, wollen wir uns zwei kritische Funktionen für die Last-basierte Simulation ansehen. Die erste Funktion ist unten dargestellt:

inline bool CheckViability_LAST(const MqlRates &rate)
   {
#define macro_AdjustSafetyFator(A) (A + (A * 1.4));
                                
      double  v0, v1, v2;
                                
      v0 = macro_AdjustSafetyFator(rate.high - rate.low);
      v1 = (rate.open - rate.low);
      v2 = (rate.high - rate.open);
      v0 += macro_AdjustSafetyFator(v1 > v2 ? v1 : v2);
      v1 = (rate.close - rate.low);
      v2 = (rate.high - rate.close);
      v0 += macro_AdjustSafetyFator(v1 > v2 ? v1 : v2);
      return ((int)(v0 / m_TickSize) < rate.tick_volume);
                                
#undef macro_AdjustSafetyFator
   }

Diese Funktion prüft, ob es möglich ist, einen Random Walk innerhalb der verfügbaren Grenzen zu erzeugen. Wie wird das gemacht? Die Methode ermittelt die Anzahl der aktuell verfügbaren Ticks. Diese Information wird von dem Balken, mit dem wir arbeiten werden, zusammen mit der Anzahl der Ticks, die wir abdecken müssen, bereitgestellt, der dann innerhalb der Funktion berechnet wird.

Beachten Sie, dass wir diesen Wert nicht direkt verwenden, um die zu bedeckende Fläche zu bestimmen. Der Grund dafür ist, dass bei einem solchen direkten Ansatz der resultierende Random Walk künstlich aussehen würde, mit allzu vorhersehbaren Bewegungen. Um dieses Problem zu entschärfen, werden wir die Berechnungen anpassen. Diese Einstellung, die mit Hilfe eines Makros implementiert wurde, definiert einen 30 % größeren Bereich, der als Sicherheitsfaktor für die korrekte Generierung des Random Walk dient. Ein weiterer wichtiger Aspekt ist die Notwendigkeit, bei den Berechnungen immer den größtmöglichen Abstand zu berücksichtigen, da die Randomisierung eine solche Erweiterung erfordern kann. Diese Möglichkeit wurde also bereits bei der Berechnung berücksichtigt.

Das Endergebnis gibt Aufschluss darüber, ob eine Random-Walk-Methode angebracht ist oder ob eine andere, direktere Randomisierungsmethode verwendet werden sollte. Diese Entscheidung wird jedoch vom aufrufenden Verfahren getroffen, nicht hier.

Im Folgenden wird die zweite Funktion im Detail beschrieben:

inline void DistributeVolumeReal(const MqlRates &rate, MqlTick &tick[])
   {
      for (int c0 = 0; c0 <= m_Marks.iMax; c0++)
         tick[c0].volume_real = 1.0;
      for (int c0 = (int)(rate.real_volume - rate.tick_volume); c0 > 0; c0--)
         tick[RandomLimit(0, m_Marks.iMax)].volume_real += 1.0;                                  
   }

Hier ist das Ziel, das Gesamtvolumen der Trades auf einem 1-Minuten-Balken zufällig zu verteilen. Die erste Schleife führt die Hauptverteilung durch, um sicherzustellen, dass jeder Tick ein minimales Anfangsvolumen erhält. Die zweite Schleife ist für die zufällige Verteilung des verbleibenden Volumens verantwortlich, um zu verhindern, dass es sich auf einen Tick konzentriert. Diese Möglichkeit besteht zwar immer noch, wird aber durch die gewählte Verteilungsmethode stark eingeschränkt.

Diese Funktionen waren bereits Teil der ursprünglichen Implementierung und wurden in den Artikeln über die Implementierung des Random Walk besprochen. Dieses Mal verfolgen wir jedoch einen stärker modularen Ansatz, um die Wiederverwendbarkeit des entwickelten Codes zu maximieren.

Nach weiteren Überlegungen haben wir Elemente identifiziert, die auch bei den letzten Simulationen noch vorhanden sind. Dazu gehören die Definition von Einstiegs- und Ausstiegspunkten sowie die Möglichkeit, Endpunkte zu definieren. In diesem Zusammenhang bemühen wir uns, bereits entwickelte Codes wiederzuverwenden. Dazu müssen wir den Code ändern, der im vorherigen Artikel vorgestellt wurde. Die Änderung ist wie folgt:

inline void Mount_BID(const int iPos, const double price, const int spread, MqlTick &tick[])
inline void MountPrice(const int iPos, const double price, const int spread, MqlTick &tick[])
   {
      if (m_IsPriceBID)
      {
         tick[iPos].bid = price;
         tick[iPos].ask = NormalizeDouble(price + (m_TickSize * spread), m_NDigits);
      }else
         tick[iPos].last = NormalizeDouble(price, m_NDigits);
   }

Wir beginnen mit dem Ersetzen des alten Funktionsnamens durch den neuen und fügen dann einen internen Test ein, um festzustellen, ob die Simulation auf der Bid- oder dem Last-Darstellung basieren wird. So kann dieselbe Funktion angepasst werden, um Ticks auf der Grundlage von Bid- und Last-Werten gemäß den in der 1-Minuten-Bar-Datei beobachteten Daten zu erzeugen.

Diese Änderung erfordert auch zwei Anpassungen. Unser Ziel ist es, die Bid- und Last-Simulation auf vereinfachte Weise zu integrieren, sodass nur die wirklich einzigartigen Aspekte der beiden Methoden behandelt werden. Die übrigen Punkte werden auf allgemeiner Basis behandelt. Nachfolgend finden Sie Änderungen im Code der Simulationsklasse:

inline void Simulation_BID(const MqlRates &rate, MqlTick &tick[])
   {
      Mount_BID(0, rate.open, rate.spread, tick);     
      for (int c0 = 1; c0 < m_Marks.iMax; c0++)
      {
         Mount_BID(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), (rate.spread + RandomLimit((int)(rate.spread | (m_Marks.iMax & 0xF)), 0)), tick);
         MountPrice(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), (rate.spread + RandomLimit((int)(rate.spread | (m_Marks.iMax & 0xF)), 0)), tick);
         m_Marks.bHigh = (rate.high == tick[c0].bid) || m_Marks.bHigh;
         m_Marks.bLow = (rate.low == tick[c0].bid) || m_Marks.bLow;
      }
      if (!m_Marks.bLow) Mount_BID(Unique(rate.high, tick), rate.low, rate.spread, tick);
      if (!m_Marks.bHigh) Mount_BID(Unique(rate.low, tick), rate.high, rate.spread, tick);
      Mount_BID(m_Marks.iMax, rate.close, rate.spread, tick);
   }

Die durchgestrichenen Zeilen werden entfernt, um sicherzustellen, dass der Code weiterhin korrekt funktioniert. Für die Darstellung von Last benötigen wir diese durchgestrichenen Teile jedoch in einer Form, die für das gesamte Simulationssystem einheitlich ist. Daher wurde dieser gemeinsame Code in die folgende Funktion übertragen:

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      m_Marks.iMax = (int) rate.tick_volume - 1;
      m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high);
      m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low);
      Simulation_Time(rate, tick);
      MountPrice(0, rate.open, rate.spread, tick);    
      if (m_IsPriceBID) Simulation_BID(rate, tick);
      else Simulation_LAST(rate, tick);
      if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick);
      if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick);
      MountPrice(m_Marks.iMax, rate.close, rate.spread, tick);
      CorretTime(tick);

      return m_Marks.iMax;
   }

Und nun ist der entscheidende Moment gekommen. Ohne wesentliche Änderungen, nur durch intelligente Wiederverwendung von Code, konnten wir beide Simulationen integrieren. Somit haben wir nun eine Simulation mit den Typen Bid- und Last-Darstellung. Wir fügen automatisch Eingabe-, Ausgabe- und Grenzwerte ein, wenn sie nicht zuvor in der randomisierten Simulation angegeben wurden. Mit dieser strategischen Anpassung haben wir den Umfang unseres Simulationssystems erheblich erweitert. Darüber hinaus hat sich die Komplexität des Codes nicht erhöht und der Code selbst ist nicht wesentlich gewachsen. Wenn Sie den bisher vorgestellten Code verwenden, erhalten Sie eine gute Leistung für die Bid-basierte Simulation. Und Sie können mindestens einen Balken im Chart mit der Darstellungsart Last wiedergeben. Nun, der Mindestwert wird falsch sein, da er Null sein wird, obwohl er mit dem richtigen Wert definiert ist. Das liegt daran, dass bei nicht initialisierten Ticks, bei denen nur die Zeit definiert ist, alle Last-Werte gleich Null sind. Das wäre kein Problem, wenn wir das gehandelte Volumen nicht bereits verteilt hätten. Kehren wir also zu unserer Funktion zurück, die die Werte des letzten Kurses in jeden Tick einträgt. Wir müssen die Simulation mit korrekten Daten versorgen.

Unsere Funktion zur Simulation des letzten Preises bleibt unverändert. Wenn man sich jedoch den Code der Bid-Simulation ansieht, kommt einem ein Gedanke: Könnten wir nicht denselben Code verwenden, um den letzten Preis zu simulieren, insbesondere wenn die Anzahl der verfügbaren Ticks nicht ausreicht, um einen vollständigen Random Walk durchzuführen? Ich hatte auch diese Frage. Nach sorgfältiger Analyse kamen wir zu dem Schluss, dass nur geringfügige Änderungen an der Bid-Modellierungsfunktion erforderlich waren. Um jedoch Verwirrung zu vermeiden, wenn wir in der Zukunft Änderungen am Code vornehmen müssen, müssen wir diese Änderungen jetzt sorgfältig planen. Das Bid-Simulationsverfahren wird durch die soeben erwähnte Funktion gestartet. Da das Konzept der letzten Simulation ähnlich ist, können wir nach einer Möglichkeit suchen, den vorherigen Aufruf beizubehalten. Daher passen wir die Bid-Simulationsfunktion so an, dass sie auch die Last-Simulation in Situationen abdecken kann, in denen kein Random Walk angewendet wird.

Manch einer mag diesen Ansatz in Frage stellen, aber im Folgenden wird gezeigt, wie der Simulationscode für die Bid-Darstellung angepasst wurde, um die Simulation für die Last-basierte Darstellung einzubeziehen.

inline void Simulation_BID(const MqlRates &rate, MqlTick &tick[])
inline void Random_Price(const MqlRates &rate, MqlTick &tick[])
   {
      for (int c0 = 1; c0 < m_Marks.iMax; c0++)
      {
         MountPrice(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), (rate.spread + RandomLimit((int)(rate.spread | (m_Marks.iMax & 0xF)), 0)), tick);
         m_Marks.bHigh = (rate.high == (m_IsPriceBID ? tick[c0].bid : tick[c0].last)) || m_Marks.bHigh;
         m_Marks.bLow = (rate.low == (m_IsPriceBID ? tick[c0].bid : tick[c0].last)) || m_Marks.bLow;
         m_Marks.bHigh = (rate.high == tick[c0].bid) || m_Marks.bHigh;
         m_Marks.bLow = (rate.low == tick[c0].bid) || m_Marks.bLow;
      }
   }

Um Verwirrung während der Laufzeit zu vermeiden, habe ich beschlossen, die Funktion umzubenennen. Das ist ein kleiner Preis für den Vorteil, einen universelleren Code zu erhalten. Die Idee hinter dieser Anpassung ist die folgende. Diese beiden Code-Elemente verdienen besondere Aufmerksamkeit. Der ternäre Operator ist ein wertvolles Erbe der Sprache C, das viele nützliche Funktionen bietet, auch wenn er von manchen als obskur angesehen wird. In diesen Segmenten wird die Art des Plotts überprüft, um den Preis entsprechend anzupassen. Bitte beachten Sie, dass die Randomisierung unabhängig von der Art der Aufzeichnung auf die gleiche Weise erfolgt. So konnten wir diese beiden Methoden kombinieren und ein effektives Simulationssystem für Bid und Last schaffen.

Nachdem die Änderungen vorgenommen wurden, ähnelte die Simulation sehr stark dem, was in dem Artikel „Entwicklung eines Wiedergabesystems — Marktsimulation (Teil 13) besprochen wurde: Geburt des SIMULATOR (III)“. Wir haben jedoch noch keine Random-Walk-Simulation in das System implementiert. Dies liegt daran, dass der Code derzeit entsprechend der vorgestellten Option angepasst wurde:

inline void Simulation_LAST(const MqlRates &rate, MqlTick &tick[])
   {
      if (CheckViability_LAST(rate))
      {
      }else Random_Price(rate, tick);
      DistributeVolumeReal(rate, tick);
   }

Daher modellieren wir noch keine typischen Szenarien, in denen die Verwendung eines Random Walk angemessen wäre. Unser Ziel ist es jedoch, dass die Bid-Simulation unter bestimmten Bedingungen einen Random Walk verwendet, so wie es auch bei der Last-Preis-Simulation der Fall ist. Und die Frage ist: Ist das möglich? Können wir diesen Ansatz noch interessanter und robuster gestalten, sodass auch Märkte wie der Devisenmarkt von der Random-Walk-Methode zur Simulation von Preisbewegungen profitieren können? Die Antwort lautet: Ja, das ist möglich. Bevor ein Random Walk speziell zur Konstruktion des letzten Preises eingesetzt werden kann, müssen einige Änderungen vorgenommen werden.

inline bool CheckViability(const MqlRates &rate)
   {
#define macro_AdjustSafetyFator(A) (int)(A + ceil(A * 1.7))
                                
      int i0, i1, i2;
                                
      i0 = macro_AdjustSafetyFator((rate.high - rate.low) / m_TickSize);
      i1 = (int)((rate.open - rate.low) / m_TickSize);
      i2 = (int)((rate.high - rate.open) / m_TickSize);
      i0 += macro_AdjustSafetyFator(i1 > i2 ? i1 : i2);
      i0 += macro_AdjustSafetyFator((i1 > i2 ? (rate.high - rate.close) : (rate.close - rate.low) / m_TickSize));

      return (i0 < rate.tick_volume);
                                
#undef macro_AdjustSafetyFator
   }

Die obige Funktion ist eine Erweiterung der Funktion, die zuvor zur Bewertung der Durchführbarkeit der Erzeugung einer Zufallsbewegung verwendet wurde. Aufgrund technischer Details und der Einführung fortgeschrittener Sicherheitsfaktoren haben wir diesen Ansatz verfeinert, um nicht über die Fähigkeit zur Ausführung der Bewegung hinwegzutäuschen. Diese Änderung ist gerechtfertigt, da sich das Prüfverfahren nicht mehr nur auf die Modellierung des Typs „Last-based“ beschränkt. Es wird auch verwendet, um die Anwendbarkeit von Random Walk bei der Simulation von Bid zu bewerten. Auf den ersten Blick scheint dies einfach zu sein, aber es erfordert besondere Vorsichtsmaßnahmen. Zur besseren Veranschaulichung dieses Punktes sehen wir uns Abbildung 01 an.

Abbildung 01

Abbildung 01 - Berechnung des längsten möglichen Weges

Die Funktion tut genau das - sie berechnet den längsten möglichen Weg zur Erstellung eines 1-Minuten-Balkens. Diese Methodik wurde angepasst, um den Prozess zu straffen, sodass auch die Art der Angebote davon profitieren kann. Beachten Sie, dass sich der Sicherheitsfaktor von 1,4 auf 1,7 erhöht hat, was es für einige Vermögenswerte sehr schwierig macht, Random Walk zu nutzen. Die Berechnung beginnt mit der Bestimmung des Abstands zwischen dem Eröffnungskurs des Balkens und seinen Extremen. Mit dieser Information verwenden wir den größeren Wert im ersten Schritt der Berechnung. Ein anderer Wert wird verwendet, um den Balken zu verschieben, wie in Abbildung 01 dargestellt. Zum Schluss führen wir eine einfache Berechnung durch, um zu prüfen, ob es möglich ist, den Random Walk zu verwenden oder nicht.

Sie werden vielleicht denken, dass wir zusätzliche Änderungen am Klassencode vornehmen müssen. Nun, wir werden Änderungen vornehmen. Diese Änderung wird jedoch in einer Weise erfolgen, die eine harmonischere Integration gewährleistet.

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      m_Marks.iMax = (int) rate.tick_volume - 1;
      m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high);
      m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low);
      Simulation_Time(rate, tick);
      MountPrice(0, rate.open, rate.spread, tick);
      if (CheckViability(rate))
      {
      }else Random_Price(rate, tick);
      if (!m_IsPriceBID) DistributeVolumeReal(rate, tick);
      if (m_IsPriceBID) Random_Price(rate, tick);
      else Simulation_LAST(rate, tick);
      if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick);
      if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick);
      MountPrice(m_Marks.iMax, rate.close, rate.spread, tick);
      CorretTime(tick);

      return m_Marks.iMax;
   }

Wir löschen die durchgestrichenen Teile und fügen die grün markierten hinzu. Es gibt jetzt Fälle, in denen ein 1-Minuten-Balken auf Märkten wie FOREX ähnliche Ticks wie auf dem Aktienmarkt erzeugen kann und umgekehrt. Dadurch kann der Simulator ein breites Spektrum an Marktbewegungen abdecken, unabhängig vom Tickvolumen. Es ist jedoch wichtig zu beachten, dass der Code, der für die Erzeugung des Random Walk verantwortlich ist, noch nicht in die obige Funktion aufgenommen wurde. Schauen wir uns also an, wie dieser Code für beide Arten der Darstellung implementiert werden würde, wobei wir uns speziell auf diese Funktionalität konzentrieren.


Implementierung eines Random Walk für Bid- und Last-Kurse

Wie bereits erwähnt, wurde die Klasse C_Simulation entwickelt, um eine einheitliche Handhabung zwischen den Simulationen von Bid- und Last-Darstellung zu gewährleisten. Ziel war es, eine möglichst genaue Simulation zu erstellen. Wir haben einen kritischen Punkt erreicht, an dem der nächste Schritt darin besteht, ein Verfahren zu implementieren, das mit einem Minimum an Code auskommt, ohne die Komplexität zu erhöhen. Diese Anpassung basiert auf dem, was wir in dem Artikel „Entwicklung eines Wiedergabesystems — Marktsimulation (Teil 15) besprochen haben: Geburt des SIMULATORS (V) - RANDOM WALK“. Ich werde also nicht im Detail auf die ursprüngliche Implementierung des Random Walk eingehen und auch nicht darauf, wie die Idee dazu entstand. Für diejenigen, die an weiteren Informationen interessiert sind, empfehle ich, den genannten Artikel zu lesen. Hier werden wir uns darauf konzentrieren, diesen Code an einen neuen Kontext anzupassen.

inline int RandomWalk(int In, int Out, const double Open, const double Close, double High, double Low, const int Spread, MqlTick &tick[], int iMode)
   {
      double vStep, vNext, price, vH = High, vL = Low;
      char i0 = 0;
                                
      vNext = vStep = (Out - In) / ((High - Low) / m_TickSize);
      for (int c0 = In, c1 = 0, c2 = 0; c0 <= Out; c0++, c1++)
      {
         price = (m_IsPriceBID ? tick[c0 - 1].bid : tick[c0 - 1].last) + (m_TickSize * ((rand() & 1) == 1 ? -1 : 1));
         price = (price > High ? price - m_TickSize : (price < Low ? price + m_TickSize : price));
         MountPrice(c0, price, (Spread + RandomLimit((int)(Spread | (m_Marks.iMax & 0xF)), 0)), tick);
         switch (iMode)
         {
            case 0:
               if (price == Close) return c0; else break;
            case 1:
               i0 |= (price == High ? 0x01 : 0);
               i0 |= (price == Low ? 0x02 : 0);
               vH = (i0 == 3 ? High : vH);
               vL = (i0 ==3 ? Low : vL);
               break;
            default: break;
         }
         if (((int)floor(vNext)) >= c1) continue;
         if ((++c2) <= 3) continue;
         vNext += vStep;
         if (iMode != 2)
         {
            if (Close > vL) vL = (i0 == 3 ? vL : vL + m_TickSize); else vH = (i0 == 3 ? vH : vH - m_TickSize);
         }else
         {
            vL = (((c2 & 1) == 1) ? (Close > vL ? vL + m_TickSize : vL) : (Close < vH ? vL : vL + m_TickSize));
            vH = (((c2 & 1) == 1) ? (Close > vL ? vH : vH - m_TickSize) : (Close < vH ? vH - m_TickSize : vH));
         }
      }
                                
      return Out;
   }

Die vorgenommenen Änderungen zielen darauf ab, die Struktur des Codes zu vereinfachen, während seine Funktionsweise unverändert bleibt. Von besonderem Interesse ist dabei, wie der vorherige Wert gelesen wird, um einen neuen Wert zu erstellen, der sich an die Art der Darstellung anpasst. Diese Flexibilität ist sehr wichtig für die Funktionalität des Simulators. Zur Ermittlung der Werte verwenden wir eine bereits bekannte und in diesem Artikel vorgestellte Funktion, die die Entwicklung des Verfahrens erleichtert. Wie wir bereits gesagt haben, werden wir die Merkmale der Funktion nicht im Detail beschreiben, da sie in einem anderen Artikel behandelt wurde.

Schauen wir uns nun an, wie die endgültige Funktion aufgebaut ist. Dies ist unser erster Versuch, diese Umsetzungsphase abzuschließen und gleichzeitig zu testen, ob die Funktion zur Erzeugung von Simulationsaufrufen auf der Grundlage von Balkendaten das erwartete Ziel erreicht. Ziel ist es, die Bid- und Last-basierte Simulation effektiv abzudecken. Nachstehend finden Sie eine detaillierte Beschreibung des Funktionscodes:

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      int     i0, i1;
      bool    b0 = ((rand() & 1) == 1);
                                
      m_Marks.iMax = (int) rate.tick_volume - 1;
      m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high);
      m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low);
      Simulation_Time(rate, tick);
      MountPrice(0, rate.open, rate.spread, tick);
      if (CheckViability(rate))
      {
         i0 = (int)(MathMin(m_Marks.iMax / 3.0, m_Marks.iMax * 0.2));
         i1 = m_Marks.iMax - i0;
         i0 = RandomWalk(1, i0, rate.open, (b0 ? rate.high : rate.low), rate.high, rate.low, rate.spread, tick, 0);
         RandomWalk(i0, i1, (m_IsPriceBID ? tick[i0].bid : tick[i0].last), (b0 ? rate.low : rate.high), rate.high, rate.low, rate.spread, tick, 1);
         RandomWalk(i1, m_Marks.iMax, (m_IsPriceBID ? tick[i1].bid : tick[i1].last), rate.close, rate.high, rate.low, rate.spread, tick, 2);
	 m_Marks.bLow = m_Marks.bHigh = true;
      }else Random_Price(rate, tick);
      if (!m_IsPriceBID) DistributeVolumeReal(rate, tick);
      if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick);
      if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick);
      MountPrice(m_Marks.iMax, rate.close, rate.spread, tick);
      CorretTime(tick);

      return m_Marks.iMax;
   }

Dieser Teil ist für die Simulation eines Random Walk über die Balken verantwortlich. Dieses Verfahren wurde zwar auch in der Vergangenheit verwendet, jedoch direkt in den Generierungscode integriert. Sie wurde nun an eine Stelle verschoben, die sie leichter verständlich und analysierbar macht, sodass sie auch für unerfahrene Programmierer zugänglich ist. Wenn Sie genauer hinsehen, können Sie erkennen, dass das Simulationssystem prüft, ob es möglich ist, einen Random Walk zu verwenden. Wenn es möglich ist, wird das System es verwenden, wenn nicht, wird es auf eine andere Methode zurückgreifen. So garantieren wir die Generierung von Preisbewegungen oder Preisverschiebungen unter allen Umständen. Das gilt sowohl für den Devisenmarkt als auch für den Aktienmarkt, das spielt keine Rolle. Unser Ziel ist es, uns stets anzupassen, um die bestmögliche Simulation zu bieten und gleichzeitig alle erreichbaren Preispunkte abzudecken, ohne von dem abzuweichen, was die Balken zeigen.

Es ist wichtig zu verstehen, dass in manchen Situationen ein bestimmter Balken nicht für die Random-Walk-Simulation geeignet ist, während ein nachfolgender Balken den Prozess sofort nutzen kann. Infolgedessen können die Preise von harmonisch und sanft bis hin zu eher abrupt variieren. Diese Diskrepanz deutet nicht unbedingt auf ein Versagen des Simulations- oder Wiedergabesystems hin, sondern ist vielmehr eine Folge der Notwendigkeit einer schnellen Kursbewegung auf einem bestimmten Balken, die für eine sanftere Random-Walk-Simulation möglicherweise nicht mit einem erheblichen Handelsvolumen einherging. Auch das Gegenteil ist der Fall: Ein hohes Handelsvolumen kann die Anwendung einer Random-Walk-Methode ermöglichen, was nicht bedeutet, dass sich der Preis in Wirklichkeit gleichmäßig bewegt hat. In einigen Fällen kann die Bewegung stark sein, aber die Dichte der gehandelten Ticks ermöglichte es uns, in der Simulation einen Random Walk anzuwenden, der nicht unbedingt die tatsächlichen Marktbedingungen auf dem jeweiligen Balken widerspiegelt.

Es mag den Anschein haben, dass wir die ideale Lösung, d. h. unser Ziel, bereits erreicht haben. Aber so weit sind wir noch nicht. Obwohl die Random-Walk-Methode weit verbreitet ist, wenn die Anzahl der Handelsgeschäfte in einem 1-Minuten-Balken groß ist, ist sie nicht anwendbar, wenn die Anzahl der Handelsgeschäfte in einem 1-Minuten-Balken etwas geringer als notwendig ist. Außerdem führt die komplette Verwendung des Random Walk zur Simulation von Balkenbewegungen, wenn der Abstand zwischen dem Höchst- und dem Tiefstkurs nahe der Anzahl der Ticks liegt, zu einer Simulation, die bizarr aussieht. In solchen Fällen ist es notwendig, das Modell zu überdenken, das in einem anderen Artikel dieser Serie „Entwicklung eines Replay System — Marktsimulation (Teil 11); Geburt des SIMULATOR (I)“ besprochen wurde:, in dem wir ein System vorgeschlagen haben, das eine Umkehrung innerhalb eines Balkens erzeugt.

Die Idee, ein solches System einzuführen, scheint nicht nur angemessen, sondern auch möglich. Ziel ist es, realistische und gültige Bewegungen zu erzeugen und nicht völlig willkürlich Kurswerte auszulösen. Die zentrale Frage ist also nicht mehr die Zeit, sondern der im Preis angegebene Wert. Die Verwendung einer Funktion, die Werte ohne jegliche Logik generiert, insbesondere in Situationen, in denen ein erfahrener Händler eine Logik in der Preisbewegung erkennen würde, ist demotivierend. Aber auch für dieses Problem gibt es eine Lösung. Es wird versucht, die Ansätze aus Teil 11 der Reihe bis zur Gegenwart zu integrieren. Während diese Lösung für Neulinge vielleicht nicht sofort ersichtlich ist, ist sie für diejenigen mit mehr Programmiererfahrung ziemlich klar. Wir werden also nicht von Grund auf eine neue Simulationsfunktion erstellen. Wir werden zwischen weniger glatten und glatteren Bewegungen abwechseln, was durch den Simulator selbst bestimmt wird. Die Schlussfolgerung über die Gleichmäßigkeit der Bewegung wird auf nur fünf Informationen basieren: Eröffnungskurs, Schlusskurs, Höchstkurs, Tiefstkurs und Tickvolumen. Dies sind die einzigen Daten, die für diese Entscheidung erforderlich sind. Ich werde hier also nicht die endgültige und endgültige Lösung geben. Mein Ziel ist es, eine der vielen Möglichkeiten zu zeigen, wie man Bewegungen innerhalb eines 1-Minuten-Balken erzeugen und simulieren kann.


Verwendung des Random Walk in verschiedenen Szenarien – Verfolgung des Weges des geringsten Aufwands

Wie bereits erwähnt, müssen Sie nach einer Methode suchen, die eine gewisse Logik enthält. Die ausschließliche Abhängigkeit von der Randomisierung führt nicht zu befriedigenden Ergebnissen, selbst wenn man einen 1-Minuten-Balken und Charts mit einer längeren Periode, wie 10 oder 15 Minuten, verwendet. Idealerweise sollten die Bewegungen allmählich erfolgen, um plötzliche Übergänge von einem Ende zum anderen zu vermeiden. So wird die Bewegung allmählich gezeichnet, was den Eindruck von Zufälligkeit erweckt, obwohl sie in Wirklichkeit das Ergebnis einfacher mathematischer Berechnungen ist, die eine scheinbare Komplexität erzeugen. Dies ist eine der Grundlagen der stochastischen Bewegungen.

Um den Fluss intelligenter und reibungsloser zu gestalten, müssen einige bestehende Funktionen abgeschafft und Regeln aufgestellt werden, die die Bewegung in kontrollierte Bahnen lenken. Bitte beachten Sie, dass wir nicht versuchen sollten, eine Bewegung in eine bestimmte Richtung zu erzwingen, aber wir müssen die Regeln für MetaTrader 5 definieren, damit er den Prozess so handhabt, wie er es für richtig hält. Dazu müssen wir zunächst den Code des Random Walk ändern. Der überarbeitete Code ist nachstehend aufgeführt:

inline int RandomWalk(int In, int Out, const double Open, const double Close, double High, double Low, const int Spread, MqlTick &tick[], int iMode, int iDesloc)
   {
      double vStep, vNext, price, vH = High, vL = Low;
      char i0 = 0;
                                
      vNext = vStep = (Out - In) / ((High - Low) / m_TickSize);
      for (int c0 = In, c1 = 0, c2 = 0; c0 <= Out; c0++, c1++)
      {
         price = (m_IsPriceBID ? tick[c0 - 1].bid : tick[c0 - 1].last) + (m_TickSize * ((rand() & 1) == 1 ? -1 : 1));
         price = (price > vH ? price - m_TickSize : (price < vL ? price + m_TickSize : price));                                  
         price = (m_IsPriceBID ? tick[c0 - 1].bid : tick[c0 - 1].last) + (m_TickSize * ((rand() & 1) == 1 ? -iDesloc : iDesloc));
         price = (price > vH ? vH : (price < vL ? vL : price));
         MountPrice(c0, price, (Spread + RandomLimit((int)(Spread | (m_Marks.iMax & 0xF)), 0)), tick);
         switch (iMode)
         {
            case 1:
               i0 |= (price == High ? 0x01 : 0);
               i0 |= (price == Low ? 0x02 : 0);
               vH = (i0 == 3 ? High : vH);
               vL = (i0 ==3 ? Low : vL);
               break;
            case 0:
               if (price == Close) return c0;
            default:
               break;
         }
         if (((int)floor(vNext)) >= c1) continue; else if ((++c2) <= 3) continue;
         vNext += vStep;
         vL = (iMode != 2 ? (Close > vL ? (i0 == 3 ? vL : vL + m_TickSize) : vL) : (((c2 & 1) == 1) ? (Close > vL ? vL + m_TickSize : vL) : (Close < vH ? vL : vL + m_TickSize)));
         vH = (iMode != 2 ? (Close > vL ? vH : (i0 == 3 ? vH : vH - m_TickSize)) : (((c2 & 1) == 1) ? (Close > vL ? vH : vH - m_TickSize) : (Close < vH ? vH - m_TickSize : vH)));
         if (iMode == 2)
         {
            vL = (((c2 & 1) == 1) ? (Close > vL ? vL + m_TickSize : vL) : (Close < vH ? vL : vL + m_TickSize));
            vH = (((c2 & 1) == 1) ? (Close > vL ? vH : vH - m_TickSize) : (Close < vH ? vH - m_TickSize : vH));
         }else
         {
            if (Close > vL) vL = (i0 == 3 ? vL : vL + m_TickSize); else vH = (i0 == 3 ? vH : vH - m_TickSize);
         }                                       
      }
                                
      return Out;
   }

Zu den Änderungen gehört die Ersetzung einiger Codeabschnitte durch neue grüne Ergänzungen. Auch wenn die Änderungen nur geringfügig erscheinen, bieten sie doch wesentlich mehr Flexibilität als die Vorgängerversion. Zuvor war die Bewegung kontinuierlich, Tick für Tick, ohne Lücken dazwischen, was ein großes Handelsvolumen erforderte, um einen Random Walk reibungslos zu simulieren. Durch die Einführung von Lücken in einen 1-Minuten-Balken wird die Anzahl der erforderlichen Abschlüsse deutlich reduziert, sodass Sie das System mit unterschiedlichen Volumina und Parametern von 1-Minuten-Balken simulieren können. Dies führt zu einer grafischen Anpassung der durch den Random Walk erzeugten Bewegung, wenn vier Grundwerte erreicht werden: open, close, high and low. Das Zwischenverhalten wird durch einen Random Walk bestimmt. Der entscheidende Aspekt ist jedoch die Funktion, die den Random Walk aufruft. Diese Funktion wird im Folgenden ausführlich beschrieben:

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      int     i0, i1, i2;
      bool    b0;
                                
      m_Marks.iMax = (int) rate.tick_volume - 1;
      m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high);
      m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low);
      Simulation_Time(rate, tick);
      MountPrice(0, rate.open, rate.spread, tick);
      if (CheckViability(rate))
      if (m_Marks.iMax > 10)
      {
         i0 = (int)(MathMin(m_Marks.iMax / 3.0, m_Marks.iMax * 0.2));
         i1 = m_Marks.iMax - i0;
         i2 = (int)(((rate.high - rate.low) / m_TickSize) / i0);
         i2 = (i2 == 0 ? 1 : i2);
         b0 = (m_Marks.iMax >= 1000 ? ((rand() & 1) == 1) : (rate.high - rate.open) < (rate.open - rate.low));
         i0 = RandomWalk(1, i0, rate.open, (b0 ? rate.high : rate.low), rate.high, rate.low, rate.spread, tick, 0, i2);
         RandomWalk(i0, i1, (m_IsPriceBID ? tick[i0].bid : tick[i0].last), (b0 ? rate.low : rate.high), rate.high, rate.low, rate.spread, tick, 1, i2);
         RandomWalk(i1, m_Marks.iMax, (m_IsPriceBID ? tick[i1].bid : tick[i1].last), rate.close, rate.high, rate.low, rate.spread, tick, 2, i2);
         m_Marks.bHigh = m_Marks.bLow = true;
      }else Random_Price(rate, tick);
      if (!m_IsPriceBID) DistributeVolumeReal(rate, tick);
      if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick);
      if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick);
      MountPrice(m_Marks.iMax, rate.close, rate.spread, tick);
      CorretTime(tick);

      return m_Marks.iMax;
   }

Diese Funktion zeigt an, dass wir jetzt nicht mehr die Möglichkeit der Erzeugung eines Random Walk auf die gleiche Weise prüfen, wie wir es im gesamten Artikel getan haben. Die Auswertung findet nun vollständig in dieser Funktion statt. So seltsam es auch klingen mag, das System wird versuchen, einen Random Walk mit einem Mindestvolumen von nur 10 Handelsgeschäfte durchzuführen. Für Volumina, die unter diesem Schwellenwert liegen, wird die reine Randomisierung verwendet, die in diesem speziellen Kontext als effizienter als der Random Walk angesehen wird. Der innovative Aspekt ist die Schaffung von Lücken innerhalb des 1-Minuten-Balkens, die durch die oben erwähnten speziellen Berechnungen gewährleistet wird. Damit der Random Walk korrekt funktioniert, muss sichergestellt werden, dass mindestens 1 Tick erzeugt wird.

Dieser Prozess ist jedoch nicht ohne Schwierigkeiten. Damit der Random Walk wirksam ist, ist eine zusätzliche Kontrolle erforderlich. Diese zusätzliche Kontrolle erfolgt durch eine spezielle Prüfung, deren Wert je nach Bedarf angepasst werden kann. Wenn das Handelsvolumen 1000 in einer Minute übersteigt, kann das Simulationssystem einen Weg wählen, indem es zufällig entscheidet, welches Hoch oder Tief es zuerst ansteuert. Ist das Volumen hingegen geringer als das festgelegte Volumen, wird die anfängliche Richtung des Random Walk auf der Grundlage der Nähe des Eröffnungskurses zum Höchst- oder Tiefstkurs des Balkens bestimmt.

Diese Methode, die als „Weg des geringsten Aufwands“ bezeichnet wird, ist dann wirksam, wenn die Anzahl der erforderlichen Bewegungen geringer ist als die Gesamtstrecke, die zurückgelegt werden muss. Dadurch werden Entscheidungen vermieden, die zu unnötig langen und komplizierten Routen führen könnten. Aufgrund dieses rechnerischen Ansatzes kann es sein, dass einige der in diesem Artikel vorgeschlagenen Diskussionen und Methoden nicht in der endgültigen Anwendung erscheinen. Die beiden folgenden Abbildungen veranschaulichen die Wirksamkeit des Systems: ein Chart auf der Grundlage echter Tickdaten und ein Simulationsergebnis unter Verwendung der Strategie des geringsten Aufwands.

Abbildung 02

Abbildung 02 - Chart auf der Grundlage realer Daten


Abbildung 03

Abbildung 03 - Chart, das anhand der vom System simulierten Daten erstellt wurde.

Auch wenn die Charts auf den ersten Blick identisch erscheinen, sind sie es nicht. Bei genauerer Betrachtung können Unterschiede festgestellt werden, z. B. die Datenquelle, die in jeder Abbildung aufgeführt ist, um die Unterschiede zwischen ihnen hervorzuheben. Dieser Vergleich lädt die Nutzer ein, ein Experiment mit den in der Anwendung dargestellten Daten speziell für den Vermögenswert EURUSD, d. h. ein Devisenpaar, durchzuführen. Diese Demonstration zeigt, dass die Simulationsmethode sowohl an die Last- als auch an die Bid-Chart-Typen angepasst werden kann, sodass die Leistung des Systems anhand vorhandener Daten getestet werden kann.


Schlussfolgerung

Dieser Artikel ist ein entscheidender Schritt bei der Vorbereitung des voll funktionsfähigen Replay-/Simulationssystems. Im nächsten Artikel werden wir uns die letzten erforderlichen Einstellungen ansehen, bevor wir den Wiedergabe-/Simulationsdienst näher beschreiben. Diese Phase ist wichtig für diejenigen, die die Leistung und Wirksamkeit des Systems unter Testbedingungen verstehen wollen.

Ein wichtiger Hinweis zu den angehängten Dateien: Aufgrund des großen Datenvolumens, insbesondere wenn es sich um echte Ticks für zukünftige Vermögenswerte handelt, stelle ich vier Dateien zur Verfügung, die jeweils mit einem bestimmten Vermögenswert oder Markt verbunden sind. Die Hauptdatei enthält den Quellcode des Systems bis zum aktuellen Stand der Entwicklung. Um die Integrität der Systemstruktur und -funktionalität zu gewährleisten, müssen alle Dateien heruntergeladen und in dem vom MQL5-Editor angegebenen Verzeichnis gespeichert werden.

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

Beigefügte Dateien |
Files_-_BOLSA.zip (1358.24 KB)
Files_-_FOREX.zip (3743.96 KB)
Files_-_FUTUROS.zip (11397.51 KB)
Datenkennzeichnung für die Zeitreihenanalyse (Teil 4):Deutung der Datenkennzeichnungen durch Aufgliederung Datenkennzeichnung für die Zeitreihenanalyse (Teil 4):Deutung der Datenkennzeichnungen durch Aufgliederung
In dieser Artikelserie werden verschiedene Methoden zur Kennzeichnung (labeling) von Zeitreihen vorgestellt, mit denen Daten erstellt werden können, die den meisten Modellen der künstlichen Intelligenz entsprechen. Eine gezielte und bedarfsgerechte Kennzeichnung von Daten kann dazu führen, dass das trainierte Modell der künstlichen Intelligenz besser mit dem erwarteten Design übereinstimmt, die Genauigkeit unseres Modells verbessert wird und das Modell sogar einen qualitativen Sprung machen kann!
Entwicklung eines Replay Systems — Marktsimulation (Teil 23): FOREX (IV) Entwicklung eines Replay Systems — Marktsimulation (Teil 23): FOREX (IV)
Jetzt erfolgt die Erstellung an der gleichen Stelle, an der wir die Ticks in Balken umgewandelt haben. Wenn also bei der Konvertierung etwas schief geht, werden wir den Fehler sofort bemerken. Dies liegt daran, dass derselbe Code, der die 1-Minuten-Balken während des schnellen Vorlaufs auf dem Chart platziert, auch für das Positionierungssystem verwendet wird, um die Balken während der normalen Performance zu platzieren. Mit anderen Worten: Der Code, der für diese Aufgabe zuständig ist, wird nirgendwo anders dupliziert. Auf diese Weise erhalten wir ein viel besseres System sowohl für die Instandhaltung als auch für die Verbesserung.
Quantisierung beim maschinellen Lernen (Teil 1): Theorie, Beispielcode, Analyse der Implementierung in CatBoost Quantisierung beim maschinellen Lernen (Teil 1): Theorie, Beispielcode, Analyse der Implementierung in CatBoost
Der Artikel befasst sich mit der theoretischen Anwendung der Quantisierung bei der Konstruktion von Baummodellen und stellt die in CatBoost implementierten Quantisierungsmethoden vor. Es werden keine komplexen mathematischen Gleichungen verwendet.
Neuronale Netze sind einfach (Teil 59): Dichotomy of Control (DoC) Neuronale Netze sind einfach (Teil 59): Dichotomy of Control (DoC)
Im vorigen Artikel haben wir uns mit dem Decision Transformer vertraut gemacht. Das komplexe stochastische Umfeld des Devisenmarktes erlaubte es uns jedoch nicht, das Potenzial der vorgestellten Methode voll auszuschöpfen. In diesem Artikel werde ich einen Algorithmus vorstellen, der die Leistung von Algorithmen in stochastischen Umgebungen verbessern soll.