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

Entwicklung eines Replay Systems — Marktsimulation (Teil 19): Erforderliche Anpassungen

MetaTrader 5Tester | 11 Januar 2024, 10:58
202 0
Daniel Jose
Daniel Jose

Einführung

Ich denke, aus den vorangegangenen Artikeln in dieser Reihe geht hervor, dass wir einige zusätzliche Punkte umsetzen müssen. Dies ist absolut notwendig, um die Arbeit besser zu organisieren, insbesondere im Hinblick auf zukünftige Verbesserungen. Wenn Sie planen, das Replay-/Simulationssystem nur für die Arbeit mit einem Asset zu verwenden, werden Sie viele der Dinge, die wir jetzt implementieren werden, nicht benötigen. Sie können sie beiseite lassen – ich meine, sie müssen nicht unbedingt in der Konfigurationsdatei vorhanden sein.

Es ist jedoch sehr wahrscheinlich, dass Sie nicht nur ein Asset, sondern mehrere verschiedene Assets oder sogar eine ziemlich große Datenbank verwenden werden. In diesem Fall müssen wir die Dinge organisieren und daher zusätzlichen Code implementieren, um dieses Ziel zu erreichen, obwohl wir in einigen sehr speziellen Fällen einfach das verwenden könnten, was wir bereits im Quellcode haben, aber auf eine implizite Weise. Das muss einfach ans Licht gebracht werden.

Ich mag es immer, die Dinge gut zu organisieren, und ich schätze, viele Leute denken und versuchen das Gleiche zu tun. Es wäre gut zu wissen und zu verstehen, wie man diese Funktion implementiert. Darüber hinaus erfahren Sie, wie Sie dem System neue Parameter hinzufügen können, wenn Sie einen bestimmten Parameter für eine bestimmte Anlage benötigen, die Sie für Studien oder Analysen verwenden möchten.

Hier werden wir den Boden bereiten, damit wir, wenn wir neue Funktionen zum Code hinzufügen müssen, dies reibungslos und einfach tun können. Der derzeitige Kodex kann einige der Dinge, die notwendig sind, um sinnvolle Fortschritte zu erzielen, noch nicht abdecken oder behandeln. Wir müssen alles strukturieren, damit wir bestimmte Dinge mit minimalem Aufwand umsetzen können. Wenn wir alles richtig machen, erhalten wir ein wirklich universelles System, das sich sehr leicht an jede Situation anpassen lässt, die es zu bewältigen gilt. Einer dieser Aspekte wird das Thema des nächsten Artikels sein. Dank der letzten beiden Artikel, in denen gezeigt wurde, wie man dem Marktbeobachtungsfenster Ticks hinzufügt, laufen die Dinge zum Glück im Allgemeinen nach Plan. Wenn Sie diese Artikel verpasst haben, können Sie sie über die folgenden Links aufrufen: Entwicklung eines Replay Systems — Marktsimulation (Teil 17): Ticks und noch mehr Ticks (I) und Entwicklung eines Replay-Systems — Marktsimulation (Teil 18): Ticks und noch mehr Ticks (II). Diese beiden Artikel liefern wertvolle Informationen darüber, was wir in zukünftigen Artikeln tun werden.

Es fehlen jedoch noch einige sehr spezifische Details, die in diesem Artikel umgesetzt werden sollen. Darüber hinaus gibt es noch andere, recht komplexe Themen, für die separate Artikel erforderlich sind, in denen erklärt wird, wie man sie bearbeitet und die Probleme löst. Beginnen wir nun mit der Implementierung des Systems, das wir in diesem Artikel sehen werden. Wir beginnen damit, die Organisation der von uns verwendeten Daten zu verbessern.


Implementierung eines Verzeichnissystems

Die Frage ist hier nicht, ob wir dieses System wirklich brauchen, sondern warum wir es einführen sollten. Im derzeitigen Entwicklungsstadium können wir das Verzeichnissystem verwenden. Allerdings werden wir noch viel Arbeit in die Implementierung des Replay/Simulationsdienstes stecken müssen. Ich meine damit, dass es viel mehr zu tun gibt, als nur eine neue Variable in eine Konfigurationsdatei einzufügen. Um zu verstehen, wovon ich spreche, werfen Sie einen Blick auf die folgenden Bilder:

Abbildung 01

Abbildung 01 – Zugriff auf Verzeichnisse im aktuellen System.


Abbildung 02

Abbildung 02 - Ein alternativer Weg zum Zugriff auf Verzeichnisse.


