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

Entwicklung eines Replay Systems — Marktsimulation (Teil 23): FOREX (IV)

MetaTrader 5Tester | 7 März 2024, 10:29
108 0
Daniel Jose
Daniel Jose

Einführung

Im vorigen Artikel „Entwicklung eines Wiedergabesystems — Marktsimulation (Teil 22): FOREX (III)“ haben wir einige Änderungen am System vorgenommen, um den Simulator in die Lage zu versetzen, Informationen auf der Grundlage der Bid-Preise und nicht nur auf der Grundlage der letzten Preise oder Last zu generieren. Aber diese Änderungen haben mich nicht zufrieden gestellt, und der Grund dafür ist einfach: Wir duplizieren den Code, und das passt mir überhaupt nicht.

Es gibt einen Punkt in diesem Artikel, an dem ich meine Unzufriedenheit deutlich gemacht habe:

... Fragen Sie mich nicht warum. Aber aus irgendeinem seltsamen Grund, von dem ich persönlich keine Ahnung habe, müssen wir diese Zeile hier hinzufügen. Wenn wir ihn nicht hinzufügen, ist der für das Tick-Volumen angezeigte Wert falsch. Achten Sie darauf, dass es eine Bedingung in der Funktion gibt. Dadurch werden Probleme bei der Verwendung des schnellen Positionierungssystems vermieden und es wird verhindert, dass ein seltsamer Balken erscheint, der auf dem Diagramm des Systems außerhalb der Zeit liegt. Obwohl dies ein sehr seltsamer Grund ist, funktioniert alles andere wie erwartet. Dabei handelt es sich um eine neue Berechnung, bei der die Ticks auf die gleiche Weise gezählt werden - sowohl bei der Arbeit mit einem Bid-basierten Asset als auch bei der Arbeit mit einem Last-basierten Instrument.

Da der Code für den Artikel aber schon fertig war und der Artikel fast fertig war, habe ich alles so gelassen, wie es war, aber das hat mich wirklich gestört. Es macht keinen Sinn, dass der Code in manchen Situationen funktioniert und in anderen nicht. Selbst beim Debuggen des Codes und beim Versuch, die Ursache des Fehlers zu finden, konnte ich ihn nicht finden. Aber nachdem ich den Code einen Moment lang in Ruhe gelassen und mir das Flussdiagramm des Systems angesehen hatte (ja, man sollte immer versuchen, ein Flussdiagramm zu verwenden, um die Programmierung zu beschleunigen), stellte ich fest, dass ich einige Änderungen vornehmen konnte, um Code-Duplikationen zu vermeiden. Und zu allem Übel wurde der Code auch noch verdoppelt. Dadurch entstand ein Problem, das ich nicht lösen konnte. Aber es gibt eine Lösung, und wir werden diesen Artikel mit einer Lösung für dieses Problem beginnen, da sein Vorhandensein es unmöglich machen kann, Simulatorcode für die Arbeit mit Devisenmarktdaten korrekt zu schreiben.


Lösung des Problems mit dem Tickvolumen

In diesem Thema werde ich zeigen, wie das Problem, das zum Ausfall des Tickvolumens führte, behoben wurde. Zuerst musste ich den Code für das Lesen der Ticks ändern, wie unten gezeigt:

datetime LoadTicks(const string szFileNameCSV, const bool ToReplay = true)
    {
        int      MemNRates,
                 MemNTicks;
        datetime dtRet = TimeCurrent();
        MqlRates RatesLocal[],
                 rate;
        bool     bNew;
        
        MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
        MemNTicks = m_Ticks.nTicks;
        if (!Open(szFileNameCSV)) return 0;
        if (!ReadAllsTicks(ToReplay)) return 0;         
        rate.time = 0;
        for (int c0 = MemNTicks; c0 < m_Ticks.nTicks; c0++)
        {
            if (!BuildBar1Min(c0, rate, bNew)) continue;
            if (bNew) ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
            m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate;
        }
        if (!ToReplay)
        {
            ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates));
            ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0);
            CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates));
            dtRet = m_Ticks.Rate[m_Ticks.nRate].time;
            m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates);
            m_Ticks.nTicks = MemNTicks;
            ArrayFree(RatesLocal);
        }else
        {
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TRADE_CALC_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CALC_MODE_EXCH_STOCKS : SYMBOL_CALC_MODE_FOREX);
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_CHART_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CHART_MODE_LAST : SYMBOL_CHART_MODE_BID);
        }
        m_Ticks.bTickReal = true;
        
        return dtRet;
    };

