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

Entwicklung eines Wiedergabesystems — Marktsimulation (Teil 21): FOREX (II)

MetaTrader 5Tester | 4 März 2024, 14:08
127 0
Daniel Jose
Daniel Jose

Einführung

Im vorigen Artikel „Entwicklung eines Wiedergabesystems — Marktsimulation (Teil 20): FOREX (I)“ haben wir begonnen, das Spiel-/Simulationssystem zusammenzustellen oder besser gesagt, anzupassen. Dies geschieht, um die Verwendung von Marktdaten, z.B. FOREX, zu ermöglichen, um zumindest eine Wiedergabe für diesen Markt machen zu können.

Um dieses Potenzial zu entwickeln, war es notwendig, eine Reihe von Änderungen und Anpassungen am System vorzunehmen, aber in diesem Artikel betrachten wir nicht nur den Devisenmarkt. Wie man sieht, konnten wir eine breite Palette möglicher Markttypen abdecken, da wir jetzt nicht nur den zuletzt gehandelten Preis sehen, sondern auch ein BID-basiertes Anzeigesystem verwenden können, das bei einigen spezifischen Arten von Vermögenswerten oder Märkten recht verbreitet ist.

Aber das ist noch nicht alles. Wir haben auch eine Möglichkeit implementiert, um zu verhindern, dass das Wiedergabe-/Simulationssystem „gesperrt“ wird, wenn die Tick-Daten zeigen, dass es für einen Vermögenswert für einen bestimmten Zeitraum keine Transaktionen gab. Das System befand sich in einem zeitlich begrenzten Modus und wurde nach einer bestimmten Zeit zur erneuten Nutzung freigegeben. Zurzeit stoßen wir nicht auf solche Probleme, obwohl es immer noch neue Herausforderungen und Probleme gibt, die auf eine Lösung und damit auf eine weitere Verbesserung unseres Replay/Simulationssystems warten.


Lösung des Problems mit der Konfigurationsdatei

Beginnen wir damit, die Konfigurationsdatei nutzerfreundlicher zu gestalten. Vielleicht haben Sie bemerkt, dass es etwas schwieriger geworden ist, damit zu arbeiten. Ich möchte nicht, dass der Nutzer beim Versuch, eine Wiedergabe oder Simulation einzurichten, verärgert ist, wenn das Diagramm völlig leer bleibt und kein Zugang zu den Kontrollindikatoren besteht, während keine Meldung erscheint, die auf einen Fehler beim Starten des Dienstes hinweist.

Infolgedessen kann beim Versuch, den Wiedergabe-/Simulationsdienst zu nutzen, eine Situation ähnlich der in der Abbildung dargestellten auftreten. 01:

Abbildung 01

Abbildung 01: Ergebnis eines Fehlers in der Boot-Sequenz


Dieser Fehler ist nicht auf einen Missbrauch oder ein falsches Verständnis der Bedienung des Systems zurückzuführen. Es erschien, weil alle Daten, die im nutzerdefinierten Asset platziert wurden, vollständig entfernt wurden. Dies kann entweder durch eine Änderung der Konfiguration des Assets oder durch die Zerstörung der darin enthaltenen Daten geschehen.

Aber in jedem Fall wird dieser Fehler häufiger auftreten, wenn wir Tick-Trades nach dem Laden von 1-Minuten-Bars laden. In diesem Fall versteht das System, dass es die Art und Weise der Preisanzeige ändern muss, und durch diese Änderung werden alle Balken gelöscht, wie im vorherigen Artikel gezeigt.

Um dieses Problem zu lösen, müssen wir zuerst das Laden der Ticks deklarieren, bevor wir die vorherigen Balken laden. Dies löst zwar das Problem, zwingt den Nutzer aber gleichzeitig dazu, sich an eine bestimmte Struktur in der Konfigurationsdatei zu halten, was ich persönlich nicht sehr sinnvoll finde. Der Grund dafür ist, dass wir durch die Entwicklung eines Programms, das für die Analyse und Ausführung der Konfigurationsdatei verantwortlich ist, dem Nutzer erlauben können, Dinge in beliebiger Reihenfolge zu deklarieren, solange eine bestimmte Syntax beachtet wird.

Im Grunde gibt es nur eine Möglichkeit, dieses Problem zu lösen. Die Art und Weise, wie wir diesen Pfad erstellen, erlaubt es uns, leichte Variationen zu erzeugen, aber im Grunde ist es immer das Gleiche. Aber das ist vom Standpunkt der Programmierung aus gesehen. Kurz gesagt, wir müssen die gesamte Konfigurationsdatei lesen und dann auf die erforderlichen Elemente in der Reihenfolge zugreifen, in der sie verfügbar sein sollen, damit alles perfekt funktioniert.

Eine Variante dieser Technik ist die Verwendung der Funktionen FileSeek und FileTell, um in der gewünschten Reihenfolge lesen zu können. Wir können nur einmal auf die Datei zugreifen und dann die gewünschten Elemente direkt im Speicher abrufen, oder wir können die Datei Stück für Stück lesen, um die gleiche Arbeit zu erledigen, wie wenn sie bereits im Speicher wäre. Es gibt aber auch eine andere Möglichkeit.

Ich persönlich ziehe es vor, sie komplett in den Speicher zu laden und dann in Teilen zu lesen, damit ich die spezifische Sequenz habe, die ich brauche, um sie zu laden und nicht das in Abb. 01 gezeigte Ergebnis zu erhalten. Wir werden also die folgende Technik anwenden: Wir werden die Konfigurationsdatei in den Speicher laden und dann die Daten laden, damit das Asset in der Wiedergabe oder Simulation verwendet werden kann.

Um die Möglichkeit zu schaffen, dass die Programmierung eine Sequenz in einer Konfigurationsdatei außer Kraft setzt, müssen wir zunächst eine Reihe von Variablen erstellen. Aber ich werde nicht alles von Grund auf neu programmieren. Stattdessen werde ich die Vorteile der bereits in MQL5 enthaltenen Funktionen nutzen.