Obwohl Abbildung 01 aus der Perspektive des Replay-/Simulationssystems dasselbe Verhalten wie Abbildung 02 aufweist, werden Sie bald feststellen, dass es viel praktischer ist, die Dinge wie in Abbildung 02 zu konfigurieren. Das liegt daran, dass wir nur einmal das Verzeichnis angeben müssen, in dem sich die Daten befinden, und das Wiedergabe-/Modellsystem kümmert sich um den Rest. Bei der Verwendung des in Abbildung 02 gezeigten Systems werden wir davor bewahrt, zu vergessen oder falsch anzugeben, wo wir nach Daten suchen müssen, wenn wir eine sehr große Datenbank verwenden, um zum Beispiel die Verwendung eines gleitenden Durchschnitts hinzuzufügen. Wenn Sie hier einen Schreibfehler machen, können zwei Situationen eintreten:

  • Im ersten Fall wird das System lediglich eine Warnung ausgeben, dass auf die Daten nicht zugegriffen werden kann. 
  • Im zweiten Fall, der schwerwiegender ist, werden falsche Daten verwendet.

Durch die Möglichkeit, ein Verzeichnis an einer Stelle einzurichten, sind derartige Fehler jedoch sehr viel unwahrscheinlicher. Es ist nicht so, dass sie überhaupt nicht vorkommen werden, aber sie werden eher unwahrscheinlich sein. Denken Sie daran, dass wir Objekte in noch spezifischere Verzeichnisse einteilen können und so Abbildung 01 und Abbildung 02 kombinieren. Hier werde ich jedoch alles auf einer einfacheren Ebene belassen. Fühlen Sie sich frei, Dinge so zu implementieren, dass sie zu Ihrer Datenverarbeitung und Ihrem Organisationsstil passen.

Wir haben die Theorie gesehen, jetzt ist es an der Zeit, zu sehen, wie man es in der Praxis macht. Das Verfahren ist relativ einfach und unkompliziert, zumindest im Vergleich zu dem, was wir noch zu tun haben. Zunächst erstellen wir eine neue private Variable für die Klasse, wie im folgenden Code gezeigt:

private :
    enum eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE};
    string m_szPath;


Wenn wir diese Variable an dieser Stelle hinzufügen, wird sie für alle internen Prozeduren der Klasse sichtbar. Sie ist jedoch von außerhalb der Klasse nicht zugänglich. Dadurch wird verhindert, dass es zu sehr verändert wird. Das liegt daran, dass wir in einigen internen Prozeduren einer Klasse den Wert einer Variablen ändern können, ohne es zu merken. Und wir haben vielleicht Schwierigkeiten zu verstehen, warum der Code nicht wie erwartet funktioniert.

Sobald dies geschehen ist, müssen wir unserer Klasse mitteilen, dass sie den neuen Befehl in der Konfigurationsdatei erkennen soll. Dieses Verfahren wird an einem ganz bestimmten Punkt durchgeführt, kann aber je nach dem, was wir hinzufügen, variieren. In unserem Fall werden wir dies in der unten dargestellten Reihenfolge tun:

inline bool Configs(const string szInfo)
    {
        const string szList[] = {
                                "POINTSPERTICK",
                                "PATH"
                                };
        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_PointsPerTick = StringToDouble(szRet[1]);
                    return true;
                case 1:
                    m_szPath = szRet[1];
                    return true;                                    
            }
            Print("Variable >>", szRet[0], "<< undefined.");
        }else
            Print("Definition of configuratoin >>", szInfo, "<< invalid.");
                                
        return false;
    }

Beachten Sie, wie viel einfacher dies ist, wenn der gesamte Code so strukturiert ist, dass Verbesserungen möglich sind. Wir müssen jedoch vorsichtig sein. Wenn wir Vorsichtsmaßnahmen ergreifen, werden wir keine Probleme haben, alles, was wir brauchen, in den Code einzubauen.

Als erstes fügen wir den Namen oder die Bezeichnung des Befehls, der in der Konfigurationsdatei verwendet werden soll, in das serielle Datenfeld ein. Beachten Sie, dass dies alles in Großbuchstaben geschrieben werden muss. Wir könnten die Groß- und Kleinschreibung berücksichtigen, aber das würde es dem Nutzer erschweren, den Befehl einzugeben und in der Konfigurationsdatei zu platzieren. Wenn Sie der Einzige sind, der das System nutzt und dieselbe Bezeichnung mit unterschiedlichen Werten verwenden möchten, ist es vielleicht eine gute Idee, die Groß- und Kleinschreibung zu berücksichtigen. Andernfalls würde diese Idee die ganze Arbeit verkomplizieren. Ich persönlich bin der Meinung, dass die Verwendung ein und desselben Begriffs für verschiedene Bedeutungen unser Leben nur noch schwieriger macht. Deshalb tue ich das nicht.