Zuvor war dieser Code Teil des Codes, der Ticks in 1-Minuten-Balken umwandelt, aber jetzt werden wir einen anderen Code verwenden. Der Grund dafür ist, dass dieser Aufruf nun mehr als einem Zweck dient und die Arbeit, die er leistet, auch zur Erstellung von sich wiederholenden Takten verwendet wird. Dadurch wird vermieden, dass der Code für die Erstellung von Balken in Klassen dupliziert wird.

Schauen wir uns den Umwandlungscode an:

inline bool BuildBar1Min(const int iArg, MqlRates &rate, bool &bNew)
inline void BuiderBar1Min(const int iFirst)
   {
      MqlRates rate;
      double   dClose = 0;
      bool     bNew;
                                
      rate.time = 0;
      for (int c0 = iFirst; c0 < m_Ticks.nTicks; c0++)
      {
         switch (m_Ticks.ModePlot)
         {
            case PRICE_EXCHANGE:
               if (m_Ticks.Info[c0].last == 0.0) continue;
               if (m_Ticks.Info[iArg].last == 0.0) return false;
               dClose = m_Ticks.Info[c0].last;
               break;
            case PRICE_FOREX:
               dClose = (m_Ticks.Info[c0].bid > 0.0 ? m_Ticks.Info[c0].bid : dClose);
               if ((dClose == 0.0) || (m_Ticks.Info[c0].bid == 0.0)) continue;
               if ((dClose == 0.0) || (m_Ticks.Info[iArg].bid == 0.0)) return false;
               break;
         }
         if (bNew = (rate.time != macroRemoveSec(m_Ticks.Info[c0].time)))
         {
            ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
            rate.time = macroRemoveSec(m_Ticks.Info[c0].time);
            rate.real_volume = 0;
            rate.tick_volume = (m_Ticks.ModePlot == PRICE_FOREX ? 1 : 0);
            rate.open = rate.low = rate.high = rate.close = dClose;
         }else
         {
            rate.close = dClose;
            rate.high = (rate.close > rate.high ? rate.close : rate.high);
            rate.low = (rate.close < rate.low ? rate.close : rate.low);
            rate.real_volume += (long) m_Ticks.Info[c0].volume_real;
            rate.tick_volume++;
         }
         m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate;
      }
      return true;                    
   }

Alle durchgestrichenen Elemente im Code wurden entfernt, da sie die korrekte Erstellung von Elementen zur Verwendung in der Klasse C_Replay verhinderten. Andererseits musste ich diese Punkte hinzufügen, um den Anrufer darüber zu informieren, was bei der Umwandlung passiert ist.

Beachten Sie, dass diese Funktion ursprünglich in der Klasse C_FileTicks privat war. Ich habe die Zugriffsebene geändert, damit sie in der Klasse C_Replay verwendet werden kann. Trotzdem möchte ich nicht, dass es zu weit über diese Grenzen hinausgeht, also wird der Zugriff nicht ‚public‘, sondern ‚protected‘ sein. Auf diese Weise können wir den Zugriff auf die von der Klasse C_Replay maximal erlaubte Stufe beschränken. Wie Sie sich erinnern, ist die höchste Stufe die Klasse C_Replay. Daher kann nur auf Prozeduren und Funktionen, die in der Klasse C_Replay als öffentlich deklariert sind, außerhalb der Klasse zugegriffen werden. Der interne Aufbau des Systems muss innerhalb dieser Klasse C_Replay vollständig verborgen sein.

Schauen wir uns nun die neue Funktion zur Erstellung von Balken an.

inline void CreateBarInReplay(const bool bViewTicks)
   {
#define def_Rate m_MountBar.Rate[0]

      bool    bNew;
      double  dSpread;
      int     iRand = rand();
                                
      if (BuildBar1Min(m_ReplayCount, def_Rate, bNew))
      {
         m_Infos.tick[0] = m_Ticks.Info[m_ReplayCount];
         if ((!m_Ticks.bTickReal) && (m_Ticks.ModePlot == PRICE_EXCHANGE))
         {                                               
            dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 );
            if (m_Infos.tick[0].last > m_Infos.tick[0].ask)
            {
               m_Infos.tick[0].ask = m_Infos.tick[0].last;
               m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread;
            }else   if (m_Infos.tick[0].last < m_Infos.tick[0].bid)
            {
               m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread;
               m_Infos.tick[0].bid = m_Infos.tick[0].last;
            }
         }
         if (bViewTicks) CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
         CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
      }
      m_ReplayCount++;
#undef def_Rate
   }

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. Ich möchte Sie aber auch auf etwas Wichtiges hinweisen, das wir dem obigen Code hinzugefügt haben. Die Simulation der Preise von Bid und Ask erfolgt nur, wenn wir uns in einem simulierten System befinden und die simulierten Daten vom Typ Börse sind. Das heißt, wenn die Darstellung auf Bid basiert, wird diese Simulation nicht mehr durchgeführt. Dies ist wichtig für das, was wir im nächsten Thema entwerfen werden.


