English Русский 中文 Español 日本語 Português
Entwicklung eines Wiedergabesystems — Marktsimulation (Teil 10): Nur echte Daten für das Replay verwenden

Entwicklung eines Wiedergabesystems — Marktsimulation (Teil 10): Nur echte Daten für das Replay verwenden

MetaTrader 5Beispiele | 20 November 2023, 09:24
220 0
Daniel Jose
Daniel Jose

Einführung

Im vorigen Artikel „Entwicklung eines Wiedergabesystems — Marktsimulation (Teil 09): Nutzerdefinierte Ereignisse“ haben wir uns angesehen, wie man nutzerdefinierte Ereignisse auslöst. Wir haben auch ein sehr interessantes System für die Interaktion des Indikators mit dem Dienst entwickelt. In diesem Artikel möchte ich betonen, wie wichtig es ist, gehandelte Ticks zu erhalten. Wenn Sie diese Praxis noch nicht ausprobiert haben, sollten Sie ernsthaft in Erwägung ziehen, sie täglich durchzuführen. Wir erfassen alle gehandelten Werte der Anlage, die Sie im Detail untersuchen müssen.

Es ist sinnlos, nach einem Wundermittel zu suchen, um verlorene Daten wiederherzustellen. Wenn Informationen einmal verloren gegangen sind, ist es unmöglich, sie wiederzuerlangen, ganz gleich, welche Methode verwendet wird. Wenn Sie einen bestimmten Vermögenswert wirklich verstehen wollen, sollten Sie dies nicht auf später verschieben. Bewahren Sie gehandelte Ticks so bald wie möglich an einem sicheren Ort auf, denn sie sind unbezahlbar. Im Laufe der Zeit werden Sie feststellen, dass die Daten des Balkens möglicherweise nicht mit den Werten der gehandelten Ticks übereinstimmen. Diese Diskrepanz kann das Verständnis erschweren, insbesondere für diejenigen, die mit dem Finanzmarkt weniger vertraut sind.

Manchmal wirken sich bestimmte Ereignisse auf die Werte aus, z. B. weil die 1-Minuten-Balken nicht die tatsächlichen Vorgänge widerspiegeln. Es gibt verschiedene Ereignisse, wie z. B. die Bekanntgabe von Gewinnen, die Gruppierung oder Aufteilung eines Vermögenswerts, den Verfall eines Vermögenswerts (im Falle von Futures), die Ausgabe von Anleihen usw. Dies sind alles Beispiele für Situationen, die den aktuellen Wert von 1-Minuten-Balken verzerren können, sodass sie nicht mit der Realität übereinstimmen, die zu diesem Zeitpunkt oder in diesem Zeitraum gehandelt wird.

Das liegt daran, dass der Preis des Vermögenswerts bei Eintreten dieser Ereignisse in einer bestimmten Weise angepasst wird. So sehr wir uns auch bemühen, wir werden nicht in der Lage sein, die Anpassung vorzunehmen, die 1-Minuten-Balken mit gehandelten Ticks gleichsetzt. Aus diesem Grund sind Ticks unbezahlbar.

HINWEIS: Einige mögen sagen, dass die Werte gleich sind, wenn wir die Differenz addieren. Dies ist jedoch nicht die Frage. Das Dilemma besteht darin, dass der Markt auf eine bestimmte Art und Weise reagiert, wenn er den Preis als in einer bestimmten Spanne liegend wahrnimmt. Wenn wir sie bei einem anderen wahrnehmen, ist die Reaktion eine andere.

Daher müssen wir in unserem Wiedergabe-/Simulationssystem eine Anpassung vornehmen, um ausschließlich gehandelte Tick-Dateien zu verwenden. Obwohl wir dies seit Beginn dieser Serie getan haben, verwenden wir die in den gehandelten Ticks enthaltenen Daten immer noch nicht wirklich, außer um Wiederholungen zu erstellen. Die Situation wird sich jedoch bald ändern. Nun werden wir auch zulassen, dass diese Dateien und damit die darin enthaltenen Daten zur Erstellung von Vorschau-Balken verwendet werden. Dies wird die derzeitige Methode ersetzen, bei der wir nur Dateien mit 1-Minuten-Vorschau-Balken verwenden.


Code-Entwicklung

Der größte Teil des Codes ist bereits implementiert, sodass wir in dieser Hinsicht nicht mehr viel Arbeit haben werden. Wir müssen jedoch über die Konfigurationsdatei nachdenken, die während der Wiedergabe/Simulation verwendet wird. Als wir anfingen, diese Datei zu verwenden, wurde uns klar, dass sie für uns von entscheidender Bedeutung war. Das Problem ist (wenn gleich nicht wirklich), dass diese Datei zwei Strukturen hat: eine Struktur, um die gehandelten Ticks aufzulisten, die zur Erstellung der Wiedergabe/Simulation verwendet werden, und eine weitere, um die Balken anzugeben, die als Bewegungsvorschau verwendet werden, die nicht in die Simulation einbezogen, sondern nur dem Asset hinzugefügt werden.