Sobald das Etikett der Befehlsmatrix hinzugefügt wurde, müssen wir seine Funktionalität implementieren. Dies wird genau an dieser Stelle gemacht. So einfach ist das. Da sie die zweite in der Kette ist und die Kette bei Null beginnt, verwenden wir den Wert 1, um anzuzeigen, dass wir diese bestimmte Funktionalität implementieren. Da nur der Verzeichnisname angegeben werden muss, ist der Befehl recht einfach. Schließlich geben wir true an den Aufrufer zurück und zeigen damit an, dass der Befehl erkannt und erfolgreich umgesetzt wurde.

Die Reihenfolge, in der das System ergänzt wird, ist genau wie dargestellt. Danach können wir die in der Konfigurationsdatei angegebenen Daten verwenden. Es gibt jedoch einen Punkt, den ich vergessen habe zu erwähnen, er ist recht einfach, verdient aber Aufmerksamkeit. In manchen Fällen kann es den Anschein haben, dass eine neu hinzugefügte Ressource Probleme verursacht, obwohl sie in Wirklichkeit nur nicht korrekt initialisiert wurde. In diesem Fall müssen wir jedes Mal, wenn wir eine private globale Variable hinzufügen, sicherstellen, dass sie im Klassenkonstruktor richtig initialisiert wird. Sie können dies im folgenden Code sehen, wo wir eine neue Variable initialisieren.

C_ConfigService()
	:m_szPath(NULL)
	{
	}

Auf diese Weise wird sichergestellt, dass wir einen bekannten Wert für die Variable haben, der noch kein Wert zugewiesen wurde. In manchen Situationen mag dieses Detail unbedeutend erscheinen, aber in anderen kann es ernsthafte Probleme vermeiden und Zeit sparen, und es gilt als gute Programmierpraxis. Nachdem diese Arbeit erledigt ist, die Variable im Klassenkonstruktor initialisiert wurde und wir festgelegt haben, wie wir ihr einen Wert zuweisen, der auf den Angaben in der Konfigurationsdatei basiert, ist es an der Zeit, diesen Wert zu verwenden. Dieser Wert wird nur in einer einzigen Funktion verwendet, die für die Steuerung des Datenbankladens zuständig ist.

Schauen wir uns an, wie man das umsetzen kann:

bool SetSymbolReplay(const string szFileConfig)
    {
        #define macroFileName ((m_szPath != NULL ? m_szPath + "\\" : "") + szInfo)
        int        file,
                iLine;
        char    cError,
                cStage;
        string  szInfo;
        bool    bBarsPrev;
        C_FileBars *pFileBars;
        
        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;
        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:
                            pFileBars = new C_FileBars(macroFileName);
                            if ((m_dtPrevLoading = (*pFileBars).LoadPreView()) == 0) cError = 3; else bBarsPrev = true;
                            delete pFileBars;
                            break;
                        case 2:
                            if (LoadTicks(macroFileName) == 0) cError = 4;
                            break;
                        case 3:
                            if ((m_dtPrevLoading = LoadTicks(macroFileName, false)) == 0) cError = 5; else bBarsPrev = true;
                            break;
                        case 4:
                            if (!BarsToTicks(macroFileName)) cError = 6;
                            break;
                        case 5:
                            if (!Configs(szInfo)) cError = 7;
                            break;
                    }
                break;
            };
            iLine += (cError > 0 ? 0 : 1);
        }
        FileClose(file);
        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, " cannot be recognized by the system...");    break;
            case 2  : Print("The system did not expect the content of the line ", iLine);                  break;
            default : Print("Error in line ", iLine);
        }
                                                        
        return (cError == 0 ? !_StopFlag : false);
#undef macroFileName
    }

Da wir dies auf einzigartige Weise und an mehreren Stellen gleichzeitig verwenden werden, habe ich mich für ein Makro entschieden, um die Codierung zu erleichtern. Alle gelb markierten Punkte erhalten genau den im Makro enthaltenen Code. Dies vereinfacht die Aufgabe erheblich, da man nicht mehrmals das Gleiche schreiben muss. Dadurch werden auch mögliche Fehler vermieden, die bei der Pflege oder Änderung von Code, der an mehreren Stellen verwendet wird, auftreten können. Schauen wir uns nun genauer an, was das Makro tut.

#define macroFileName ((m_szPath != NULL ? m_szPath + "\\" : "") + szInfo)