Starten wir die Simulation der Bid-basierten Darstellung (Forex-Modus).

Im Folgenden werden wir ausschließlich die Klasse C_Simulation betrachten. Wir werden dies tun, um Daten zu modellieren, die von der aktuellen Implementierung des Systems nicht abgedeckt werden. Aber zuerst müssen wir eine Kleinigkeit tun:

bool BarsToTicks(const string szFileNameCSV)
   {
      C_FileBars *pFileBars;
      int         iMem = m_Ticks.nTicks,
                  iRet;
      MqlRates    rate[1];
      MqlTick     local[];
                                
      pFileBars = new C_FileBars(szFileNameCSV);
      ArrayResize(local, def_MaxSizeArray);
      Print("Converting bars to ticks. Please wait...");
      while ((*pFileBars).ReadBar(rate) && (!_StopFlag))
      {
         ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
         m_Ticks.Rate[++m_Ticks.nRate] = rate[0];
         if ((iRet = Simulation(rate[0], local)) < 0)
         {
            ArrayFree(local);
            delete pFileBars;
            return false;
         }
         for (int c0 = 0; c0 <= iRet; c0++)
         {
            ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
            m_Ticks.Info[m_Ticks.nTicks++] = local[c0];
         }
      }
      ArrayFree(local);
      delete pFileBars;
      m_Ticks.bTickReal = false;
                                
      return ((!_StopFlag) && (iMem != m_Ticks.nTicks));
   }

Wenn etwas schief geht und wir das System komplett abschalten wollen, brauchen wir eine Möglichkeit, anderen Klassen mitzuteilen, dass die Simulation fehlgeschlagen ist. Dies ist der einfachste Weg, dies zu tun. Allerdings gefällt mir die Art und Weise, wie wir diese Funktion geschaffen haben, nicht wirklich. Es funktioniert zwar, aber es fehlen einige Dinge, die wir der Klasse C_Simulation mitteilen müssen. Nachdem ich den Code analysiert hatte, beschloss ich, die Funktionsweise der Funktion zu ändern. Sie muss geändert werden, um eine Verdoppelung des Codes zu vermeiden. Vergessen Sie also die vorherige Funktion. Obwohl es funktioniert, werden wir die folgende Variante verwenden:

int SetSymbolInfos(void)
   {
      int iRet;
                                
      CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, iRet = (m_Ticks.ModePlot == PRICE_EXCHANGE ? 4 : 5));
      CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TRADE_CALC_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CALC_MODE_EXCH_STOCKS : SYMBOL_CALC_MODE_FOREX);
      CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_CHART_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CHART_MODE_LAST : SYMBOL_CHART_MODE_BID);
                                
      return iRet;
   }
//+------------------------------------------------------------------+
   public  :
//+------------------------------------------------------------------+
      bool BarsToTicks(const string szFileNameCSV)
      {
         C_FileBars      *pFileBars;
         C_Simulation    *pSimulator = NULL;
         int             iMem = m_Ticks.nTicks,
                         iRet = -1;
         MqlRates        rate[1];
         MqlTick         local[];
         bool            bInit = false;
                                
         pFileBars = new C_FileBars(szFileNameCSV);
         ArrayResize(local, def_MaxSizeArray);
         Print("Converting bars to ticks. Please wait...");
         while ((*pFileBars).ReadBar(rate) && (!_StopFlag))
         {
            if (!bInit)
            {
               m_Ticks.ModePlot = (rate[0].real_volume > 0 ? PRICE_EXCHANGE : PRICE_FOREX);
               pSimulator = new C_Simulation(SetSymbolInfos());
               bInit = true;
            }
            ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
            m_Ticks.Rate[++m_Ticks.nRate] = rate[0];
            if (pSimulator == NULL) iRet = -1; else iRet = (*pSimulator).Simulation(rate[0], local);
            if (iRet < 0) break;
            for (int c0 = 0; c0 <= iRet; c0++)
            {
               ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
               m_Ticks.Info[m_Ticks.nTicks++] = local[c0];
            }
         }
         ArrayFree(local);
         delete pFileBars;
         delete pSimulator;
         m_Ticks.bTickReal = false;
                                
         return ((!_StopFlag) && (iMem != m_Ticks.nTicks) && (iRet > 0));
      }