Genauer gesagt, wir werden die Standardbibliothek MQL5 verwenden. Nun, wir werden nur einen kleinen Teil dessen verwenden, was in dieser Bibliothek vorgestellt wird. Der große Vorteil dieses Ansatzes besteht darin, dass die darin enthaltenen Funktionen bereits gründlich getestet wurden. Wenn Sie irgendwann in der Zukunft Verbesserungen an Ihrem Programm vornehmen, egal welche, wird das System diese automatisch übernehmen. Auf diese Weise wird der Programmierprozess viel einfacher, da wir weniger Zeit für die Test- und Optimierungsphase benötigen.

Jetzt wollen wir sehen, was wir brauchen. Dies wird im folgenden Code gezeigt:

#include "C_FileBars.mqh"
#include "C_FileTicks.mqh"
//+------------------------------------------------------------------+
#include <Arrays\ArrayString.mqh>
//+------------------------------------------------------------------+
class C_ConfigService : protected C_FileTicks
{
// ... Internal class code ....
}

Hier werden wir die Include-Datei verwenden, die in MQL5 enthalten ist. Das ist für unsere Zwecke völlig ausreichend.

Danach werden wir private globale Variablen für die Klasse benötigen:

    private :
//+------------------------------------------------------------------+
        enum eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE};
        string m_szPath;
        struct st001
        {
            CArrayString *pTicksToReplay, *pBarsToTicks, *pTicksToBars, *pBarsToPrev;
        }m_GlPrivate;

Diese Variablen, die sich innerhalb der Struktur befinden, sind eigentlich Zeiger auf die Klasse, die wir verwenden werden. Es ist wichtig, darauf hinzuweisen, dass sich die Arbeit mit Zeigern von der Arbeit mit Variablen unterscheidet, da Zeiger Strukturen sind, die auf einen Speicherplatz, genauer gesagt eine Adresse, zeigen, während Variablen nur Werte enthalten.

Ein wichtiges Detail in dieser ganzen Geschichte ist, dass MQL5 standardmäßig und zur Erleichterung vieler Programmierer (insbesondere derer, die gerade erst mit dem Programmieren anfangen) nicht genau das gleiche Konzept von Zeigern wie in C/C++ verwendet. Wer schon einmal in C/C++ programmiert hat, weiß, wie nützlich, aber gleichzeitig auch gefährlich und verwirrend Zeiger sein können. In MQL5 haben die Entwickler jedoch versucht, einen Großteil der mit der Verwendung von Zeigern verbundenen Verwirrung und Gefahren zu beseitigen. Für praktische Zwecke ist zu beachten, dass wir tatsächlich Zeiger für den Zugriff auf die Klasse CArrayString verwenden werden.

Wenn Sie mehr über die Methoden der Klasse CArrayString wissen möchten, lesen Sie bitte die Dokumentation der Klasse. Klicken Sie einfach auf CArrayString und sehen Sie, was bereits für unsere Zwecke zur Verfügung steht. Sie ist für unsere Zwecke gut geeignet.

Da wir für den Zugriff auf Objekte Zeiger verwenden, müssen diese zuerst initialisiert werden, was wir im folgenden Code getan haben.

bool SetSymbolReplay(const string szFileConfig)
   {

// ... Internal code ...

      m_GlPrivate.pTicksToReplay = m_GlPrivate.pTicksToBars = m_GlPrivate.pBarsToTicks = m_GlPrivate.pBarsToPrev = NULL;
// ... The rest of the code ...

   }

Der vielleicht größte Fehler, den Programmierer bei der Verwendung von Zeigern, insbesondere in C/C++, machen, ist, dass sie versuchen, die Zeigerdaten zu verwenden, bevor sie initialisiert wurden. Die Gefahr bei Zeigern ist, dass sie nie auf einen Speicherbereich zeigen, den wir verwenden können, bevor sie initialisiert sind. Der Versuch, in einem unbekannten Bereich des Gedächtnisses zu lesen und vor allem zu schreiben, bedeutet höchstwahrscheinlich, dass wir an einer kritischen Stelle schreiben oder die falschen Informationen lesen. In diesem Fall stürzt das System häufig ab, was zu verschiedenen Problemen führt. Seien Sie daher sehr vorsichtig bei der Verwendung von Zeigern in Ihren Programmen.

Mit allen Vorsichtsmaßnahmen im Hinterkopf, lassen Sie uns sehen, wie es in unserer Replay/Simulations-System-Konfigurationsfunktion geht. Die gesamte Funktion ist im folgenden Code dargestellt:

bool SetSymbolReplay(const string szFileConfig)
   {
#define macroFileName ((m_szPath != NULL ? m_szPath + "\\" : "") + szInfo)
      int    file,
             iLine;
      char   cError,
             cStage;

          string szInfo;

          bool   bBarsPrev;
                                                                

          if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
      {

             Print("Failed to open the configuration file [", szFileConfig, "]. Closing the service...");

             return false;
      }

          Print("Loading ticks for replay. Please wait....");

          ArrayResize(m_Ticks.Rate, def_BarsDiary);
      m_Ticks.nRate = -1;
      m_Ticks.Rate[0].time = 0;
      iLine = 1;
      cError = cStage = 0;
      bBarsPrev = false;
      m_GlPrivate.pTicksToReplay = m_GlPrivate.pTicksToBars = m_GlPrivate.pBarsToTicks = m_GlPrivate.pBarsToPrev = NULL;

          while ((!FileIsEnding(file)) && (!_StopFlag) && (cError == 0))
      {

             switch (GetDefinition(FileReadString(file), szInfo))
         {

                case Transcription_DEFINE:
               cError = (WhatDefine(szInfo, cStage) ? 0 : 1);

                   break;

                case Transcription_INFO:

                   if (szInfo != "") switch (cStage)
               {

                      case 0:
                     cError = 2;

                         break;

                      case 1:

                         if (m_GlPrivate.pBarsToPrev == NULL) m_GlPrivate.pBarsToPrev = new CArrayString();
                     (*m_GlPrivate.pBarsToPrev).Add(macroFileName);
                     pFileBars = new C_FileBars(macroFileName);
                     if ((m_dtPrevLoading = (*pFileBars).LoadPreView()) == 0) cError = 3; else bBarsPrev = true;
                     delete pFileBars;
                     break;
                  case 2:
                     if (m_GlPrivate.pTicksToReplay == NULL) m_GlPrivate.pTicksToReplay = new CArrayString();
                     (*m_GlPrivate.pTicksToReplay).Add(macroFileName);
                     if (LoadTicks(macroFileName) == 0) cError = 4;
                     break;
                  case 3:
                     if (m_GlPrivate.pTicksToBars == NULL) m_GlPrivate.pTicksToBars = new CArrayString();
                     (*m_GlPrivate.pTicksToBars).Add(macroFileName);
                     if ((m_dtPrevLoading = LoadTicks(macroFileName, false)) == 0) cError = 5; else bBarsPrev = true;
                     break;
                  case 4:
                     if (m_GlPrivate.pBarsToTicks == NULL) m_GlPrivate.pBarsToTicks = new CArrayString();
                     (*m_GlPrivate.pBarsToTicks).Add(macroFileName);
                     if (!BarsToTicks(macroFileName)) cError = 6;
                     break;
                  case 5:
                     if (!Configs(szInfo)) cError = 7;
                     break;
               }
               break;
            };
            iLine += (cError > 0 ? 0 : 1);
         }
         FileClose(file);
         Cmd_TicksToReplay(cError);
         Cmd_BarsToTicks(cError);
         bBarsPrev = (Cmd_TicksToBars(cError) ? true : bBarsPrev);
         bBarsPrev = (Cmd_BarsToPrev(cError) ? true : bBarsPrev);
         switch(cError)
         {
            case 0:
               if (m_Ticks.nTicks <= 0)
               {
                  Print("No ticks to use. Closing the service...");
                  cError = -1;
               }else if (!bBarsPrev) FirstBarNULL();
               break;
            case 1  : Print("Command in line ", iLine, " could not be recognized by the system...");    break;
            case 2  : Print("The contents of the line are unexpected for the system ", iLine);                  break;
            default : Print("Error occurred while accessing one of the specified files...");
         }
                                              
         return (cError == 0 ? !_StopFlag : false);
#undef macroFileName
      }

Die durchgestrichenen Teile des vorherigen Codes wurden entfernt, da wir sie hier nicht mehr benötigen. Sie wurden an einem anderen Ort untergebracht. Doch bevor Sie sich den neuen Standort ansehen, sollten Sie sich ansehen, wie der Code in diesem Stadium funktioniert.

Wie Sie sehen, gibt es hier sich wiederholenden Code, obwohl wir immer auf verschiedene Zeiger zugreifen werden.

Dieser Code ist für den new-Operator bestimmt, um einen Speicherbereich zu erstellen, in dem die Klasse existieren wird. Gleichzeitig initialisiert dieser Operator die Klasse. Da es keinen Initialisierungskonstruktor gibt, wird die Klasse in allen Fällen mit Standardwerten erstellt und initialisiert.

Nachdem die Klasse initialisiert wurde, ist der Zeigerwert nicht mehr NULL. Der Zeiger verweist nur auf das erste Element, das wie eine Liste aussehen würde, wenn es sich nicht um ein Array handeln würde. Nun, da der Zeiger auf die richtige Stelle im Speicher zeigt, können wir eine der Klassenmethoden verwenden, um eine Zeichenkette daran anzuhängen.

Dies geschieht auf nahezu transparente Weise. Wir müssen nicht wissen, wo und wie die Informationen, die wir hinzufügen, in der Klasse organisiert sind. Wichtig ist, dass die Klasse jedes Mal, wenn wir die Methode aufrufen, Daten für uns speichert.

Bitte beachten Sie, dass wir die Dateien, die in der Konfigurationsdatei aufgeführt werden, nie referenziert haben. Beim Lesen in Echtzeit werden lediglich die grundlegenden Betriebsinformationen eingestellt. Das Lesen der Daten, ob Histogramme oder Tickdaten, wird später erfolgen.

Sobald das System das Lesen der Konfigurationsdatei beendet hat, gehen wir zum nächsten Schritt über. Jetzt kommt die wichtigste Frage, über die Sie unbedingt nachdenken sollten, wenn Sie dieses System hinzufügen oder verwenden. Achten Sie also genau darauf, was ich Ihnen jetzt erkläre, denn es könnte für Ihr spezielles System wichtig sein. Für uns ist das nicht wichtig.

Hier haben wir 4 Funktionen, und die Reihenfolge, in der sie erscheinen, wirkt sich auf das Endergebnis aus. Kümmern Sie sich vorerst nicht um den Code der einzelnen Funktionen. Hier ist es notwendig, die Reihenfolge ihres Auftretens im Code zu berücksichtigen, denn je nach Reihenfolge wird das System das Diagramm auf die eine oder andere Weise anzeigen. Das mag Ihnen verwirrend erscheinen, aber wir wollen versuchen zu verstehen, wie sich die Situation entwickeln wird. Wir müssen wissen, welche Reihenfolge je nach Art der Datenbank, die wir verwenden wollen, am besten geeignet ist.

In der im Code verwendeten Reihenfolge führt das System die folgenden Aktionen durch. Zunächst werden die tatsächlichen Datenticks gelesen. Anschließend werden die Daten mithilfe eines Modellierungsverfahrens in Ticks umgewandelt. Dann werden Balken erstellt, wobei die anfängliche Umwandlung von Ticks in Balken beibehalten wird, und schließlich werden 1-Minuten-Balken geladen.

Die Daten werden in genau der oben beschriebenen Reihenfolge gelesen. Wenn wir diese Reihenfolge ändern wollen, müssen wir die Reihenfolge ändern, in der diese Funktionen aufgerufen werden. Sie sollten jedoch keine vorherigen Balken lesen, bevor Sie die Ticks lesen, die wir modellieren werden. Dies ist darauf zurückzuführen, dass beim Lesen von Ticks für die Wiedergabe oder von Balken für die Verwendung in einem Tester alles, was sich auf dem Diagramm befindet, entfernt wird. Dies hängt mit den Details zusammen, die im vorherigen Artikel besprochen wurden. Die Tatsache, dass die Reihenfolge der Verwendung oder des Auslesens im System und nicht in der Konfigurationsdatei festgelegt wird, ermöglicht es jedoch, Balken vor Ticks zu deklarieren und gleichzeitig dem System zu erlauben, mit möglichen Problemen fertig zu werden.