Dies ermöglicht es Ihnen, die Informationen korrekt zu trennen und zu korrigieren, aber jetzt stehen wir vor einem kleinen Problem: Wenn die Tick-Datei in der Balkenstruktur enthalten ist, wird das System eine Fehlerwarnung ausgeben. Wir wollen das so beibehalten, damit nicht die Gefahr besteht, dass durch einen Tippfehler bei der Erstellung der Konfigurationsdatei ein Replay/Simulation entsteht, bei dem die Dinge außer Kontrolle geraten. Das Gleiche passiert, wenn wir versuchen, eine 1-Minuten-Balken-Datei hinzuzufügen, als ob es sich um eine gehandelte Tick-Datei handeln würde: Das System wird dies einfach als Fehler behandeln.

Wie können wir dann einen Teil dieses Dilemmas lösen und Tickdateien als Vorschau-Balken verwenden, damit das System dies nicht als Fehler wahrnimmt? Hier werde ich eine von mehreren möglichen Lösungen anbieten. Der erste Schritt besteht darin, der Header-Datei C_Replay.mqh eine neue Definition hinzuzufügen.

#define def_STR_FilesBar        "[BARS]"
#define def_STR_FilesTicks      "[TICKS]"
#define def_STR_TicksToBars     "[TICKS->BARS]"

// ... The rest of the code...

Anhand dieser Definition lässt sich feststellen, wann eine oder mehrere Tickdateien als Balkendateien behandelt werden sollten. Dies wird die nächsten Schritte erleichtern. Es gibt jedoch noch ein weiteres Detail: Wir werden uns nicht darauf beschränken, diese Definition hinzuzufügen. Wir werden dem Nutzer auch erlauben, sie wie andere Definitionen zu ändern, aber auf eine elegantere Weise. Dadurch wird möglicherweise alles klarer und verständlicher für den Nutzer. Für das System ergeben sich jedoch keine großen Änderungen, da es weiterhin alles korrekt interpretieren wird.

Wenn der Nutzer also beschließt, die folgende Definition in die Konfigurationsdatei einzugeben:

[ TICKS -> BARS ]


Dies sollte als eine gültige Definition verstanden werden. Gleichzeitig haben wir die Parameter ein wenig erweitert, um sie für den Nutzer leichter verständlich zu machen. Der Grund dafür ist, dass manche Leute es vorziehen, Daten nicht zu gruppieren, sondern sie logisch zu trennen, was vollkommen akzeptabel ist und wir zulassen können. Um diese Flexibilität zu gewährleisten und dem Nutzer die Möglichkeit zu geben, die bereitgestellten Definitionen geringfügig zu „verändern“, müssen wir dem Code des Dienstes einige Details hinzufügen. Dies macht den Code übersichtlicher und ermöglicht es uns, zukünftige Funktionen so einfach wie möglich zu erweitern. Zu diesem Zweck erstellen wir einen Enumerator. Es gibt jedoch ein Detail, das unten zu sehen ist:

class C_Replay
{
        private :
                enum eTranscriptionDefine {Transcription_FAILED, Transcription_INFO, Transcription_DEFINE};
                int      m_ReplayCount;
                datetime m_dtPrevLoading;

// ... The rest of the code...

Dieser Enumerator ist für die Klasse C_Replay privat, d. h. er ist außerhalb der Klasse nicht zugänglich. Indem wir sie in einem privaten Deklarationsblock unterbringen, können wir auch die Komplexität der Konfigurationsdatei leicht erhöhen. Wir werden dieses Detail jedoch in zukünftigen Artikeln dieser Serie behandeln, da es für diesen Artikel zu umfangreich ist.

Danach können wir uns auf den nächsten Punkt konzentrieren, der umgesetzt werden muss. Bei diesem Element handelt es sich um eine Funktion, mit der die Klassifizierung der in der Konfigurationsdatei enthaltenen Daten festgelegt werden kann. Schauen wir uns einmal an, wie dieser Prozess funktioniert. Wir werden hier den folgenden Code verwenden:

inline eTranscriptionDefine GetDefinition(const string &In, string &Out)
{
        string szInfo;
        
        szInfo = In;
        Out = "";
        StringToUpper(szInfo);
        StringTrimLeft(szInfo);
        StringTrimRight(szInfo);
        if (StringSubstr(szInfo, 0, 1) != "[")
        {
                Out = szInfo;
                return Transcription_INFO;
        }
        for (int c0 = 0; c0 < StringLen(szInfo); c0++)
                if (StringGetCharacter(szInfo, c0) > ' ')
                        StringAdd(Out, StringSubstr(szInfo, c0, 1));                                    
                
        return Transcription_DEFINE;
}


Hier wollen wir den gesamten Prozess der Vorkonvertierung von Daten aus der Konfigurationsdatei zentralisieren. Unabhängig davon, wie die weitere Arbeit abläuft, wird die oben genannte Funktion alle Vorprüfungen durchführen, um sicherzustellen, dass die empfangenen Daten einem bestimmten Muster entsprechen. Als Erstes müssen alle Zeichen in Großbuchstaben umgewandelt werden. Danach entfernen wir alle Elemente, die für die nachfolgenden Schritte nicht benötigt werden. Ist dies geschehen, prüfen wir das erste Zeichen der Sequenz, und wenn es sich von „[“ unterscheidet, erhalten wir einen Hinweis darauf, dass die Sequenz eher eine Information als eine Definition darstellt.

In diesem speziellen Fall geben wir einfach das Ergebnis des zuvor ausgeführten Prozesses zurück. Wenn dies nicht der Fall ist, werden wir uns auf die Suche nach diesen Definitionen machen. Auch wenn sie in einem anderen Format vorliegen, kann der Inhalt angemessen und korrekt sein. Beim Lesen müssen wir jedes Zeichen ignorieren, dessen Wert kleiner als ein Leerzeichen ist. Selbst wenn wir also schreiben: [ B A R S ], interpretiert das System das als [BARS]. In diesem Fall kann die Eingabe leichte Unterschiede in der Schreibweise aufweisen, aber solange der Inhalt mit den Erwartungen übereinstimmt, werden wir das entsprechende Ergebnis erhalten.

Wir haben jetzt ein neues System zum Lesen und Konfigurieren auf der Grundlage des Inhalts der Konfigurationsdatei.

bool SetSymbolReplay(const string szFileConfig)
{
        int     file;
        string  szInfo;
        bool    isBars = true;
                                
        if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
        {
                MessageBox("Failed to open\nconfiguration file.", "Market Replay", MB_OK);
                return false;
        }
        Print("Loading data for replay. Please wait....");
        ArrayResize(m_Ticks.Rate, def_BarsDiary);
        m_Ticks.nRate = -1;
        m_Ticks.Rate[0].time = 0;
        while ((!FileIsEnding(file)) && (!_StopFlag)) switch (GetDefinition(FileReadString(file), szInfo))
        {
		case Transcription_DEFINE:
			if (szInfo == def_STR_FilesBar) isBars = true; else
                        if (szInfo == def_STR_FilesTicks) isBars = false;
                        break;
		case Transcription_INFO:
			if (szInfo != "") if (!(isBars ? LoadPrevBars(szInfo) : LoadTicksReplay(szInfo)))
			{
				if (!_StopFlag)
					MessageBox(StringFormat("File %s from %s\ncould not be loaded.", szInfo, (isBars ? def_STR_FilesBar : def_STR_FilesTicks), "Market Replay", MB_OK));
				FileClose(file);
				return false;
			}
			break;
	}
        FileClose(file);

        return (!_StopFlag);
}


Sie sollten sich jedoch nicht zu sehr freuen. Das Einzige, was sich hier geändert hat, ist die Struktur, die vorher vorhanden war. Die Vorgehensweise bleibt dieselbe. Wir können immer noch keine Dateien mit gehandelten Ticks als 1-Minuten-Vorschau-Balken verwenden. Wir müssen den obigen Code einrichten.

Wenn wir genau hinschauen, werden wir feststellen, dass wir die Fähigkeit erlangt haben, das zu tun, was vorher unmöglich war. Stellen Sie sich vor: Die Daten werden an eine Prozedur gesendet, die alle Vorverarbeitungen der aus der Konfigurationsdatei gelesenen Daten zentralisiert. Die im obigen Code verwendeten Daten sind bereits „sauber“. Bedeutet dies, dass wir Kommentare in die Konfigurationsdatei aufnehmen können? Die Antwort lautet JA. Jetzt können wir dies tun. Wir müssen nur das Format des Kommentars bestimmen: ob er eine oder mehrere Zeilen hat. Beginnen wir mit einem einzeiligen Kommentar. Das Verfahren ist einfach und klar:

inline eTranscriptionDefine GetDefinition(const string &In, string &Out)
{
        string szInfo;
                                
        szInfo = In;
        Out = "";
        StringToUpper(szInfo);
        StringTrimLeft(szInfo);
        StringTrimRight(szInfo);
        if (StringSubstr(szInfo, 0, 1) == "#") return Transcription_INFO;
        if (StringSubstr(szInfo, 0, 1) != "[")
        {
                Out = szInfo;
                return Transcription_INFO;
        }
        for (int c0 = 0; c0 < StringLen(szInfo); c0++)
                if (StringGetCharacter(szInfo, c0) > ' ')
                        StringAdd(Out, StringSubstr(szInfo, c0, 1));                                    
                                
        return Transcription_DEFINE;
}


Wenn das angegebene Zeichen am Anfang der Zeile steht, wird sie als Kommentar behandelt und insgesamt ignoriert. Jetzt können wir also Kommentare in die Konfigurationsdatei einfügen. Interessant, nicht wahr? Durch einfaches Hinzufügen einer Codezeile unterstützen wir nun Kommentare. Doch kommen wir zurück zum Hauptproblem. Unser Code berücksichtigt noch keine Dateien mit gehandelten 1-Minuten-Balken. Dazu müssen wir einige Änderungen vornehmen. Bevor wir solche Änderungen vornehmen, müssen wir sicherstellen, dass das System weiterhin wie bisher funktioniert, aber mit einigen neuen Funktionen. So erhalten wir den folgenden Code:

bool SetSymbolReplay(const string szFileConfig)
        {
#define macroERROR(MSG) { FileClose(file); if (MSG != "") MessageBox(MSG, "Market Replay", MB_OK); return false; }
                int     file,
                        iLine;
                string  szInfo;
                char    iStage;
                                
                if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
                {
                        MessageBox("Failed to open\nconfiguration file.", "Market Replay", MB_OK);
                        return false;
                }
                Print("Loading data for replay. Please wait....");
                ArrayResize(m_Ticks.Rate, def_BarsDiary);
                m_Ticks.nRate = -1;
                m_Ticks.Rate[0].time = 0;
                iStage = 0;
                iLine = 1;
                while ((!FileIsEnding(file)) && (!_StopFlag))
                {
                        switch (GetDefinition(FileReadString(file), szInfo))
                        {
                                case Transcription_DEFINE:
                                        if (szInfo == def_STR_FilesBar) iStage = 1; else
                                        if (szInfo == def_STR_FilesTicks) iStage = 2; else
                                        if (szInfo == def_STR_TicksToBars) iStage = 3; else
                                                macroERROR(StringFormat("%s is not recognized in system\nin line %d.", szInfo, iLine));
                                        break;
                                case Transcription_INFO:
                                        if (szInfo != "") switch (iStage)
                                        {
                                                case 0:
                                                        macroERROR(StringFormat("Couldn't recognize command in line %d\nof the configuration file.", iLine));
                                                        break;
                                                case 1:
                                                        if (!LoadPrevBars(szInfo)) macroERROR(StringFormat("This line is declared in line %d", iLine));
                                                        break;
                                                case 2:
                                                        if (!LoadTicksReplay(szInfo)) macroERROR(StringFormat("This line is declared in line %d", iLine));
                                                        break;
                                                case 3:
                                                        break;
                                        }
                                        break;
                        };
                        iLine++;
                }
                FileClose(file);
                return (!_StopFlag);
#undef macroERROR
        }


Wenn wir diese Prozedur aufrufen, werden als erstes einige Parameter für unsere Zwecke initialisiert. Wir zählen die Zeilen - dies ist notwendig, damit das System genau melden kann, wo der Fehler aufgetreten ist. Aus diesem Grund haben wir ein Makro, das eine allgemeine Fehlermeldung erzeugt, die in verschiedenen Situationen verwendet wird. Unser System wird nun in Schritten arbeiten. Daher müssen wir in den folgenden Zeilen explizit festlegen, was verarbeitet werden soll. Das Überspringen dieses Schrittes wird als Fehler gewertet. In diesem Fall wird der erste Fehler immer gleich Null sein, da wir bei der Initialisierung des Verfahrens angegeben haben, dass wir uns in der Phase Null der Analyse befinden. 

Obwohl diese Strukturierung komplex erscheint, ermöglicht sie es uns, jeden gewünschten Aspekt schnell und effizient zu erweitern. Hinzufügungen wirken sich selten auf den vorherigen Code aus. Dieser Ansatz ermöglicht es uns, eine Konfigurationsdatei mit einem internen Format wie diesem zu verwenden:

#First set initial bars, I think 3 is enough ....
[Bars]
WIN$N_M1_202108020900_202108021754
WIN$N_M1_202108030900_202108031754
WIN$N_M1_202108040900_202108041754

#I have a tick file but I will use it for pre-bars ... thus we have 4 files with bars
[ Ticks -> Bars]
WINQ21_202108050900_202108051759

#Now use the file of traded ticks to run replay ...
[Ticks]
WINQ21_202108060900_202108061759

#End of the configuration file...


Jetzt können Sie klären, was vor sich geht, und ein effizienteres Format verwenden. Aber wir haben noch nicht umgesetzt, was wir wollen. Ich zeige nur, wie man mit kleinen Änderungen am Code die Dinge interessanter gestalten kann. Und diese Veränderungen sind nicht so schwierig, wie Sie vielleicht denken.

Fahren wir fort. Wir werden die notwendigen Änderungen vornehmen, um die gehandelten Tick-Dateien als Vorschau-Balken zu verwenden. Und bevor Sie mit der Entwicklung eines komplexen Codes beginnen, möchte ich Sie darauf hinweisen, dass der erforderliche Code bereits im Wiedergabe-/Simulationsdienst vorhanden ist. Sie ist nur unter der Komplexität versteckt. Nun müssen wir diesen Code extrahieren und veröffentlichen. Diese Arbeit muss sehr sorgfältig ausgeführt werden, da jeder Fehler das gesamte bestehende System gefährden kann. Um dies zu verstehen, schauen wir uns den folgenden Code an, der im vorherigen Artikel erwähnt wurde:

bool LoadTicksReplay(const string szFileNameCSV)
{
        int     file,
                old;
        string  szInfo = "";
        MqlTick tick;
        MqlRates rate;
                        
        if ((file = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
        {
                ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                ArrayResize(m_Ticks.Rate, def_BarsDiary, def_BarsDiary);
                old = m_Ticks.nTicks;
                for (int c0 = 0; c0 < 7; c0++) szInfo += FileReadString(file);
                if (szInfo != def_Header_Ticks)
                {
                        Print("File ", szFileNameCSV, ".csv is not a file with traded ticks.");
                        return false;
                }
                Print("Loading ticks for replay. Wait...");
                while ((!FileIsEnding(file)) && (m_Ticks.nTicks < (INT_MAX - 2)) && (!_StopFlag))
                {
                        ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                        szInfo = FileReadString(file) + " " + FileReadString(file);
                        tick.time = macroRemoveSec(StringToTime(StringSubstr(szInfo, 0, 19)));
                        tick.time_msc = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                        tick.bid = StringToDouble(FileReadString(file));
                        tick.ask = StringToDouble(FileReadString(file));
                        tick.last = StringToDouble(FileReadString(file));
                        tick.volume_real = StringToDouble(FileReadString(file));
                        tick.flags = (uchar)StringToInteger(FileReadString(file));
                        if ((m_Ticks.Info[old].last == tick.last) && (m_Ticks.Info[old].time == tick.time) && (m_Ticks.Info[old].time_msc == tick.time_msc))
                                m_Ticks.Info[old].volume_real += tick.volume_real;
                        else
                        {                                                       
                                m_Ticks.Info[m_Ticks.nTicks] = tick;
                                if (tick.volume_real > 0.0)
                                {
                                        m_Ticks.nRate += (BuiderBar1Min(rate, tick) ? 1 : 0);
                                        rate.spread = m_Ticks.nTicks;
                                        m_Ticks.Rate[m_Ticks.nRate] = rate;
                                        m_Ticks.nTicks++;
                                }
                                old = (m_Ticks.nTicks > 0 ? m_Ticks.nTicks - 1 : old);
                        }
                }
                if ((!FileIsEnding(file)) && (!_StopFlag))
                {
                        Print("Too much data in the tick file.\nCannot continue...");
                        return false;
                }
        }else
        {
                Print("Tick file ", szFileNameCSV,".csv not found...");
                return false;
        }
        return (!_StopFlag);
};


Obwohl dieser Code dazu gedacht ist, gehandelte Ticks zu lesen und zu speichern, um sie später als Replay/Simulation zu verwenden, gibt es einen wichtigen Punkt zu beachten. Irgendwann werden wir etwas erstellen, das einem 1-Minuten-Balken entspricht und nur gehandelte Tick-Daten verwendet.

Denken wir nach: Ist das nicht genau das, was wir tun wollen? Wir möchten eine Datei mit gehandelten Ticks lesen und einen 1-Minuten-Balken erstellen und diesen dann in einem Asset speichern, das für die Wiedergabe/Simulation verwendet wird. Wir speichern ihn jedoch nicht als gehandelten Tick, sondern als Daten, die als Vorschau-Balken interpretiert werden. Wenn wir also, anstatt diese Ticks einfach nur zu speichern, kleine Änderungen an dem genannten Code vornehmen, können wir den beim Lesen der gehandelten Ticks erstellten Balken als Vorschau-Balken in das Asset zur Analyse in der Wiedergabe/Simulation einfügen.

Bei dieser Aufgabe muss ein wichtiges Detail berücksichtigt werden. Wir werden dies bei der Besprechung der Implementierung behandeln, denn Sie werden es erst verstehen, wenn Sie sehen, wie der Code tatsächlich aufgebaut ist und funktioniert. Schauen wir uns zunächst den Code an, der für die Umwandlung der gehandelten Ticks in Balken verantwortlich ist. Er ist weiter unten dargestellt:

bool SetSymbolReplay(const string szFileConfig)
        {
#define macroERROR(MSG) { FileClose(file); MessageBox((MSG != "" ? MSG : StringFormat("Error occurred in line %d", iLine)), "Market Replay", MB_OK); return false; }
                int     file,
                        iLine;
                string  szInfo;
                char    iStage;
                                
                if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
                {
                        MessageBox("Failed to open\nconfiguration file.", "Market Replay", MB_OK);
                        return false;
                }
                Print("Loading data for replay. Please wait....");
                ArrayResize(m_Ticks.Rate, def_BarsDiary);
                m_Ticks.nRate = -1;
                m_Ticks.Rate[0].time = 0;
                iStage = 0;
                iLine = 1;
                while ((!FileIsEnding(file)) && (!_StopFlag))
                {
                        switch (GetDefinition(FileReadString(file), szInfo))
                        {
                                case Transcription_DEFINE:
                                        if (szInfo == def_STR_FilesBar) iStage = 1; else
                                        if (szInfo == def_STR_FilesTicks) iStage = 2; else
                                        if (szInfo == def_STR_TicksToBars) iStage = 3; else
                                                macroERROR(StringFormat("%s is not recognized in system\nin line %d.", szInfo, iLine));
                                        break;
                                case Transcription_INFO:
                                        if (szInfo != "") switch (iStage)
                                        {
                                                case 0:
                                                        macroERROR(StringFormat("Couldn't recognize command in line %d\nof the configuration file.", iLine));
                                                        break;
                                                case 1:
                                                        if (!LoadPrevBars(szInfo)) macroERROR("");
                                                        break;
                                                case 2:
                                                        if (!LoadTicksReplay(szInfo)) macroERROR("");
                                                        break;
                                                case 3:
                                                        if (!LoadTicksReplay(szInfo, false)) macroERROR("");
                                                        break;
                                        }
                                        break;
                        };
                        iLine++;
                }
                FileClose(file);
                        return (!_StopFlag);
#undef macroERROR
        }


Wir haben zwar eine kleine Korrektur am Fehlermeldesystem vorgenommen, um die Meldungen zu vereinheitlichen, aber das ist nicht unser Ziel. Wir sind wirklich an dem Aufruf interessiert, der 1-Minuten-Balken aus gehandelten Ticks erzeugt. Beachten Sie, dass der Aufruf identisch ist mit dem, den wir zuvor verwendet haben, nur mit einem zusätzlichen Parameter. Dieses einfache zusätzliche Parameterdetail ist entscheidend und erspart es uns, die gesamte Funktion neu zu schreiben, wie im folgenden Code zu sehen ist.

Die gesamte erforderliche Logik ist bereits in der ursprünglichen Replay-/Simulationsdienstfunktion vorhanden, um die Daten aus der gehandelten Tick-Datei zu verarbeiten und in 1-Minuten-Balken zu konvertieren. Was wir wirklich tun müssen, ist, diese Balken als vorläufige Balken anzupassen, ohne die Gesamtleistung zu beeinträchtigen. Denn ohne die notwendigen Änderungen können bei der Nutzung des Replay/Simulationsdienstes Fehler auftreten. Schauen wir uns also die Änderungen an, die am Code zum Lesen der gehandelten Ticks vorgenommen wurden. So kann das System diese Ticks als Balken verwenden.

bool LoadTicksReplay(const string szFileNameCSV, const bool ToReplay = true)
{
        int     file,
                old,
                MemNRates,
                MemNTicks;
        string  szInfo = "";
        MqlTick tick;
        MqlRates rate,
                RatesLocal[];
                                
        MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
        MemNTicks = m_Ticks.nTicks;
        if ((file = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
        {
                ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                ArrayResize(m_Ticks.Rate, def_BarsDiary, def_BarsDiary);
                old = m_Ticks.nTicks;
                for (int c0 = 0; c0 < 7; c0++) szInfo += FileReadString(file);
                if (szInfo != def_Header_Ticks)
                {
                        Print("File", szFileNameCSV, ".csv is not a file with traded ticks.");
                        return false;
                }
                Print("Loading ticks for replay. Wait...");
                while ((!FileIsEnding(file)) && (m_Ticks.nTicks < (INT_MAX - 2)) && (!_StopFlag))
                {
                        ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                        szInfo = FileReadString(file) + " " + FileReadString(file);
                        tick.time = macroRemoveSec(StringToTime(StringSubstr(szInfo, 0, 19)));
                        tick.time_msc = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                        tick.bid = StringToDouble(FileReadString(file));
                        tick.ask = StringToDouble(FileReadString(file));
                        tick.last = StringToDouble(FileReadString(file));
                        tick.volume_real = StringToDouble(FileReadString(file));
                        tick.flags = (uchar)StringToInteger(FileReadString(file));
                        if ((m_Ticks.Info[old].last == tick.last) && (m_Ticks.Info[old].time == tick.time) && (m_Ticks.Info[old].time_msc == tick.time_msc))
                                m_Ticks.Info[old].volume_real += tick.volume_real;
                        else
                        {                                                       
                                m_Ticks.Info[m_Ticks.nTicks] = tick;
                                if (tick.volume_real > 0.0)
                                {
                                        m_Ticks.nRate += (BuiderBar1Min(rate, tick) ? 1 : 0);
                                        rate.spread = (ToReplay ? m_Ticks.nTicks : 0);
                                        m_Ticks.Rate[m_Ticks.nRate] = rate;
                                        m_Ticks.nTicks++;
                                }
                                old = (m_Ticks.nTicks > 0 ? m_Ticks.nTicks - 1 : old);
                        }
                }
                if ((!FileIsEnding(file)) && (!_StopFlag))
                {
                        Print("Too much data in the tick file.\nCannot continue...");
                        FileClose(file);
                        return false;
                }
                FileClose(file);
        }else
        {
                Print("Tick file ", szFileNameCSV,".csv not found...");
                return false;
        }
        if ((!ToReplay) && (!_StopFlag))
        {
                ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates));
                ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0);
                CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates));
                m_dtPrevLoading = m_Ticks.Rate[m_Ticks.nRate].time;
                m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates);
                m_Ticks.nTicks = MemNTicks;
                ArrayFree(RatesLocal);
        }
        return (!_StopFlag);
};