Die zweite Option ist im Hinblick auf unsere Ziele wesentlich effektiver. Außerdem vermeiden wir die Duplizierung des Codes, vor allem, weil wir dadurch die folgenden Vorteile erhalten:

  • Wir beseitigen die Vererbung der Klasse C_Simulation. Dies wird das System noch flexibler machen.
  • Wir initialisieren die Asset-Daten, das bisher nur bei Verwendung von echten Ticks durchgeführt wurde.
  • Es entsteht die entsprechende Breite für die in der grafischen Darstellung verwendeten Symbole.
  • Die Klasse C_Simulation wird als Zeiger verwendet. Das bedeutet eine effizientere Nutzung des Systemspeichers, denn nachdem die Klasse ihre Arbeit beendet hat, wird der von ihr belegte Speicher wieder freigegeben.
  • Stellen Sie sicher, dass es nur einen Eintrittspunkt und einen Austrittspunkt aus der Funktion gibt.
Einige Dinge werden sich im Vergleich zum vorherigen Artikel ändern. Aber lassen Sie uns mit der Implementierung der Klasse C_Simulation fortfahren. Das wichtigste Detail bei der Entwicklung der Klasse C_Simulation ist, dass wir eine beliebige Anzahl von Ticks im System haben können. Dies ist zwar kein Problem (zumindest im Moment), aber die Schwierigkeit besteht darin, dass in vielen Fällen die Spanne, die wir zwischen dem Höchst- und dem Tiefstwert abdecken müssen, bereits viel größer ist als die Anzahl der gemeldeten oder zu erstellenden Ticks. Dabei werden der Abschnitt, der vom Eröffnungskurs ausgeht und bis zu einem der Extremwerte reicht, und der Abschnitt, der von einem der Extremwerte ausgeht und bis zum Schlusskurs reicht, nicht mitgezählt. Wenn wir diese Berechnung mit einem RANDOM WALK durchführen, wird dies in einer großen Anzahl von Fällen unmöglich sein. Daher müssen wir den in früheren Artikeln erstellten Random Walk eliminieren und eine neue Methode zur Erzeugung von Ticks entwickeln. Wie ich bereits sagte, ist das Problem bei Devisen nicht so eindeutig.

Das Problem bei diesem Ansatz ist, dass man oft zwei verschiedene Methoden schaffen und so harmonisch wie möglich gestalten muss. Das Schlimmste daran ist, dass die Random-Walk-Simulation in einigen Fällen sehr viel näher an den realen Vermögenswerten liegt. Aber wenn wir es mit einem geringen Handelsvolumen zu tun haben (weniger als 500 Handelsgeschäfte pro Minute), dann ist Random Walk völlig ungeeignet. In dieser Situation können wir einen etwas exotischeren Ansatz wählen, um alle möglichen Fälle abzudecken. Das erste, was wir tun werden (da wir die Klasse initialisieren müssen), ist einen Konstruktor für die Klasse zu definieren, dessen Code unten zu sehen ist:

C_Simulation(const int nDigits)
   {
      m_NDigits       = nDigits;
      m_IsPriceBID    = (SymbolInfoInteger(def_SymbolReplay, SYMBOL_CHART_MODE) == SYMBOL_CHART_MODE_BID);
      m_TickSize      = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
   }

Hier initialisieren wir einfach die privaten Daten der Klasse, um sie nicht an anderer Stelle suchen zu müssen. Vergewissern Sie sich daher, dass alle Einstellungen in der Konfigurationsdatei der zu simulierenden Kühlstelle korrekt gesetzt sind, einschließlich der Darstellungsart. Andernfalls kann es zu seltsamen Fehlern im System kommen.

Jetzt können wir anfangen, weiterzumachen, da wir einige grundlegende Initialisierungen der Klasse vorgenommen haben. Schauen wir uns zunächst die Probleme an, die gelöst werden müssen. Zunächst müssen wir einen zufälligen Zeitwert generieren, der jedoch in der Lage sein muss, alle Ticks zu verarbeiten, die auf 1-Minuten-Balken erzeugt werden. Dies ist eigentlich der einfachste Teil der Umsetzung. Bevor wir jedoch mit der Erstellung von Funktionen beginnen, müssen wir eine spezielle Art von Prozedur erstellen (siehe unten):

template < typename T >
inline T RandomLimit(const T Limit01, const T Limit02)
   {
      T a = (Limit01 > Limit02 ? Limit01 - Limit02 : Limit02 - Limit01);
      return (Limit01 >= Limit02 ? Limit02 : Limit01) + ((T)(((rand() & 32767) / 32737.0) * a));
   }

Was genau bringt uns dieses Verfahren? Es kann überraschend sein, diese Funktion zu sehen, ohne zu verstehen, was vor sich geht. Ich werde also versuchen, so einfach wie möglich zu erklären, was diese Funktion eigentlich tut und warum sie so seltsam aussieht.