Erinnern Sie sich, dass wir eine Variable mit einem bestimmten Wert initialisiert haben? In dem Moment, in dem wir versuchen, die Variable zu verwenden, werden wir genau prüfen, welchen Wert sie enthält. Wenn dies derselbe Pfad ist, den wir im Konstruktor initialisiert haben, haben wir einen definierten Pfad. Wenn es sich um den Namen aus der Konfigurationsdatei handelt, haben wir einen anderen Pfad, aber so oder so haben wir einen Namen, unter dem wir auf die Datei zugreifen können.

Dieses System ist so universell, dass Sie das Verzeichnis jederzeit ändern können, ohne etwas anderes in dem bereits fertiggestellten und kompilierten System zu ändern. Auf diese Weise müssen wir nicht den gesamten Code neu kompilieren, wenn wir die Konfigurationsdatei ändern. Das Einzige, was wir tun müssen, um das Verzeichnis zu ändern, in dem wir arbeiten wollen, ist die Verwendung des folgenden Fragments in der Konfigurationsdatei:

[Config]
Path = < NEW PATH >

Dabei enthält <NEW PATH> die neue Adresse (Pfad), die von nun an in der Konfigurationsdatei verwendet wird. Dies ist sehr angenehm, da der Arbeitsaufwand bei der Arbeit mit Datenbanken, die eine Verzeichnisstruktur enthalten können, erheblich reduziert wird. Denken Sie daran, dass Sie die Daten im Verzeichnis systematisieren und organisieren sollten, damit Sie die gewünschte Datei leichter finden können.

Sobald dies erledigt ist, können wir zum nächsten Schritt übergehen, in dem wir einige Dinge abschließen, die implementiert werden müssen. Dies wird im nächsten Thema behandelt.


Anpassen der nutzerdefinierten Daten eines Symbols

Um unser Auftragssystem zu implementieren, benötigen wir zunächst drei Grundwerte: minimum volume, tick value and tick size (Mindestvolumen, Tick-Wert, Tick-Größe). Nur einer dieser Werttypen ist derzeit implementiert, und seine Implementierung ist nicht genau das, was sie sein sollte, da es vorkommen kann, dass der Wert nicht in der Konfigurationsdatei festgelegt ist. Dies erschwert unsere Aufgabe, ein synthetisches Symbol zu erstellen, das nur dazu dient, wahrscheinliche Marktbewegungen zu simulieren.

Ohne die geforderte Korrektur können wir später bei der Arbeit mit dem Bestellsystem inkonsistente Daten im System haben. Wir müssen sicherstellen, dass diese Daten korrekt konfiguriert sind. Dies wird uns vor Problemen bewahren, wenn wir versuchen, etwas in unser System zu implementieren, das bereits einen recht umfangreichen Code hat. Deshalb werden wir damit beginnen, die falschen Punkte zu korrigieren, um in der nächsten Phase unserer Arbeit Probleme zu vermeiden. Wenn wir Probleme haben, dann sollten sie anderer Natur sein. Das Auftragssystem wird nicht mit dem Dienst interagieren, der zur Erstellung des Replay-/Simulationsdienstes des Marktes verwendet wird. Die einzigen Informationen, auf die wir stoßen, sind das Chart und der Name des Symbols, mehr nicht. Zumindest ist das im Moment meine Absicht. Ich weiß nicht, ob wir wirklich Erfolg haben werden.

Für dieses Szenario müssen wir als erstes die drei Werte initialisieren, die wir unbedingt benötigen. Sie werden jedoch auf Null gesetzt. Betrachten wir dies Schritt für Schritt. Zunächst müssen wir unser Problem lösen. Dies geschieht im folgenden Code:

C_Replay(const string szFileConfig)
    {
        m_ReplayCount = 0;
        m_dtPrevLoading = 0;
        m_Ticks.nTicks = 0;
        Print("************** Market Replay Service **************");
        srand(GetTickCount());
        GlobalVariableDel(def_GlobalVariableReplay);
        SymbolSelect(def_SymbolReplay, false);
        CustomSymbolDelete(def_SymbolReplay);
        CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay), _Symbol);
        CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX);
        CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX);
        SymbolSelect(def_SymbolReplay, true);
        CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0.0);
        CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation");
        m_IdReplay = (SetSymbolReplay(szFileConfig) ? 0 : -1);
    }

Hier setzen wir den Anfangswert auf Null, aber als Bonus geben wir auch eine Beschreibung unseres nutzerdefinierten Symbols an. Dies ist nicht notwendig, aber es kann interessant sein, wenn Sie ein Fenster mit einer Liste von Symbolen öffnen und ein Symbol mit einem eindeutigen Namen sehen. Sie haben wahrscheinlich schon bemerkt, dass wir die bisherige Variable nicht mehr verwenden werden. Die Variable, die an einer bestimmten Stelle deklariert wurde, wie im folgenden Code gezeigt:

class C_FileTicks
{
    protected:
        struct st00
        {
            MqlTick  Info[];
            MqlRates Rate[];
            int      nTicks,
                     nRate;
            bool     bTickReal;
        }m_Ticks;
        double       m_PointsPerTick;
    private :
        int          m_File;

Alle Stellen, an denen diese Variable erscheint, sollten sich nun auf den in dem Symbol enthaltenen und definierten Wert beziehen. Jetzt haben wir neuen Code, aber im Wesentlichen wurde dieser Wert an zwei Stellen im gesamten Replay-/Simulationssystem erwähnt. Die erste ist unten abgebildet:

inline long RandomWalk(long pIn, long pOut, const MqlRates &rate, MqlTick &tick[], int iMode)
    {
        double vStep, vNext, price, vHigh, vLow, PpT;
        char i0 = 0;

        PpT = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
        vNext = vStep = (pOut - pIn) / ((rate.high - rate.low) / PpT);
        vHigh = rate.high;
        vLow = rate.low;
        for (long c0 = pIn, c1 = 0, c2 = 0; c0 < pOut; c0++, c1++)
        {
            price = tick[c0 - 1].last + (PpT * ((rand() & 1) == 1 ? -1 : 1));
            price = tick[c0].last = (price > vHigh ? price - PpT : (price < vLow ? price + PpT : price));
            switch (iMode)
            {
                case 0:
                    if (price == rate.close)
                        return c0;
                        break;
                case 1:
                    i0 |= (price == rate.high ? 0x01 : 0);
                        i0 |= (price == rate.low ? 0x02 : 0);
                        vHigh = (i0 == 3 ? rate.high : vHigh);
                        vLow = (i0 ==3 ? rate.low : vLow);
                        break;
                case 2:
                    break;
            }
            if ((int)floor(vNext) < c1)
            {
                if ((++c2) <= 3) continue;
                vNext += vStep;
                if (iMode == 2)
                {
                    if ((c2 & 1) == 1)
                    {
                        if (rate.close > vLow) vLow += PpT; else vHigh -= PpT;
                    }else
                    {
                        if (rate.close < vHigh) vHigh -= PpT; else vLow += PpT;
                    }
                } else
                {
                    if (rate.close > vLow) vLow = (i0 == 3 ? vLow : vLow + PpT); else vHigh = (i0 == 3 ? vHigh : vHigh - PpT);
                }
            }
        }

        return pOut;
    }

Da wir denselben Code nicht an mehreren Stellen wiederholen wollen, verwenden wir eine lokale Variable, um uns zu helfen. Das Prinzip ist jedoch dasselbe: Wir beziehen uns auf den im Symbol definierten Wert. Der zweite Punkt, auf den sich dieser Wert bezieht, befindet sich in der Klasse C_Replay. Aus praktischen Gründen werden wir jedoch etwas anders vorgehen als oben gezeigt, als wir den Random Walk erstellt haben. Die Darstellung und Verwendung von Informationen in den Diagrammen verschlechtert die Leistung, da zu viele unnötige Aufrufe erfolgen. Das liegt daran, dass bei der Erstellung eines Random Walk jeder Balken drei Zugriffe auf ihn erzeugt.

Aber wenn sie einmal erstellt ist, kann sie Tausende von Ticks enthalten, die alle in nur drei Aufrufen erstellt wurden. Dadurch wird die Leistung bei der Präsentation und beim Plotten etwas verlangsamt, aber wir werden sehen, wie sich das in der Praxis auswirkt. Wenn wir eine Datei mit echten Ticks verwenden, d. h. wiedergeben, wird eine solche Verlangsamung nicht auftreten. Dies liegt daran, dass das System bei der Verwendung echter Daten keine zusätzlichen Informationen benötigt, um 1-Minuten-Balken zu zeichnen und die Informationen in den Tick-Chart im Fenster der Marktübersicht zu übertragen. Wir haben dies in zwei früheren Artikeln behandelt.

Wenn wir jedoch 1-Minuten-Balken verwenden wollen, um Ticks zu erzeugen, d. h. um eine Simulation durchzuführen, müssen wir die Tick-Größe kennen, damit der Dienst mit dieser Information ein geeignetes Bewegungsmodell erstellen kann. Diese Bewegung wird im Fenster der Marktübersicht sichtbar. Diese Informationen werden jedoch nicht benötigt, um Balken zu erstellen, da die Konvertierung in der Klasse C_FileTicks durchgeführt wurde.

Mit diesem Wissen müssen wir die Funktion betrachten, die das angegebene Chart erzeugt, und somit prüfen, wie viele Aufrufe während der Ausführung eingehen werden. Nachfolgend finden Sie eine Funktion, die während der Simulation verwendet wird:

inline void CreateBarInReplay(const bool bViewMetrics, const bool bViewTicks)
        {
#define def_Rate m_MountBar.Rate[0]
        
                bool bNew;
        MqlTick tick[1];
        static double PointsPerTick = 0.0;
        
        if (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time))
        {
                        PointsPerTick = (PointsPerTick == 0.0 ? SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) : PointsPerTick);                    
                        if (bViewMetrics) Metrics();
                        m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                        def_Rate.real_volume = 0;
                        def_Rate.tick_volume = 0;
        }
        bNew = (def_Rate.tick_volume == 0);
        def_Rate.close = (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : def_Rate.close);
        def_Rate.open = (bNew ? def_Rate.close : def_Rate.open);
        def_Rate.high = (bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high);
        def_Rate.low = (bNew || (def_Rate.close < def_Rate.low) ? def_Rate.close : def_Rate.low);
        def_Rate.real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real;
        def_Rate.tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > 0 ? 1 : 0);
        def_Rate.time = m_MountBar.memDT;
        CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
        if (bViewTicks)
        {
                        tick = m_Ticks.Info[m_ReplayCount];
                        if (!m_Ticks.bTickReal)
                        {
                                static double BID, ASK;
                                double  dSpread;
                                int     iRand = rand();
                        
                                dSpread = PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? PointsPerTick : 0 ) : 0 );
                                if (tick[0].last > ASK)
                                {
                                        ASK = tick[0].ask = tick[0].last;
                                        BID = tick[0].bid = tick[0].last - dSpread;
                                }
                                if (tick[0].last < BID)
                                {
                                        ASK = tick[0].ask = tick[0].last + dSpread;
                                        BID = tick[0].bid = tick[0].last;
                                }
                        }
                        CustomTicksAdd(def_SymbolReplay, tick); 
        }
                m_ReplayCount++;