Zunächst müssen wir einige zusätzliche Variablen hinzufügen. In diesen lokalen Variablen werden wichtige Informationen vorübergehend gespeichert, sodass Sie das System später bei Bedarf in seinen ursprünglichen Zustand zurückversetzen können. Beachten Sie, dass der gesamte Code mit dem Original identisch bleibt. Wir könnten 1-Minuten-Balken hinzufügen, wenn sie erscheinen, aber ich habe einen anderen Ansatz gewählt. Natürlich musste ich an einer bestimmten Stelle Änderungen vornehmen, nämlich genau dort, wo wir bewerten, ob die Daten als Balken gelten oder nicht. Wenn die Daten als Balken verwendet werden, müssen wir für eine einheitliche und logische Darstellung sorgen.

Außerdem stellen wir sicher, dass die Ablesung ohne Beeinträchtigung des ursprünglichen Verfahrens durchgeführt wird. Die gehandelten Ticks werden ausgelesen und in 1-Minuten-Balken umgewandelt, hier gibt es keine Änderungen. Sobald die Datei jedoch vollständig gelesen wurde und alles wie erwartet funktioniert, beginnt eine neue Phase. Und das ist der Punkt, an dem die wirkliche Veränderung stattfindet. Wenn die Datei mi den Ticks als Vorschau-Balkensystem zugewiesen wurde und der Nutzer während des Lesens nicht die Beendigung des Wiedergabe-/Simulationsdienstes angefordert hat, dann haben wir einen echten Zustand gefunden. Anschließend ergreifen wir gezielte Maßnahmen, um die korrekte Verwendung der Daten zu gewährleisten und das System in seinen ursprünglichen Zustand zurückzuversetzen. So vermeiden wir Probleme und ungewöhnliche Situationen in der Spielphase.