Doch so einfach ist die Sache nicht. So erlauben wir dem System, die Reihenfolge der Ereignisse zu bestimmen, oder besser gesagt, die Reihenfolge, in der sie gelesen werden. Die Art und Weise, wie alles abläuft, wenn wir alle Daten im Speicher lesen und dann interpretieren, macht es dem System jedoch schwer, die genaue Zeile zu melden, in der das Lesen fehlgeschlagen ist. Wir haben noch den Namen der Datei, auf deren Daten nicht zugegriffen werden konnte.

Um diese Unannehmlichkeit zu beheben, dass das System nicht in der Lage ist, die genaue Zeile zu melden, in der der Fehler aufgetreten ist, müssen wir eine kleine und recht einfache Ergänzung des Codes vornehmen. Vergessen Sie nicht: Wir wollen die Funktionalität unseres Systems nicht einschränken. Stattdessen wollen wir sie aufstocken und ausweiten, um so viele Fälle wie möglich zu erfassen.

Bevor wir die Lösung umsetzen, möchte ich, dass Sie verstehen, warum wir diese Entscheidung getroffen haben.

Um dieses Definitionsproblem zu lösen, müssen wir während der Ladephase sowohl den Namen der Datei als auch die Zeile, in der sie deklariert wurde, speichern. Diese Lösung ist eigentlich ganz einfach. Alles, was wir tun müssen, ist, einen weiteren Satz von Variablen mit der Klasse CArrayInt zu deklarieren. Diese Klasse speichert Zeichenketten, die den einzelnen Dateinamen entsprechen.

Auf den ersten Blick scheint dies eine gute Lösung zu sein, da wir die Standardbibliothek verwenden, aber es ist ein bisschen teuer, weil es uns zwingt, viel mehr Variablen hinzuzufügen, als es notwendig wäre, wenn wir unsere eigene Lösung entwickeln würden. Wir werden das gleiche Prinzip wie die Klassen in der Standardbibliothek verwenden, jedoch mit der Möglichkeit, gleichzeitig mit einer großen Menge an Daten zu arbeiten.

Man könnte meinen, dass wir auf diese Weise unser Projekt verkomplizieren, weil wir eine bereits getestete und implementierte Lösung verwenden können, die in der Standardbibliothek verfügbar ist, und ich persönlich bin in vielen Fällen mit dieser Idee einverstanden. Aber hier ist es nicht sehr sinnvoll, da wir nicht alle diese in der Standardbibliothek verfügbaren Methoden benötigen. Sie brauchen nur zwei davon. Somit gleichen die Kosten für die Umsetzung die zusätzlichen Arbeitskosten aus. So erscheint eine neue Klasse in unserem Projekt: C_Array.

Der vollständige Code ist nachstehend aufgeführt:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
class C_Array
{
    private :
//+------------------------------------------------------------------+
        string  m_Info[];
        int     m_nLine[];
        int     m_maxIndex;
//+------------------------------------------------------------------+
    public  :
        C_Array()
            :m_maxIndex(0)
        {};
//+------------------------------------------------------------------+
        ~C_Array()
        {
            if (m_maxIndex > 0)
            {
                ArrayResize(m_nLine, 0);
                ArrayResize(m_Info, 0);
            }
        };
//+------------------------------------------------------------------+
        bool Add(const string Info, const int nLine)
        {
            m_maxIndex++;
            ArrayResize(m_Info, m_maxIndex);
            ArrayResize(m_nLine, m_maxIndex);
            m_Info[m_maxIndex - 1] = Info;
            m_nLine[m_maxIndex - 1] = nLine;

            return true;
        }
//+------------------------------------------------------------------+
        string At(const int Index, int &nLine) const
        {
            if (Index >= m_maxIndex)
            {
                nLine = -1;
                return "";
            }
            nLine = m_nLine[Index];
            return m_Info[Index];
        }
//+------------------------------------------------------------------+
};
//+------------------------------------------------------------------+

Achten Sie darauf, wie einfach und kompakt es ist, aber gleichzeitig erfüllt es perfekt den Zweck, den wir brauchen. Mit dieser Klasse können wir sowohl die Zeile als auch den Namen der Datei speichern, die die für die Wiedergabe oder Simulation benötigten Informationen enthält. Alle diese Daten werden in der Konfigurationsdatei angegeben. Indem wir diese Klasse zu unserem Projekt hinzufügen, erhalten wir das gleiche Maß an Funktionalität, oder besser gesagt, wir erhöhen es, da wir nun auch Daten von einem Markt wiedergeben können, der dem Devisenmarkt ähnlich ist.

Am Code der Klasse, die für das Einrichten der Wiedergabe/Simulation zuständig ist, müssen einige Änderungen vorgenommen werden. Diese Änderungen beginnen im folgenden Code:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_FileBars.mqh"
#include "C_FileTicks.mqh"
#include "C_Array.mqh"
//+------------------------------------------------------------------+
class C_ConfigService : protected C_FileTicks
{
        protected:
//+------------------------------------------------------------------+
                datetime m_dtPrevLoading;
                int      m_ReplayCount;
//+------------------------------------------------------------------+
inline void FirstBarNULL(void)
                {
                        MqlRates rate[1];

                        for(int c0 = 0; m_Ticks.Info[c0].volume_real == 0; c0++)
                                rate[0].close = m_Ticks.Info[c0].last;
                        rate[0].open = rate[0].high = rate[0].low = rate[0].close;
                        rate[0].tick_volume = 0;
                        rate[0].real_volume = 0;
                        rate[0].time = m_Ticks.Info[0].time - 60;
                        CustomRatesUpdate(def_SymbolReplay, rate);
                        m_ReplayCount = 0;
                }
//+------------------------------------------------------------------+
        private :
//+------------------------------------------------------------------+
                enum eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE};
                string m_szPath;
                struct st001
                {
                        C_Array *pTicksToReplay, *pBarsToTicks, *pTicksToBars, *pBarsToPrev;
                        int     Line;
                }m_GlPrivate;