#undef def_Rate
        }

Hier deklarieren wir eine statische lokale Variable. Dies wird verwendet, um unnötige Aufrufe der Funktion zu vermeiden, die die Tick-Größe festlegt. Diese Erfassung erfolgt nur einmal während der Lebensdauer und Laufzeit des Dienstes, während die Variable nur an den angegebenen Stellen verwendet wird. Es macht also keinen Sinn, sie über diese Funktion hinaus zu erweitern. Beachten Sie jedoch, dass diese Stelle, an der die Variable tatsächlich verwendet wird, nur im Simulationsmodus verfügbar ist. Im Wiedergabemodus hat diese Variable keinen praktischen Nutzen.

Damit wurde auch das Problem mit der Tick-Größe gelöst. Es gibt noch zwei weitere Probleme, die gelöst werden müssen. Das Problem mit den Ticks ist jedoch noch nicht vollständig gelöst. Es gibt ein Problem mit der Initialisierung. Wir werden dieses Problem lösen, während wir die anderen beiden Probleme lösen, da der Ansatz derselbe sein wird.


Letzte zu erstellende Details

Die Frage ist, was wir tatsächlich anpassen sollten. Es stimmt, dass wir im nutzerdefinierten Symbol mehrere Dinge einstellen können. Die meisten von ihnen werden jedoch für unsere Zwecke nicht benötigt. Wir müssen uns nur auf das konzentrieren, was wir wirklich brauchen. Außerdem müssen wir es so einrichten, dass wir diese Informationen, wenn wir sie brauchen, auf einfache und universelle Weise erhalten. Ich sage das, weil wir bald mit der Erstellung eines Bestellsystems beginnen werden, aber ich bin mir nicht sicher, ob es wirklich so schnell gehen wird. Auf jeden Fall möchte ich, dass unser EA mit Replay/Simulation kompatibel ist und sich für den Einsatz auf dem realen Markt eignet, sowohl mit einem Demokonto als auch mit einem echten Konto. Dazu müssen die Komponenten die gleichen Informationen enthalten, wie sie auf dem realen Markt vorhanden sind.

In diesem Fall müssen wir sie mit Nullwerten initialisieren. Dadurch wird sichergestellt, dass die Werte des nutzerdefinierten Symbols mit den Werten des echten Symbols übereinstimmen. Die Initialisierung der Werte auf Null bedeutet außerdem, dass wir sie später testen können, und sie erleichtert die Implementierung und das Testen möglicher Fehler in der Symbolkonfiguration erheblich.