Der erste Schritt besteht darin, Speicher für die temporäre Speicherung von 1-Minuten-Balken zuzuweisen. Danach werden die beim Lesen der Datei erstellten Balken in diesen temporären Bereich verschoben. Diese Aktion ist entscheidend für den nächsten Schritt, nämlich das Einfügen von Balken in die Wiedergabe-/Simulationsanlage, um deren Erfolg sicherzustellen. Ohne diese vorherige Aktion wäre es schwierig, die 1-Minuten-Balken im Asset korrekt zu positionieren, sodass sie als Vorschau-Balken interpretiert werden würden. Hätten wir uns für die direkte Methode des Hinzufügens von Balken zum Zeitpunkt der Erstellung entschieden, wäre diese ganze Logik nicht nötig. Bei der gewählten Methode, bei der wir zuerst die Datei lesen und dann die Balken speichern, brauchen wir jedoch diese Manipulationen, um einen korrekten Betrieb zu gewährleisten.

Durch die Verteilung und Übertragung von Daten auf diese Weise wird der Prozess vereinfacht, da keine Schleife für die Übertragung erstellt werden muss. Nach Abschluss der Übersetzung korrigieren wir den Wert der Endposition der Balken und stellen die zu Beginn des Verfahrens gespeicherten Werte wieder her. Auf diese Weise funktioniert das System so, als ob sich nichts geändert hätte. Für das System sieht es so aus, als ob das Lesen der Balken direkt aus der 1-Minuten-Balkendatei und nicht aus dem Tick-Chart erfolgt wäre. Die Balken werden wie erwartet angezeigt, und wir können endlich etwas temporären Speicherplatz freigeben.