In dem neuen Code benötigen wir eine Art von Funktion, die einen Zufallswert zwischen zwei Extremen erzeugen kann. In einigen Fällen muss dieser Wert als Double-Datentyp gebildet werden, während in anderen Fällen Ganzzahlwerte benötigt werden. Die Erstellung von zwei praktisch identischen Prozeduren zur Durchführung derselben Art von Faktorisierung wäre mit erheblichem Aufwand verbunden. Um dies zu vermeiden, erzwingen wir, oder besser gesagt, teilen wir dem Compiler mit, dass wir dieselbe Faktorisierung verwenden müssen, und überladen sie, sodass wir im Code dieselbe Funktion verwenden können, aber in der ausführbaren Form tatsächlich zwei verschiedene Funktionen haben werden. Wir verwenden diese Deklaration für diesen Zweck – sie definiert den Typ, in diesem Fall den Buchstaben T. Dies muss überall dort wiederholt werden, wo der Compiler den Typ festlegen soll. Deshalb sollten Sie darauf achten, dass Sie nichts verwechseln. Lassen Sie den Compiler Korrekturen vornehmen, um Probleme beim Casting zu vermeiden.

Es wird also immer die gleiche Berechnung durchgeführt, die jedoch je nach Art der verwendeten Variablen angepasst wird. Der Compiler wird dies tun, da er entscheidet, welcher Typ der richtige ist. Auf diese Weise können wir bei jedem Aufruf eine Pseudo-Zufallszahl erzeugen, unabhängig vom verwendeten Typ, aber beachten Sie, dass die Typen beider Grenzen gleich sein sollten. Mit anderen Worten: Sie können double nicht mit integer oder long integer nicht mit short integer mischen. Das wird nicht funktionieren. Dies ist die einzige Einschränkung dieses Ansatzes, wenn wir die Typüberladung verwenden.

Aber wir sind noch nicht fertig. Wir haben die obige Funktion erstellt, um die Erzeugung von Makros im Code der Klasse C_Simulation zu vermeiden. Gehen wir nun zum nächsten Schritt über - der Erstellung des Simulationszeitsystems. Diese Erzeugung ist im folgenden Code zu sehen:

inline void Simulation_Time(int imax, const MqlRates &rate, MqlTick &tick[])
   {
      for (int c0 = 0, iPos, v0 = (int)(60000 / rate.tick_volume), v1 = 0, v2 = v0; c0 <= imax; c0++, v1 = v2, v2 += v0)
      {
         iPos = RandomLimit(v1, v2);
         tick[c0].time = rate.time + (iPos / 1000);
         tick[c0].time_msc = iPos % 1000;
      }
   }

Hier simulieren wir, dass die Zeit leicht zufällig ist. Auf den ersten Blick mag dies recht verwirrend erscheinen. Aber glauben Sie mir, die Zeit ist hier zufällig, obwohl sie immer noch nicht der Logik entspricht, die von der Klasse C_Replay erwartet wird. Das liegt daran, dass der Wert in Millisekunden nicht korrekt eingestellt ist. Diese Anpassung wird später vorgenommen. Hier wollen wir nur, dass die Zeit zufällig generiert wird, aber innerhalb eines 1-Minuten-Balkens. Wie können wir das tun? Zunächst teilen wir die Zeit von 60 Sekunden, die eigentlich 60.000 Millisekunden beträgt, durch die Anzahl der zu erzeugenden Ticks. Dieser Wert ist für uns wichtig, da er uns sagt, welchen Grenzwertbereich wir verwenden werden. Danach werden wir in jeder Iteration der Schleife mehrere einfache Zuweisungen durchführen. Das Geheimnis der Erzeugung eines zufälligen Timers liegt nun in diesen drei Zeilen innerhalb der Schleife. In der ersten Zeile bitten wir den Compiler, einen Aufruf zu erzeugen, in dem wir Integer-Daten verwenden werden. Dieser Aufruf gibt einen Wert im angegebenen Bereich zurück. Wir werden dann zwei sehr einfache Berechnungen durchführen. Zunächst passen wir den generierten Wert an die Zeit des Minutenbalkens an, und dann verwenden wir denselben generierten Wert, um die Zeit in Millisekunden anzupassen. Somit hat jeder Tick einen völlig zufälligen Zeitwert. Denken Sie daran, dass wir in diesem frühen Stadium nur die Zeit korrigieren. Der Zweck dieser Einstellung besteht darin, eine übermäßige Vorhersehbarkeit zu vermeiden.