Es war notwendig, nur diese Punkte in die Datei aufzunehmen. Außerdem müssen wir Korrekturen an der Konfigurationsfunktion vornehmen. Es wird so aussehen:

bool SetSymbolReplay(const string szFileConfig)
    {
#define macroFileName ((m_szPath != NULL ? m_szPath + "\\" : "") + szInfo)
        int     file;
        char    cError,
                cStage;
        string  szInfo;
        bool    bBarsPrev;

        if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
        {
            Print("Failed to open configuration file [", szFileConfig, "]. Closing the service...");
            return false;
        }
        Print("Loading ticks for replay. Please wait....");
        ArrayResize(m_Ticks.Rate, def_BarsDiary);
        m_Ticks.nRate = -1;
        m_Ticks.Rate[0].time = 0;
        cError = cStage = 0;
        bBarsPrev = false;
        m_GlPrivate.Line = 1;
        m_GlPrivate.pTicksToReplay = m_GlPrivate.pTicksToBars = m_GlPrivate.pBarsToTicks = m_GlPrivate.pBarsToPrev = NULL;
        while ((!FileIsEnding(file)) && (!_StopFlag) && (cError == 0))
        {
            switch (GetDefinition(FileReadString(file), szInfo))
            {
                case Transcription_DEFINE:
                    cError = (WhatDefine(szInfo, cStage) ? 0 : 1);
                    break;
                case Transcription_INFO:
                if (szInfo != "") switch (cStage)
                {
                    case 0:
                        cError = 2;
                        break;
                    case 1:
                        if (m_GlPrivate.pBarsToPrev == NULL) m_GlPrivate.pBarsToPrev = new C_Array();
                        (*m_GlPrivate.pBarsToPrev).Add(macroFileName, m_GlPrivate.Line);
                        break;
                    case 2:
                        if (m_GlPrivate.pTicksToReplay == NULL) m_GlPrivate.pTicksToReplay = new C_Array();
                        (*m_GlPrivate.pTicksToReplay).Add(macroFileName, m_GlPrivate.Line);
                        break;
                    case 3:
                        if (m_GlPrivate.pTicksToBars == NULL) m_GlPrivate.pTicksToBars = new C_Array();
                        (*m_GlPrivate.pTicksToBars).Add(macroFileName, m_GlPrivate.Line);
                        break;
                    case 4:
                        if (m_GlPrivate.pBarsToTicks == NULL) m_GlPrivate.pBarsToTicks = new C_Array();
                        (*m_GlPrivate.pBarsToTicks).Add(macroFileName, m_GlPrivate.Line);
                        break;
                    case 5:
                        if (!Configs(szInfo)) cError = 7;
                        break;
                }
                break;
            };
            m_GlPrivate.Line += (cError > 0 ? 0 : 1);
        }
        FileClose(file);
        Cmd_TicksToReplay(cError);
        Cmd_BarsToTicks(cError);
        bBarsPrev = (Cmd_TicksToBars(cError) ? true : bBarsPrev);
        bBarsPrev = (Cmd_BarsToPrev(cError) ? true : bBarsPrev);
        switch(cError)
        {
            case 0:
                if (m_Ticks.nTicks <= 0)
                {
                    Print("No ticks to use. Closing the service...");
                    cError = -1;
                }else if (!bBarsPrev) FirstBarNULL();
                break;
            case 1  : Print("Command in line ", m_GlPrivate.Line, " could not be recognized by the system..."); break;
            case 2  : Print("The contents of the line is unexpected for the system: ", m_GlPrivate.Line);              break;
            default : Print("Access error occurred in the files indicated in line: ", m_GlPrivate.Line);
        }
        
        return (cError == 0 ? !_StopFlag : false);
#undef macroFileName

    }

Jetzt können wir sehen, dass wir dem Nutzer mitteilen können, dass ein Fehler aufgetreten ist und in welcher Zeile. Die Kosten dafür sind praktisch gleich Null, denn wie Sie sehen, bleiben die Funktionen fast dieselben wie vorher, nur dass ein neues Element hinzugefügt wurde. Das ist sehr gut, wenn man bedenkt, wie viel Arbeit wir zu tun hätten, wenn wir die Standardbibliothek verwenden würden. Bitte verstehen Sie mich richtig: Ich sage nicht, dass Sie die Standardbibliothek nicht verwenden sollten, ich zeige nur, dass es Zeiten gibt, in denen Sie Ihre eigene Lösung erstellen müssen, weil in diesen Fällen die Kosten die Arbeit überwiegen.

Wir können nun die neuen Funktionen betrachten, die dem System hinzugefügt wurden, nämlich die vier oben gezeigten Funktionen. Sie sind alle recht einfach, ihre Codes werden unten gezeigt und, da sie alle nach dem gleichen Prinzip funktionieren, werde ich sie gleich erklären, um den Prozess nicht zu langwierig zu machen.

inline void Cmd_TicksToReplay(char &cError)
    {
        string szInfo;

        if (m_GlPrivate.pTicksToReplay != NULL)
        {
            for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++)
            {
                if ((szInfo = (*m_GlPrivate.pTicksToReplay).At(c0, m_GlPrivate.Line)) == "") break;
                if (LoadTicks(szInfo) == 0) cError = 4;                                         
            }
            delete m_GlPrivate.pTicksToReplay;
        }
    }
//+------------------------------------------------------------------+
inline void Cmd_BarsToTicks(char &cError)
    {
        string szInfo;

        if (m_GlPrivate.pBarsToTicks != NULL)
        {
            for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++)
            {
                if ((szInfo = (*m_GlPrivate.pBarsToTicks).At(c0, m_GlPrivate.Line)) == "") break;
                if (!BarsToTicks(szInfo)) cError = 6;
            }
            delete m_GlPrivate.pBarsToTicks;
        }
    }
//+------------------------------------------------------------------+
inline bool Cmd_TicksToBars(char &cError)
    {
        bool bBarsPrev = false;
        string szInfo;

        if (m_GlPrivate.pTicksToBars != NULL)
        {
            for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++)
            {
                if ((szInfo = (*m_GlPrivate.pTicksToBars).At(c0, m_GlPrivate.Line)) == "") break;
                if ((m_dtPrevLoading = LoadTicks(szInfo, false)) == 0) cError = 5; else bBarsPrev = true;
            }
            delete m_GlPrivate.pTicksToBars;
        }
        return bBarsPrev;
    }