C_Replay(const string szFileConfig)
    {
        m_ReplayCount = 0;
        m_dtPrevLoading = 0;
        m_Ticks.nTicks = 0;
        Print("************** Market Replay Service **************");
        srand(GetTickCount());
        GlobalVariableDel(def_GlobalVariableReplay);
        SymbolSelect(def_SymbolReplay, false);
        CustomSymbolDelete(def_SymbolReplay);
        CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay), _Symbol);
        CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX);
        CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX);
        SymbolSelect(def_SymbolReplay, true);
        CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0);
        CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0);
        CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0);
        CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation");
        m_IdReplay = (SetSymbolReplay(szFileConfig) ? 0 : -1);
    }

Hier führen wir die Werte aus, die wir in der Zukunft, in der nächsten Zeit, wirklich brauchen. Diese Werte sind die Tick-Größe, der Tick-Wert und das Volumen (in diesem Fall der Schritt). Da die Stufe jedoch sehr oft der Mindestmenge entspricht, die verwendet werden sollte, sehe ich kein Problem darin, sie anstelle der Mindestmenge einzustellen. Auch weil dieser Schritt in der nächsten Phase noch wichtiger für uns ist. Dafür gibt es einen weiteren Grund: Ich habe versucht, das Mindestvolumen einzustellen, aber aus irgendeinem Grund konnte ich das nicht tun. MetaTrader 5 ignorierte einfach die Tatsache, dass wir ein Mindestvolumen festlegen mussten.

Sobald dies geschehen ist, müssen wir etwas anderes tun und überprüfen, ob diese Werte tatsächlich initialisiert wurden. Dies wird im folgenden Code gemacht:

bool ViewReplay(ENUM_TIMEFRAMES arg1)
   {
#define macroError(A) { Print(A); return false; }
   u_Interprocess info;
   
   if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0)
        macroError("Configuração do ativo não esta completa, falta declarar o tamanho do ticket.");
   if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0)
        macroError("Configuração do ativo não esta completa, falta declarar o valor do ticket.");
   if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0)
        macroError("Configuração do ativo não esta completa, falta declarar o volume mínimo.");
   if (m_IdReplay == -1) return false;
   if ((m_IdReplay = ChartFirst()) > 0) do
   {
        if (ChartSymbol(m_IdReplay) == def_SymbolReplay)
        {
            ChartClose(m_IdReplay);
            ChartRedraw();
        }
   }while ((m_IdReplay = ChartNext(m_IdReplay)) > 0);
   Print("Aguardando permissão do indicador [Market Replay] para iniciar replay ...");
   info.u_Value.IdGraphic = m_IdReplay = ChartOpen(def_SymbolReplay, arg1);
   ChartApplyTemplate(m_IdReplay, "Market Replay.tpl");
   ChartRedraw(m_IdReplay);
   GlobalVariableDel(def_GlobalVariableIdGraphics);
   GlobalVariableTemp(def_GlobalVariableIdGraphics);
   GlobalVariableSet(def_GlobalVariableIdGraphics, info.u_Value.df_Value);
   while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(750);
   
   return ((!_StopFlag) && (ChartSymbol(m_IdReplay) != ""));
#undef macroError
  }

Um unnötige Wiederholungen zu vermeiden, werden wir ein Makro verwenden. Es wird eine Fehlermeldung angezeigt und mit einer Systemfehlermeldung beendet. Hier überprüfen wir nacheinander die Werte, die in der Konfigurationsdatei deklariert und initialisiert werden müssen. Wenn eines dieser Symbole nicht initialisiert wurde, wird der Nutzer aufgefordert, das nutzerdefinierte Symbol korrekt zu konfigurieren. Andernfalls wird der Replay-/Simulationsdienst nicht mehr funktionieren. Ab diesem Zeitpunkt gilt er als funktionsfähig und in der Lage, dem Auftragssystem, in diesem Fall dem zu erstellenden EA, die notwendigen Daten für eine korrekte Modellierung oder Wiedergabe des Marktes zu liefern. So können wir den Versand von Aufträgen simulieren.

Das ist gut, aber um diese Werte zu initialisieren, müssen wir einige Ergänzungen am System vornehmen, wie unten gezeigt:

inline bool Configs(const string szInfo)
    {
        const string szList[] = {
                                "PATH",
                                "POINTSPERTICK",
                                "VALUEPERPOINTS",
                                "VOLUMEMINIMAL"
                                };
        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;                                    
            }
            Print("Variable >>", szRet[0], "<< undefined.");
        }else
            Print("Definition of configuratoin >>", szInfo, "<< invalid.");

        return false;
    }