Fahren wir fort mit der Simulation der Preise. Ich möchte Sie noch einmal daran erinnern, dass wir uns nur auf das auf Geboten basierende Plottsystem konzentrieren werden. Anschließend werden wir das Simulationssystem miteinander verknüpfen, sodass wir eine viel allgemeinere Möglichkeit haben, eine solche Simulation durchzuführen, die sowohl Bid als auch Last umfasst. Hier konzentrieren wir uns auf Bid. Um in diesem ersten Schritt eine Simulation zu erzeugen, werden wir den Spread immer auf dem gleichen Abstand halten. Wir wollen den Code nicht verkomplizieren, bevor wir getestet haben, ob er tatsächlich funktioniert. Diese erste Simulation wird mit mehreren relativ kurzen Funktionen durchgeführt. Wir werden kurze Funktionen verwenden, um alles so modular wie möglich zu gestalten. Den Grund dafür werden Sie später sehen.

Schauen wir uns nun den ersten der Aufrufe an, die zur Erstellung der gebotsbasierten Simulation gemacht werden:

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

Die obige Funktion ist recht einfach zu verstehen. Allerdings scheint der schwierigste Teil die zufällige Konstruktion des Gebotswertes zu sein. Aber auch in diesem Fall ist alles ganz einfach. Es werden pseudozufällige Werte im Bereich zwischen dem Höchst- und dem Mindestwert des Balkens erzeugt. Beachten Sie aber, dass ich den Wert normalisiere. Der Grund dafür ist, dass der erzielte Wert in der Regel außerhalb der Preisspanne liegt. Deshalb müssen wir sie normalisieren. Aber ich denke, der Rest der Funktion sollte klar sein.

Wenn Sie genau hinsehen, werden Sie feststellen, dass wir zwei Funktionen haben, die oft im Modellierungsteil erwähnt werden: MOUNT_BID und UNIQUE. Jede von ihnen dient einem bestimmten Zweck. Beginnen wir mit Unique. Der Code ist unten dargestellt:

inline int Unique(const int imax, const double price, const MqlTick &tick[])
   {
      int iPos = 1;
                                
      do
      {
         iPos = (imax > 20 ? RandomLimit(1, imax - 1) : iPos + 1);
      }while ((m_IsPriceBID ? tick[iPos].bid : tick[iPos].last) == price);
                                
      return iPos;
   }

Diese Funktion verhindert, dass der Wert eines der Limits oder eines anderen Preises bei der Erzeugung einer Zufallsposition gelöscht wird. Im Moment werden wir sie nur für die Grenzwerte verwenden. Beachten Sie, dass wir entweder den simulierten Bid-Wert oder den simulierten Last-Wert verwenden können. Jetzt arbeiten wir nur noch mit Bid. Dies ist der einzige Zweck dieser Funktion: sicherzustellen, dass wir den Grenzwert nicht überschreiben.

Schauen wir uns nun die Funktion Mount_BID an, deren Code unten angegeben ist:

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

Obwohl dieser Code in diesem frühen Stadium noch recht einfach ist und nicht an die Schönheit der reinen Programmierung heranreicht, macht er uns das Leben sehr viel leichter. So vermeiden Sie die Wiederholung des Codes an mehreren Stellen und, was am wichtigsten ist, Sie können sich daran erinnern, den Wert zu normalisieren, der an der Position des Ask-Preises platziert werden soll. Wenn diese Normalisierung nicht durchgeführt wird, treten bei der weiteren Verwendung dieses Ask-Wertes Probleme auf. Der ASK-Preiswert wird immer durch den Spread-Wert ausgeglichen. Im Moment ist dieser Versatz jedoch immer konstant. Das liegt daran, dass dies die erste Implementierung ist, und wenn wir das Zufallsgenerator-System jetzt implementieren würden, wäre es völlig unklar, warum und wie der Streuwert willkürlich festgelegt wird.

Der hier angezeigte Spread-Wert ist der Wert, der auf dem jeweiligen 1-Minuten-Balken angezeigt wird. Jeder Balken kann einen anderen Spread haben, aber es gibt noch etwas, das Sie verstehen müssen. Wenn Sie eine Simulation durchführen, um ein System zu erhalten, das einem realen Markt ähnelt (d.h. den Daten in einer realen Tick-Datei), dann werden Sie feststellen, dass der verwendete Spread der kleinere der Werte ist, die bei der Bildung des 1-Minuten-Balkens vorhanden sind. Wenn Sie jedoch eine zufällige Simulation durchführen, bei der die Daten dem realen Markt entsprechen oder auch nicht, kann dieser Spread einen beliebigen Wert haben. Hier werden wir uns an die Idee halten, zu konstruieren, was auf dem Markt passieren könnte. Daher ist der Wert der Spanne immer derjenige, der in der Balkendatei angegeben ist.

Damit das System funktioniert, ist eine weitere Funktion erforderlich. Sie sollte für das Einrichten des Timings verantwortlich sein, damit die Klasse C_Replay die richtigen Timing-Werte hat. Dieser Code ist unten zu sehen:

inline void CorretTime(int imax, MqlTick &tick[])
   {
      for (int c0 = 0; c0 <= imax; c0++)
         tick[c0].time_msc += (tick[c0].time * 1000);
   }

Diese Funktion passt einfach die angegebene Zeit in Millisekunden entsprechend an. Wenn Sie genau hinsehen, können Sie erkennen, dass die Berechnungen dieselben sind wie die, die in der Funktion verwendet werden, die die tatsächlichen Ticks aus einer Datei lädt. Der Grund für diesen modularen Ansatz ist, dass es interessant sein kann, Aufzeichnungen über jede der ausgeführten Funktionen zu führen. Wäre der gesamte Code miteinander verbunden, wäre es schwieriger, solche Datensätze zu erstellen. Auf diese Weise ist es jedoch möglich, Berichte zu erstellen und sie zu studieren und somit zu prüfen, was verbessert werden sollte und was nicht, um spezifischen Bedürfnissen gerecht zu werden.

Wichtiger Hinweis: In diesem frühen Stadium werde ich die Verwendung des auf Last basierenden Systems blockieren. Wir werden ihn an einigen Stellen ändern, damit er auch in Zeiten geringer Liquidität mit Vermögenswerten funktioniert. Dies ist derzeit nicht möglich, aber wir werden dies später beheben. Wenn Sie jetzt versuchen, eine Simulation auf der Grundlage der letzten Preise durchzuführen, wird das System dies nicht zulassen. Wir werden dies später beheben.

Um dies zu gewährleisten, werden wir eine der Programmiertechniken anwenden. Es wird etwas sehr Komplexes und gut Verwaltetes sein. Siehe den nachstehenden Code:

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      int imax;
                        
      imax = (int) rate.tick_volume - 1;
      Simulation_Time(imax, rate, tick);
      if (m_IsPriceBID) Simulation_BID(imax, rate, tick); else return -1;
      CorretTime(imax, tick);

      return imax;
   }

Jedes Mal, wenn das System den Modus „Last plotting“ (Darstellung der Last-Preise) verwendet, wird ein Fehler ausgegeben. Das liegt daran, dass wir die auf Last basierende Simulation verbessern müssen. Deshalb musste ich diesen komplexen und raffinierten Trick hinzufügen. Wenn Sie versuchen, eine auf Last basierende Simulation durchzuführen, werden Sie einen negativen Wert erhalten. Ist das nicht eine komplizierte Methode?

Doch bevor wir diesen Artikel abschließen, wollen wir uns noch einmal mit dem Thema der Modellierung der Bid-Darstellung befassen. Als Ergebnis werden wir eine leicht verbesserte Art der Randomisierung haben. Im Grunde genommen müssen wir einen Zeitpunkt so ändern, dass er einen zufälligen Streuwert hat. Dies kann mit der Funktion Mount_Bid oder Simulation_Bid geschehen. In gewisser Weise ist dies keine große Sache, aber um den in der 1-Minuten-Bar-Datei angegebenen Mindestwert für die Streuung zu gewährleisten, werden wir eine Änderung an der unten dargestellten Funktion vornehmen:

inline void Simulation_BID(int imax, const MqlRates &rate, MqlTick &tick[])
   {
      bool    bHigh  = (rate.open == rate.high) || (rate.close == rate.high), 
      bLow = (rate.open == rate.low) || (rate.close == rate.low);

      Mount_BID(0, rate.open, rate.spread, tick);     
      for (int c0 = 1; c0 < imax; c0++)
      {
         Mount_BID(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), (rate.spread + RandomLimit((int)(rate.spread | (imax & 0xF)), 0)), tick);
         bHigh = (rate.high == tick[c0].bid) || bHigh;
         bLow = (rate.low == tick[c0].bid) || bLow;
      }
      if (!bLow) Mount_BID(Unique(imax, rate.high, tick), rate.low, rate.spread, tick);
      if (!bHigh) Mount_BID(Unique(imax, rate.low, tick), rate.high, rate.spread, tick);
      Mount_BID(imax, rate.close, rate.spread, tick);
   }