Auf diese Weise haben wir mit minimalem Aufwand ein ernstes Problem gelöst. Die vorgeschlagene Lösung ist jedoch nicht die einzig mögliche, auch wenn sie anscheinend die wenigsten Änderungen am Quellcode erfordert.


Entfernen aller Grafiken des Replays

Es gibt ein interessantes Detail in dem System, wenn es geschlossen wird. Normalerweise geschieht dies, wenn wir ein Chart schließen, das einen Kontrollindikator enthält. Eigentlich ist es kein Problem, sondern eine kleine Unannehmlichkeit. Viele Leute ziehen es vor, mehrere Charts desselben Assets gleichzeitig zu öffnen. Das ist normal und verständlich und kann in manchen Situationen nützlich sein. Aber bedenken Sie: Wenn der Replay-/Simulationsdienst versucht, Spuren eines verwendeten Assets zu entfernen, schlägt er einfach fehl. Der Grund dafür ist einfach: Es gibt ein weiteres Chart mit einem Replay-Asset.

Daher kann das System diese Spuren nicht beseitigen. Es gibt noch einen Vermögenswert im Marktbeobachtungsfenster, der keinen Sinn macht. Sie kann zwar manuell entfernt werden, aber das ist nicht das, was wir brauchen. Der Replay-/Simulationsdienst sollte alle Spuren automatisch beseitigen. Um dies zu beheben, müssen wir einige Änderungen am Code vornehmen. Um wirklich zu verstehen, was hinzugefügt wird, schauen wir uns den Quellcode unten an:

void CloseReplay(void)
{
        ArrayFree(m_Ticks.Info);
        ArrayFree(m_Ticks.Rate);
        ChartClose(m_IdReplay);
        SymbolSelect(def_SymbolReplay, false);
        CustomSymbolDelete(def_SymbolReplay);
        GlobalVariableDel(def_GlobalVariableReplay);
        GlobalVariableDel(def_GlobalVariableIdGraphics);
}


Achten Sie auf das Verhalten dieses Codes: Wenn Sie ihn aufrufen, wird nur das Chart geschlossen, das vom Dienst geöffnet wurde. Wenn es keine anderen offenen Charts gibt, die sich auf das wiedergegebene Asset beziehen, können Sie es aus dem Marktbeobachtungsfenster entfernen. Wie bereits erwähnt, kann der Nutzer jedoch auch andere Charts mit Hilfe des Marktwiedergabe-Assets öffnen. Wenn wir in dieser Situation versuchen, den Vermögenswert aus dem Marktbeobachtungsfenster zu entfernen, schlagen wir fehl. Um dieses Problem zu lösen, müssen wir die Art und Weise ändern, wie der Dienst beendet wird. Für eine komplexere, aber auch effektivere Lösung müssen wir die entsprechenden Zeilen anpassen. Der entsprechende Code ist nachstehend aufgeführt:

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