Das Hinzufügen von Dingen zum System ist recht einfach. Hier haben wir zwei neue Werte hinzugefügt, die durch einfaches Bearbeiten der Datei konfiguriert werden können, die die Wiedergabe oder Simulation eines beliebigen Symbols konfiguriert. Dabei handelt es sich um einen Tick-Wert, der einen Aufruf einer Funktion auslöst, die den entsprechenden Wert erwartet, und einen Schrittgrößenwert, der ebenfalls eine interne Funktion aufruft, die diesen Wert anpasst. Alle weiteren Ergänzungen werden in den nächsten Schritten erfolgen.


Abschließende Überlegungen

Ich habe noch nicht getestet, ob die Werte geeignet sind oder nicht. Seien Sie daher vorsichtig bei der Bearbeitung der Konfigurationsdatei, um Fehler bei der Verwendung des Auftragssystems zu vermeiden.

Wie auch immer, Sie können anhand der beigefügten nutzerdefinierten Symbole überprüfen, wie die Dinge laufen.

Wichtiger Hinweis: Trotz der Tatsache, dass das System praktisch funktionsfähig ist, ist dies nicht ganz richtig. Da es derzeit nicht möglich ist, eine Wiedergabe oder Simulation mit Forex-Daten durchzuführen. Denn der Forex-Markt verwendet einige Dinge, die das System noch nicht verarbeiten kann. Der Versuch, dies zu tun, führt zu Bereichsfehlern in den System-Arrays, egal ob im Wiedergabe- oder Simulationsmodus. Aber ich arbeite an Korrekturen, damit ich mit Forex-Marktdaten arbeiten kann.

Im nächsten Artikel werden wir uns mit diesem Thema befassen: FOREX.


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

Beigefügte Dateien |
Neuronale Netze leicht gemacht (Teil 52): Forschung mit Optimismus und Verteilungskorrektur Neuronale Netze leicht gemacht (Teil 52): Forschung mit Optimismus und Verteilungskorrektur
Da das Modell auf der Grundlage des Erfahrungswiedergabepuffers trainiert wird, entfernt sich die aktuelle Strategie oder Politik des Akteurs immer weiter von den gespeicherten Beispielen, was die Effizienz des Trainings des Modells insgesamt verringert. In diesem Artikel befassen wir uns mit einem Algorithmus zur Verbesserung der Effizienz bei der Verwendung von Stichproben in Algorithmen des verstärkten Lernens.
Entwicklung eines Replay Systems — Marktsimulation (Teil 18): Ticks und noch mehr Ticks (II). Entwicklung eines Replay Systems — Marktsimulation (Teil 18): Ticks und noch mehr Ticks (II).
Offensichtlich sind die aktuellen Metriken sehr weit von der idealen Zeit für die Erstellung eines 1-Minuten-Balkens entfernt. Das ist das erste, was wir in Angriff nehmen werden. Die Behebung des Synchronisationsproblems ist nicht schwierig. Das mag schwierig erscheinen, ist aber eigentlich ganz einfach. Wir haben die erforderliche Korrektur im vorigen Artikel nicht vorgenommen, da er darauf abzielte, zu erklären, wie man die Tick-Daten, die zur Erstellung der 1-Minuten-Balken im Chart verwendet wurden, in das Fenster der Marktübersicht überträgt.
Entwicklung eines Replay Systems — Marktsimulation (Teil 20): FOREX (I) Entwicklung eines Replay Systems — Marktsimulation (Teil 20): FOREX (I)
Das ursprüngliche Ziel dieses Artikels ist es nicht, alle Möglichkeiten des Forex-Handels abzudecken, sondern das System so anzupassen, dass Sie zumindest ein Replay des Marktes durchführen können. Wir lassen die Simulation noch einen Moment auf sich warten. Wenn wir jedoch keine Ticks, sondern nur Balken haben, können wir mit ein wenig Aufwand mögliche Abschlüsse simulieren, die auf dem Forex-Markt passieren könnten. Dies wird der Fall sein, bis wir uns mit der Anpassung des Simulators befassen. Der Versuch, mit Forex-Daten innerhalb des Systems zu arbeiten, ohne sie zu verändern, führt zu einer Reihe von Fehlern.
Neuronale Netze leicht gemacht (Teil 53): Aufteilung der Belohnung Neuronale Netze leicht gemacht (Teil 53): Aufteilung der Belohnung
Wir haben bereits mehrfach darüber gesprochen, wie wichtig die richtige Wahl der Belohnungsfunktion ist, mit der wir das gewünschte Verhalten des Agenten anregen, indem wir Belohnungen oder Bestrafungen für einzelne Aktionen hinzufügen. Aber die Frage nach der Entschlüsselung unserer Signale durch den Agenten bleibt offen. In diesem Artikel geht es um die Aufteilung der Belohnung im Sinne der Übertragung einzelner Signale an den trainierten Agenten.