//+------------------------------------------------------------------+
inline bool Cmd_BarsToPrev(char &cError)
    {
        bool bBarsPrev = false;
        string szInfo;
        C_FileBars      *pFileBars;

        if (m_GlPrivate.pBarsToPrev != NULL)
        {
            for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++)
            {
                if ((szInfo = (*m_GlPrivate.pBarsToPrev).At(c0, m_GlPrivate.Line)) == "") break;
                pFileBars = new C_FileBars(szInfo);
                if ((m_dtPrevLoading = (*pFileBars).LoadPreView()) == 0) cError = 3; else bBarsPrev = true;
                delete pFileBars;
            }
            delete m_GlPrivate.pBarsToPrev;
        }

        return bBarsPrev;
    }
//+------------------------------------------------------------------+

Aber warten Sie..., schauen Sie sich alle vier Funktionen oben genau an..., es gibt eine Menge sich wiederholenden Code... Was tun wir, wenn wir eine Menge sich wiederholenden Code haben? Wir versuchen, die Funktion auf eine einfachere Ebene zu bringen, um die Wiederverwendung von Code zu ermöglichen. Ich weiß, das mag sich dumm oder nachlässig anhören, aber in dieser Artikelserie zeige ich nicht nur, wie Programme von Grund auf neu erstellt werden, sondern auch, wie gute Programmierer ihre Programme verbessern, indem sie den Wartungsaufwand reduzieren und sie erstellen.

Es geht nicht darum, wie viele Jahre Programmiererfahrung Sie haben, sondern darum, dass Sie, auch wenn Sie gerade erst in die Welt der Programmierung einsteigen, verstehen, dass Sie oft Funktionen verbessern und Code reduzieren können und damit die Entwicklung einfacher und schneller machen. Die Optimierung von Code auf Wiederverwendbarkeit mag zunächst wie Zeitverschwendung erscheinen. Es wird uns jedoch bei der Entwicklung helfen, denn wenn wir etwas verbessern müssen, haben wir weniger Dinge, die wir an mehreren Stellen wiederholen müssen. Stellen Sie sich diese Frage beim Programmieren: Kann ich die Menge des Codes, der zur Ausführung dieser Aufgabe verwendet wird, reduzieren?

Das ist genau das, was die folgende Funktion tut: Sie ersetzt die vorherigen 4 Funktionen, sodass wir, wenn wir das System verbessern müssen, nur eine Funktion ändern müssen, nicht vier.

inline bool CMD_Array(char &cError, eWhatExec e1)
    {
        bool        bBarsPrev = false;
        string      szInfo;
        C_FileBars    *pFileBars;
        C_Array     *ptr = NULL;

        switch (e1)
        {
            case eTickReplay: ptr = m_GlPrivate.pTicksToReplay; break;
            case eTickToBar : ptr = m_GlPrivate.pTicksToBars;   break;
            case eBarToTick : ptr = m_GlPrivate.pBarsToTicks;   break;
            case eBarPrev   : ptr = m_GlPrivate.pBarsToPrev;    break;
        }                               
        if (ptr != NULL)
        {
            for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++)
            {
                if ((szInfo = ptr.At(c0, m_GlPrivate.Line)) == "") break;
                switch (e1)
                {
                    case eTickReplay:
                        if (LoadTicks(szInfo) == 0) cError = 4;
                        break;
                    case eTickToBar :
                        if ((m_dtPrevLoading = LoadTicks(szInfo, false)) == 0) cError = 5; else bBarsPrev = true;
                        break;
                    case eBarToTick :
                        if (!BarsToTicks(szInfo)) cError = 6;
                        break;
                    case eBarPrev   :
                        pFileBars = new C_FileBars(szInfo);
                        if ((m_dtPrevLoading = (*pFileBars).LoadPreView()) == 0) cError = 3; else bBarsPrev = true;
                        delete pFileBars;
                        break;
                }
            }
            delete ptr;
        }

        return bBarsPrev;
    }

Es ist sehr wichtig zu beachten, dass diese Funktion eigentlich in zwei Segmente unterteilt ist, obwohl sie innerhalb derselben Funktion liegt. Im ersten Segment wird ein Zeiger initialisiert, der für den Zugriff auf die beim Lesen der Konfigurationsdatei gespeicherten Daten verwendet wird. Dieser Abschnitt ist recht einfach und ich glaube nicht, dass jemand Schwierigkeiten haben wird, ihn zu verstehen. Danach gehen wir zum zweiten Segment über, in dem wir den Inhalt von nutzerdefinierten Dateien lesen werden. In diesem Fall liest jeder der Zeiger den im Speicher abgelegten Inhalt und erledigt seine Aufgabe. Am Ende wird der Zeiger zerstört, wodurch der verwendete Speicher freigegeben wird.

Aber bevor wir auf die Einrichtungsfunktion zurückkommen, möchte ich Ihnen zeigen, dass Sie mit diesem System noch mehr machen können. Zu Beginn dieses Themas habe ich gesagt, dass man ein Programm kompilieren muss, wenn man darüber nachdenkt, wie das Lesen abläuft, aber im Laufe der Arbeit an diesem Artikel habe ich darüber nachgedacht und beschlossen, dass es möglich ist, den Umfang zu erweitern, sodass man das Programm nicht ständig neu kompilieren muss.

Die Idee ist, zunächst die simulierten Daten zu laden und dann bei Bedarf frühere Balken zu laden. Aber wir haben ein Problem: Sowohl die Ticks, die in der Wiedergabe/Simulation verwendet werden, als auch die vorherigen Takte können aus Dateien vom Typ „Tick“ oder „Bar“ stammen, aber wir müssen dies irgendwie angeben. Ich schlage daher vor, eine Variable zu verwenden, die der Nutzer definieren kann. Lassen Sie uns sehr genau sein, um die Dinge einfach zu halten und auf lange Sicht lebensfähig zu machen. Verwenden wir dazu die folgende Tabelle:

Wert Lese-Modus
1 Tick - Tick-Modus
2 Tick - Balkenmodus
3 Balken - Tick-Modus 
4 Balken - Balken-Modus 

Tabelle 01 - Daten für das interne Lesemodell

Die obige Tabelle zeigt, wie das Lesen erfolgt. Wir beginnen immer mit dem Lesen der Daten, die in der Wiedergabe oder Simulation verwendet werden sollen, und lesen dann den Wert, der als vorherige Daten im Diagramm verwendet werden soll. Wenn der Nutzer dann den Wert 1 meldet, funktioniert das System wie folgt: Datei mit echten Ticks - Datei mit in Ticks konvertierten Balken - Datei mit in vorherige Daten konvertierten Ticks - Datei mit vorherigen Balken. Dies wäre Option 1, aber nehmen wir an, dass wir das System aus irgendeinem Grund so ändern wollen, dass es dieselbe Datenbank verwendet, aber eine andere Art von Analyse durchführt. In diesem Fall können Sie z. B. den Wert 4 melden, und das System wird dieselbe Datenbank verwenden, aber das Ergebnis wird etwas anders sein, da das Lesen in der folgenden Reihenfolge erfolgt: Datei mit in Ticks umgewandelten Balken - Datei mit echten Ticks - Datei mit früheren Balken - Datei mit in frühere Daten umgewandelten Ticks... Wenn wir dies ausprobieren, werden wir sehen, dass Indikatoren, wie z. B. der Durchschnitt oder andere, kleine Änderungen von einem Modus zum anderen erzeugen, selbst wenn wir dieselbe Datenbank verwenden.

Diese Implementierung erfordert nur einen minimalen Programmieraufwand, daher halte ich es für sinnvoll, eine solche Ressource anzubieten.

Sehen wir uns nun an, wie ein solches System implementiert werden kann. Zunächst fügen wir der Klasse eine globale, aber private Variable hinzu, damit wir weiterarbeiten können.

class C_ConfigService : protected C_FileTicks
{

       protected:
//+------------------------------------------------------------------+

          datetime m_dtPrevLoading;

          int      m_ReplayCount,
               m_ModelLoading;

In dieser Variablen wird das Modell gespeichert. Für den Fall, dass der Nutzer sie nicht in der Konfigurationsdatei definiert, setzen wir einen Anfangswert für sie.

C_ConfigService()
   :m_szPath(NULL), m_ModelLoading(1)
   {
   }

Mit anderen Worten, wir werden standardmäßig immer im Tick-Tick-Modus laufen und arbeiten. Danach müssen Sie dem Nutzer die Möglichkeit geben, den zu verwendenden Wert anzugeben. Auch das ist einfach:

inline bool Configs(const string szInfo)
    {
        const string szList[] = 
        {
            "PATH",
            "POINTSPERTICK",
            "VALUEPERPOINTS",
            "VOLUMEMINIMAL",
            "LOADMODEL"
        };
        string  szRet[];
        char    cWho;

        if (StringSplit(szInfo, '=', szRet) == 2)
        {
            StringTrimRight(szRet[0]);
            StringTrimLeft(szRet[1]);
            for (cWho = 0; cWho < ArraySize(szList); cWho++) if (szList[cWho] == szRet[0]) break;
            switch (cWho)
            {
                case 0:
                    m_szPath = szRet[1];
                    return true;
                case 1:
                    CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, StringToDouble(szRet[1]));
                    return true;
                case 2:
                    CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, StringToDouble(szRet[1]));
                    return true;
                case 3:
                    CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, StringToDouble(szRet[1]));
                    return true;
                case 4:
                    m_ModelLoading = StringInit(szRet[1]);
                    m_ModelLoading = ((m_ModelLoading < 1) && (m_ModelLoading > 4) ? 1 : m_ModelLoading);
                    return true;                            
            }
            Print("Variable >>", szRet[0], "<< undefined.");
        }else
        Print("Set-up configuration >>", szInfo, "<< invalid.");

        return false;
    }

Hier geben wir den Namen an, den der Nutzer bei der Arbeit mit der Variablen verwenden wird, und wir können auch den zu verwendenden Wert festlegen, wie in Tabelle 01 gezeigt. Hier gibt es einen kleinen Punkt: Wir müssen sicherstellen, dass der Wert innerhalb der vom System erwarteten Grenzen liegt. Wenn der Nutzer einen anderen als den erwarteten Wert angibt, tritt kein Fehler auf, sondern das System verwendet den Standardwert.

Nachdem dies alles erledigt ist, können wir sehen, wie die endgültige Lade- und Einrichtungsfunktion aussieht. Es ist unten aufgeführt:

bool SetSymbolReplay(const string szFileConfig)
    {
#define macroFileName ((m_szPath != NULL ? m_szPath + "\\" : "") + szInfo)
        int     file;
        char    cError,
                cStage;
        string  szInfo;
        bool    bBarsPrev;

        if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
        {
            Print("Failed to open configuration file [", szFileConfig, "]. Closing the service...");
            return false;
        }
        Print("Loading ticks for replay. Please wait....");
        ArrayResize(m_Ticks.Rate, def_BarsDiary);
        m_Ticks.nRate = -1;
        m_Ticks.Rate[0].time = 0;
        cError = cStage = 0;
        bBarsPrev = false;
        m_GlPrivate.Line = 1;
        m_GlPrivate.pTicksToReplay = m_GlPrivate.pTicksToBars = m_GlPrivate.pBarsToTicks = m_GlPrivate.pBarsToPrev = NULL;
        while ((!FileIsEnding(file)) && (!_StopFlag) && (cError == 0))
        {
            switch (GetDefinition(FileReadString(file), szInfo))
            {
                case Transcription_DEFINE:
                    cError = (WhatDefine(szInfo, cStage) ? 0 : 1);
                    break;
                case Transcription_INFO:
                    if (szInfo != "") switch (cStage)
                    {
                        case 0:
                            cError = 2;
                            break;
                        case 1:
                            if (m_GlPrivate.pBarsToPrev == NULL) m_GlPrivate.pBarsToPrev = new C_Array();
                            (*m_GlPrivate.pBarsToPrev).Add(macroFileName, m_GlPrivate.Line);
                            break;
                        case 2:
                            if (m_GlPrivate.pTicksToReplay == NULL) m_GlPrivate.pTicksToReplay = new C_Array();
                            (*m_GlPrivate.pTicksToReplay).Add(macroFileName, m_GlPrivate.Line);
                            break;
                        case 3:
                            if (m_GlPrivate.pTicksToBars == NULL) m_GlPrivate.pTicksToBars = new C_Array();
                            (*m_GlPrivate.pTicksToBars).Add(macroFileName, m_GlPrivate.Line);
                            break;
                        case 4:
                            if (m_GlPrivate.pBarsToTicks == NULL) m_GlPrivate.pBarsToTicks = new C_Array();
                            (*m_GlPrivate.pBarsToTicks).Add(macroFileName, m_GlPrivate.Line);
                            break;
                        case 5:
                            if (!Configs(szInfo)) cError = 7;
                            break;
                    }
                break;
            };
            m_GlPrivate.Line += (cError > 0 ? 0 : 1);
        }
        FileClose(file);
        CMD_Array(cError, (m_ModelLoading <= 2 ? eTickReplay : eBarToTick));
        CMD_Array(cError, (m_ModelLoading <= 2 ? eBarToTick : eTickReplay));
        bBarsPrev = (CMD_Array(cError, ((m_ModelLoading & 1) == 1 ? eTickToBar : eBarPrev)) ? true : bBarsPrev);
        bBarsPrev = (CMD_Array(cError, ((m_ModelLoading & 1) == 1 ? eBarPrev : eTickToBar)) ? true : bBarsPrev);
        switch(cError)
        {
            case 0:
                if (m_Ticks.nTicks <= 0)
                {
                    Print("No ticks to use. Closing the service...");
                    cError = -1;
                }else if (!bBarsPrev) FirstBarNULL();
                break;
            case 1  : Print("Command in line ", m_GlPrivate.Line, " could not be recognized by the system..."); break;
            case 2  : Print("The contents of the line is unexpected for the system: ", m_GlPrivate.Line);              break;
            default : Print("Access error occurred in the files indicated in line: ", m_GlPrivate.Line);
        }

        return (cError == 0 ? !_StopFlag : false);
#undef macroFileName
    }

Jetzt haben wir ein kleines System, mit dem wir auswählen können, welches Modell für das Laden verwendet wird. Denken Sie jedoch daran, dass wir immer mit dem Laden der Daten beginnen, die in der Wiedergabe oder Simulation verwendet werden, und dann die Daten laden, die als vorherige Balken verwendet werden sollen.


Abschließende Überlegungen

Damit ist diese Phase der Arbeit an der Konfigurationsdatei abgeschlossen. Jetzt kann der Nutzer (zumindest vorläufig) alles, was er braucht, schon in diesem frühen Stadium angeben.

Dieser Artikel hat ziemlich viel Zeit in Anspruch genommen, aber meiner Meinung nach hat es sich gelohnt, da wir uns nun eine Zeit lang nicht mehr um Probleme mit der Konfigurationsdatei kümmern müssen. Es wird immer wie erwartet funktionieren, egal wie es aufgebaut ist.

Im nächsten Artikel werden wir das Replay-/Simulationssystem weiter anpassen, um den Forex-Markt und ähnliche Märkte weiter abzudecken, da der Aktienmarkt schon viel weiter fortgeschritten ist. Ich werde mich auf den Devisenmarkt konzentrieren, damit das System dort genauso funktionieren kann wie am Aktienmarkt.

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

Beigefügte Dateien |
Market_Replay_ev221.zip (14387.15 KB)
Algorithmen zur Optimierung mit Populationen: Stochastische Diffusionssuche (SDS) Algorithmen zur Optimierung mit Populationen: Stochastische Diffusionssuche (SDS)
Der Artikel behandelt die stochastische Diffusionssuche (SDS), einen sehr leistungsfähigen und effizienten Optimierungsalgorithmus, der auf den Prinzipien des Random Walk basiert. Der Algorithmus ermöglicht es, optimale Lösungen in komplexen mehrdimensionalen Räumen zu finden, wobei er sich durch eine hohe Konvergenzgeschwindigkeit und die Fähigkeit auszeichnet, lokale Extrema zu vermeiden.
Entwurfsmuster in der Softwareentwicklung und MQL5 (Teil 4): Verhaltensmuster 2 Entwurfsmuster in der Softwareentwicklung und MQL5 (Teil 4): Verhaltensmuster 2
In diesem Artikel werden wir unsere Serie über das Thema Entwurfmuster abschließen. Wir haben erwähnt, dass es drei Arten von Entwurfmuster gibt: Erzeugungs-, Verhaltens- und strukturelle Muster. Wir werden die verbleibenden Muster des Verhaltenstyps vervollständigen, die dabei helfen können, die Methode der Interaktion zwischen Objekten so festzulegen, dass unser Code sauber wird.
Entwicklung eines Replay Systems — Marktsimulation (Teil 22): FOREX (III) Entwicklung eines Replay Systems — Marktsimulation (Teil 22): FOREX (III)
Obwohl dies der dritte Artikel zu diesem Thema ist, muss ich für diejenigen, die den Unterschied zwischen dem Aktienmarkt und dem Devisenmarkt noch nicht verstanden haben, erklären: Der große Unterschied besteht darin, dass es auf dem Devisenmarkt keine Informationen über einige Punkte gibt, die im Laufe des Handels tatsächlich aufgetreten sind.
MQL5-Assistent-Techniken, die Sie kennen sollten (Teil 09): K-Means-Clustering mit fraktalen Wellen MQL5-Assistent-Techniken, die Sie kennen sollten (Teil 09): K-Means-Clustering mit fraktalen Wellen
Das K-Means-Clustering verfolgt den Ansatz, Datenpunkte als einen Prozess zu gruppieren, der sich zunächst auf die Makroansicht eines Datensatzes konzentriert und zufällig generierte Clusterzentren verwendet, bevor er heranzoomt und diese Zentren anpasst, um den Datensatz genau darzustellen. Wir werden uns dies ansehen und einige Anwendungsfälle ausnutzen.