Dieser Code mag seltsam erscheinen, aber seine Funktion ist recht einfach. Zunächst erfassen wir die ID des ersten Charts. Es ist zu beachten, dass sie nicht unbedingt zuerst geöffnet wird. Als Nächstes starten wir eine Schleife. In dieser Schleife wird geprüft, auf welches Asset das Chart verweist. Wenn es sich um die Wiedergabemöglichkeit handelt, wird die Grafik geschlossen. Um festzustellen, ob die Schleife abgeschlossen werden soll oder nicht, fordern wir die Plattform auf, uns die ID des nächsten Charts in der Liste mitzuteilen. Wenn die Liste keine weiteren Charts enthält, wird ein Wert kleiner als Null zurückgegeben und die Schleife beendet. Andernfalls wird die Schleife erneut ausgeführt. Dadurch wird sichergestellt, dass alle Fenster, deren Asset dasjenige ist, das wir zur Wiedergabe verwenden, unabhängig von ihrer Anzahl geschlossen werden. Anschließend werden zwei Versuche unternommen, das Replay-Asset aus dem Marktbeobachtungsfenster zu entfernen. Der Grund für die zwei Versuche ist, dass ein Versuch ausreicht, um das Asset zu entfernen, wenn nur das Fenster des Wiedergabe-/Simulationsdienstes geöffnet ist. Wenn der Nutzer jedoch andere Fenster geöffnet hat, kann ein zweiter Versuch erforderlich sein.

In der Regel ist ein Versuch ausreichend. Wenn wir das Asset nicht aus dem Market Watch-Fenster entfernen können, können wir auch das nutzerdefinierte Symbol nicht entfernen. Dennoch werden wir alle Inhalte in der Ressource der Nutzer entfernen, da sie außerhalb des Wiedergabe-/Simulationsdienstes nicht von Nutzen sind. Dort sollte nichts zurückgelassen werden. Selbst wenn wir das nutzerdefinierte Asset nicht vollständig entfernen können, ist das kein großes Problem, da sich nichts darin befinden wird. Das Ziel dieses Verfahrens ist es jedoch, sie vollständig von der Plattform zu entfernen.


Schlussfolgerung

Im folgenden Video können Sie das Ergebnis der in diesem Artikel vorgestellten Arbeit sehen. Einige Dinge sind vielleicht noch nicht sichtbar, aber wenn Sie sich die Videos ansehen, können Sie sich ein klares Bild von den Fortschritten bei der Wiedergabe/Simulation machen, die in all diesen Artikeln beschrieben werden. Sehen Sie sich einfach das Video an und vergleichen Sie die Veränderungen von Anfang an bis heute.



Im nächsten Artikel werden wir das System weiterentwickeln, da es noch einige wirklich notwendige Funktionen zu implementieren gibt.


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

Beigefügte Dateien |
Market_Replay.zip (13061.7 KB)
Entwicklung eines Replay-Systems — Marktsimulation (Teil 11): Die Geburt des SIMULATORS (I) Entwicklung eines Replay-Systems — Marktsimulation (Teil 11): Die Geburt des SIMULATORS (I)
Um die Daten, die die Balken bilden, nutzen zu können, müssen wir auf das Replay verzichten und einen Simulator entwickeln. Wir werden 1-Minuten-Balken verwenden, weil sie den geringsten Schwierigkeitsgrad aufweisen.
Neuronale Netze leicht gemacht (Teil 48): Methoden zur Verringerung der Überschätzung von Q-Funktionswerten Neuronale Netze leicht gemacht (Teil 48): Methoden zur Verringerung der Überschätzung von Q-Funktionswerten
Im vorigen Artikel haben wir die DDPG-Methode vorgestellt, mit der Modelle in einem kontinuierlichen Aktionsraum trainiert werden können. Wie andere Q-Learning-Methoden neigt jedoch auch DDPG dazu, die Werte der Q-Funktion zu überschätzen. Dieses Problem führt häufig dazu, dass ein Agent mit einer suboptimalen Strategie ausgebildet wird. In diesem Artikel werden wir uns einige Ansätze zur Überwindung des genannten Problems ansehen.
Entwicklung eines Replay-Systems — Marktsimulation (Teil 12): Die Geburt des SIMULATORS (II) Entwicklung eines Replay-Systems — Marktsimulation (Teil 12): Die Geburt des SIMULATORS (II)
Die Entwicklung eines Simulators kann viel interessanter sein, als es scheint. Heute gehen wir ein paar Schritte weiter in diese Richtung, denn die Dinge werden immer interessanter.
Neuronale Netze leicht gemacht (Teil 47): Kontinuierlicher Aktionsraum Neuronale Netze leicht gemacht (Teil 47): Kontinuierlicher Aktionsraum
In diesem Artikel erweitern wir das Aufgabenspektrum unseres Agenten. Der Ausbildungsprozess wird einige Aspekte des Geld- und Risikomanagements umfassen, die ein wesentlicher Bestandteil jeder Handelsstrategie sind.