Hier bieten wir eine Randomisierung des Streuungswertes an, die jedoch nur zu Demonstrationszwecken dient. Wenn Sie es wünschen, können Sie die Grenzwerte ein wenig anders handhaben. Wir müssen nur ein wenig nachbessern. Jetzt sollten Sie verstehen, dass ich diese Zufallsgenerierung verwende, was einigen etwas seltsam vorkommt, aber hier ist, was ich tatsächlich tue: Ich sorge dafür, dass der größtmögliche Wert verwendet werden kann, um die Streuung zu randomisieren. Dieser Wert basiert auf einer Berechnung, bei der wir den Streuwert bitweise mit einem Wert kombinieren, der zwischen 1 und 16 liegen kann, da wir nur einen Teil aller Bits verwenden. Wenn die Streuung Null ist (und an einigen Stellen wird sie tatsächlich Null sein), erhalten wir trotzdem einen Wert, der mindestens 3 ist, da die Werte 1 und 2 keine tatsächliche Randomisierung der Streuung bewirken. Dies liegt daran, dass ein Wert von 1 nur anzeigt, dass der Eröffnungskurs gleich dem Schlusskurs ist, während ein Wert von 2 anzeigt, dass der Eröffnungskurs entweder gleich oder verschieden vom Schlusskurs sein kann. Aber in diesem Fall ist es der Wert 2, der tatsächlich den Wert schafft. In allen anderen Fällen geht es um die Schaffung von Zufälligkeiten in der Verbreitung.

Ich hoffe, es ist jetzt klar, warum ich die Funktion Mount_Bid nicht mit einer Zufallsgenerierung versehen habe. Wenn ich dies täte, gäbe es einige Punkte, an denen die von der Balken-Datei gemeldete Mindestspanne nicht stimmen würde. Aber, wie gesagt, Sie können frei experimentieren und das System an Ihren Geschmack und Stil anpassen.


Schlussfolgerung

In diesem Artikel haben wir die mit der Code-Duplizierung verbundenen Probleme gelöst. Ich denke, es ist jetzt klar, welche Probleme bei der Verwendung von doppeltem Code auftreten. Bei sehr großen Projekten muss man damit immer vorsichtig sein. Selbst dieser Code, der nicht sehr umfangreich ist, kann aufgrund dieser Nachlässigkeit ernsthafte Probleme verursachen.

Ein letztes Detail, das ebenfalls Erwähnung verdient, ist, dass es in einer echten Tickdatei manchmal eine Art „falsche“ Bewegung gibt. Dies ist hier jedoch nicht der Fall; solche „falschen“ Bewegungen treten auf, wenn nur einer der Preise, entweder Bid oder ASK, variiert. Der Einfachheit halber habe ich solche Situationen jedoch außer Acht gelassen. Meiner Meinung nach ist dies für ein System, das den Markt simuliert, nicht sehr sinnvoll. Dies würde keine operativen Verbesserungen bringen. Bei jeder Änderung von Bid ohne Ask müssten wir Ask ohne Bid machen. Dies ist notwendig, um das für den realen Markt erforderliche Gleichgewicht zu erhalten.

Damit ist die Frage der gebotsbasierten Modellierung, zumindest für diesen ersten Versuch, abgeschlossen. In Zukunft werde ich möglicherweise Änderungen an diesem System vornehmen, damit es anders funktioniert. Aber bei der Verwendung mit den Forex-Daten habe ich festgestellt, dass es recht gut funktioniert, obwohl es für andere Märkte vielleicht nicht ausreicht.

Mit der beigefügten Datei erhalten Sie Zugang zu dem System in seinem derzeitigen Entwicklungsstadium. Wie ich jedoch bereits in diesem Artikel sagte, sollten Sie nicht versuchen, die Modellierung mit Börsenwerten durchzuführen, sondern nur mit Deviseninstrumenten. Obwohl Sie alle Instrumente wiederholen können, ist die Simulation für börsengehandelte Vermögenswerte deaktiviert. Im nächsten Artikel werden wir dieses Problem beheben, indem wir das Replay System für Börsen so verbessern, dass es auch in Umgebungen mit geringer Liquidität funktionieren kann. Damit sind unsere Überlegungen zur Simulation abgeschlossen. Wir sehen uns im nächsten Artikel!

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

Beigefügte Dateien |
Market_Replay_7vx23.zip (14388.45 KB)
Entwicklung eines Replay Systems — Marktsimulation (Teil 24): FOREX (V) Entwicklung eines Replay Systems — Marktsimulation (Teil 24): FOREX (V)
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. Das ist gut, denn Änderungen in einem Modus führen automatisch zu Verbesserungen im anderen, vor allem wenn es um die Handhabung der Zeit zwischen den Ticks geht.
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.
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!
Neuronale Netze leicht gemacht (Teil 60): Online Decision Transformer (ODT) Neuronale Netze leicht gemacht (Teil 60): Online Decision Transformer (ODT)
Die letzten beiden Artikel waren der Decision-Transformer-Methode gewidmet, die Handlungssequenzen im Rahmen eines autoregressiven Modells der gewünschten Belohnungen modelliert. In diesem Artikel werden wir uns einen weiteren Optimierungsalgorithmus für diese Methode ansehen.